Skill: christiancattaneo/wreckit-ralph (version 2.4.0)

--- name: reckit description: > Bulletproof AI code verification. The agent IS the engine — no external tools required. Spawns parallel verification workers that slop-scan, type-check, mutation-test, and cross-verify before shipping. Language-agnostic. Framework-agnostic. Now with Swift/iOS support. Use when: (1) Building new projects and need verified, tested code ("build X with tests"), (2) Migrating/rebuilding codebases ("rewrite in TypeScript"), (3) Fixing bugs with proof nothing else broke ("fix this bug, verify no regressions"), (4) Auditing existing code quality ("audit this project", "how good are these tests?"), (5) Any request mentioning "reckit", "wreckit", "mutation testing", "verification", "proof bundle", "code audit", or "bulletproof". Produces a proof bundle (.wreckit/) with gate results and Ship/Caution/Blocked verdict. metadata: openclaw: platforms: [macos, linux] notes: "Uses sessions_spawn for parallel verification swarms. Requires maxSpawnDepth >= 2." --- # Reckit — Bulletproof AI Code Verification Build it. Break it. Prove it works. ## Philosophy AI can't verify itself. Structure the pipeline so it can't silently agree with itself. Separate Builder/Tester/Breaker roles across fresh contexts. Use independent oracles. > **Full 14-step framework:** `references/verification-framework.md` ## Modes Auto-detected from context: | Mode | Trigger | Description | |------|---------|-------------| | 🟢 BUILD | Empty repo + PRD | Full pipeline for greenfield | | 🟡 REBUILD | Existing code + migration spec | BUILD + behavior capture + replay | | 🔴 FIX | Existing code + bug report | Fix, verify, check regressions | | 🔵 AUDIT | Existing code, no changes | Verify and report only | ## Gates Read the gate file before executing it. Each contains: question, checks, pass/fail criteria. | Gate | BUILD | REBUILD | FIX | AUDIT | File | |------|-------|---------|-----|-------|------| | AI Slop Scan | ✅ | ✅ | ✅ | ✅ | `references/gates/slop-scan.md` | | Type Check | ✅ | ✅ | ✅ | ✅ | `references/gates/type-check.md` | | Ralph Loop | ✅ | ✅ | ✅ | ❌ | `references/gates/ralph-loop.md` | | Test Quality | ✅ | ✅ | ✅ | ✅ | `references/gates/test-quality.md` | | Mutation Kill | ✅ | ✅ | ✅ | ✅ | `references/gates/mutation-kill.md` | | Cross-Verify | ✅ | ❌ | ❌ | ❌ | `references/gates/cross-verify.md` | | Behavior Capture | ❌ | ✅ | ❌ | ❌ | `references/gates/behavior-capture.md` | | Regression | ❌ | ✅ | ✅ | ❌ | `references/gates/regression.md` | | SAST | ❌ | ❌ | ✅ | ✅ | `references/gates/sast.md` | | LLM-as-Judge | opt | opt | opt | opt | `references/gates/llm-judge.md` | | Design Review | ❌ | ❌ | ❌ | ✅ | `references/gates/design-review.md` | | CI Integration | ✅ | ✅ | ❌ | ✅ | `references/gates/ci-integration.md` | | Proof Bundle | ✅ | ✅ | ✅ | ✅ | `references/gates/proof-bundle.md` | ## Scripts Deterministic helpers — run these, don't rewrite them: **Core (all modes):** - `scripts/project-type.sh [path]` — classify project context + calibration profile (`skip_gates`, thresholds, tolerated warns) - `scripts/detect-stack.sh [path]` — auto-detect language, framework, test runner → JSON - `scripts/check-deps.sh [path]` — verify all deps exist in registries (hallucination check) - `scripts/slop-scan.sh [path]` — semantic slop scan (tracked vs untracked debt, categorized output) → JSON - `scripts/type-check.sh [path]` — run type checker (tsc/mypy/cargo/go vet) → JSON - `scripts/ralph-loop.sh [path]` — validate IMPLEMENTATION_PLAN.md structure → JSON - `scripts/coverage-stats.sh [path]` — extract raw coverage numbers from test runner - `scripts/mutation-test.sh [path] [test-cmd]` — mutation testing (mutmut/cargo-mutants/Stryker/AI) - `scripts/mutation-test-stryker.sh [path]` — Stryker-specific mutation testing → JSON - `scripts/red-team.sh [path]` — SAST + 20+ vulnerability patterns → JSON - `scripts/regex-complexity.sh [path] [--context library|app]` — targeted ReDoS analysis → JSON - `scripts/proof-bundle.sh [path] [mode]` — corroboration-based aggregation + proof bundle writer - `scripts/run-all-gates.sh [path] [mode] [--log-file]` — sequential gate runner with telemetry + adaptive skipping/tolerance **Mode-specific:** - `scripts/behavior-capture.sh [path]` — capture golden fixtures before rebuild (REBUILD) - `scripts/design-review.sh [path]` — dep graph, coupling, circular deps (AUDIT/REBUILD) → JSON - `scripts/ci-integration.sh [path]` — CI config detection and scoring → JSON - `scripts/differential-test.sh [path]` — oracle comparison, golden tests (BUILD/REBUILD) → JSON **Extended verification:** - `scripts/dynamic-analysis.sh [path]` — memory leaks, race conditions, FD leaks → JSON - `scripts/perf-benchmark.sh [path]` — benchmark detection + regression vs baseline → JSON - `scripts/property-test.sh [path]` — property-based/fuzz testing, generates stubs → JSON **Bootstrap:** - `scripts/run-audit.sh [path] [mode] [--spawn]` — generate orchestrator task + optional spawn ## Swarm Architecture For multi-gate parallel execution, read `references/swarm/orchestrator.md`. **Quick overview:** ``` Main agent → wreckit orchestrator (depth 1) ├─ Planning: Architect worker ├─ Building: Sequential Implementer workers ├─ Verification: Parallel gate workers ├─ Sequential: Cross-verify / regression / judge └─ Decision: Proof bundle → Ship / Caution / Blocked ``` **Critical:** Read `references/swarm/collect.md` before spawning workers. Never fabricate results. Wait for all workers to report back. Worker output format: `references/swarm/handoff.md`. **Config required:** ```json { "agents.defaults.subagents": { "maxSpawnDepth": 2, "maxChildrenPerAgent": 8 } } ``` ## Decision Framework | Verdict | Criteria | |---------|----------| | **Ship** ✅ | No hard blocks; no corroborated multi-domain fail evidence above block threshold | | **Caution** ⚠️ | Single non-hard fail, warning-only risk, or corroboration below block threshold | | **Blocked** 🚫 | Any hard block OR corroborated non-hard failure pattern (multi-signal, multi-domain, high-confidence) | Hard-block + corroboration rule details: `references/gates/corroboration.md` ## Supported Languages & Stacks | Language | Gates Available | Notes | |----------|----------------|-------| | TypeScript/JS | 11/11 | Full support via Stryker, tsc, vitest/jest | | Python | 11/11 | Full support via mutmut, mypy/pyright, pytest | | Rust | 11/11 | Full support via cargo-mutants, cargo check/test | | Go | 11/11 | Full support via go vet, go test | | **Swift (SPM)** | **9/11** | mutation = AI-estimated CAUTION, cross-verify = manual | | **Swift (Xcode)** | **7/11** | type-check = xcodebuild, mutation = AI-estimated, coverage = limited | | **iOS apps** | **7/11** | Same as Xcode projects | | Java/Kotlin | 10/11 | Gradle/Maven, mutation via PIT (manual setup) | | Shell | 8/11 | shellcheck, limited mutation testing | ### Swift Notes - **Mutation testing requires manual verification** — no automated mutation testing tool exists for Swift as of 2026. The mutation gate uses AI-estimated analysis (counts mutation surface, compares to test count) and always outputs `CAUTION`, never `SHIP`. - **SPM projects** get high-confidence type checking via `swift build` (the compiler IS the type checker). - **Xcode projects** get medium-confidence type checking via `xcodebuild` with auto-detected schemes. - **Dependency checking** lists SPM dependencies but notes that no automated CVE database exists for Swift packages — manual review is always recommended. - **CocoaPods** projects: `pod outdated` is checked if Podfile present. - **Build systems detected:** SPM, xcodebuild, CocoaPods, Carthage, mixed. ## Running an Audit (Single-Agent, No Swarm) For small projects or when swarm isn't needed, run gates sequentially: 1. `scripts/detect-stack.sh` → know your target (language, test cmd, type checker) 2. `scripts/check-deps.sh` → verify deps are real (not hallucinated) 3. `scripts/slop-scan.sh` → find placeholders, template artifacts, empty stubs 4. Run type checker (from detect-stack output) → `references/gates/type-check.md` 5. Run tests + `scripts/coverage-stats.sh` → `references/gates/test-quality.md` 6. `scripts/mutation-test.sh` → `references/gates/mutation-kill.md` (uses mutmut/cargo-mutants/Stryker if available) 7. `scripts/red-team.sh` → `references/gates/sast.md` (20+ vulnerability patterns, JSON report) 8. `scripts/design-review.sh` → `references/gates/design-review.md` (dep graph, circular deps, god modules) 9. `scripts/ci-integration.sh` → `references/gates/ci-integration.md` (CI config detection + scoring) 10. `scripts/dynamic-analysis.sh` → `references/gates/dynamic-analysis.md` (memory leaks, race conditions) 11. `scripts/perf-benchmark.sh` → `references/gates/performance.md` (benchmark detection + regression) 12. `scripts/property-test.sh` → `references/gates/property-based.md` (fuzzing, invariant checks) 13. `scripts/differential-test.sh` → `references/gates/differential.md` (oracle comparison, metamorphic tests) 14. echo '[...gate-results-json...]' | `scripts/proof-bundle.sh [path] [mode]` → writes `.wreckit/proof.json`, `dashboard.json`, `decision.md` ## Quick Start ``` "Use wreckit to audit [project]. Don't change anything." "Use wreckit to build [project] from this PRD." "Use wreckit to fix [bug]. Prove nothing else breaks." "Use wreckit to rebuild [project] in [framework]." ``` ## Dashboard `assets/dashboard/` contains a local web dashboard for viewing proof bundles across repos. Run: `node assets/dashboard/server.mjs` (port 3939). Reads `.wreckit/dashboard.json` from projects. ## Codex CLI Notes (2026-02-22) When using Codex CLI to build/run projects: - `--full-auto` sandbox blocks `npm install` network access (ENOTFOUND registry.npmjs.org) - Fix: use [REDACTED_SECRET] flag instead - Auth: `echo "$OPENAI_API_KEY" | codex login --with-api-key` stores credentials to `~/.codex/auth.json` - Config: `~/.codex/config.toml` with `model = "gpt-5.2-codex"` and `[shell_environment_policy] inherit = "all"` - `gpt-5.3-codex` is Copilot/VS Code only — not available via direct API. Use `gpt-5.2-codex`.

path: GAMEPLAN.md
content: # wreckit Build-Out Gameplan
_Sequential execution. Update status as each task completes._

## Status Legend
- [ ] TODO
- [🔄] IN PROGRESS
- [✅] DONE
- [❌] BLOCKED

---

## Task 1: Fix Orchestrator Collect Pattern
**Goal:** Make swarm actually work. No more fabricated results.
**Files:** `references/swarm/orchestrator.md`, `references/swarm/collect.md`, `references/swarm/orchestrator-prompt.md`, `scripts/run-audit.sh`
- [x] Rewrite orchestrator.md — explicit collect loop, anti-fabrication rules, timeout handling
- [x] Create orchestrator-prompt.md — copy-pasteable system prompt with hardcoded collect pattern
- [x] Update collect.md — verification checklist, concrete timeout/error examples
- [x] Create scripts/run-audit.sh — CLI bootstrap that validates config + outputs ready-to-paste orchestrator task
- [x] Fix broken path references (~/Projects/wreckit/ → ~/Projects/wreckit-ralph/) in orchestrator.md + orchestrator-prompt.md
**Status:** [✅] DONE — 2026-02-21

---

## Task 2: Adversarial Red-Team Gate
**Goal:** Actually find security bugs, not just describe what to look for.
**Files:** `references/gates/red-team.md`, `scripts/red-team.sh`
- [x] Gate definition — injection, logic bypass, input abuse, concurrency, resource exhaustion, crypto, error handling
- [x] Script — grep for 20+ vulnerable patterns, run adversarial inputs, JSON report
- [x] Test against ~/Projects/ipeaky/
**Status:** [✅] DONE — red-team.md gate + red-team.sh script (286 lines, 20 vulnerability patterns, JSON report).

---

## Task 3: Differential Testing Gate
**Goal:** Compare implementation against oracle/reference, catch "plausible but wrong" logic.
**Files:** `references/gates/differential.md`, `scripts/differential-test.sh`
- [x] Gate definition — oracle comparison, metamorphic testing, golden tests
- [x] Script — detect oracle, run both, diff outputs, JSON report (271 lines)
- [x] property-based.md + property-test.sh (356 lines, framework detection, stubs for Python/TS/Rust)
**Status:** [✅] DONE

---

## Task 4: Property-Based Testing Gate
**Goal:** Fuzz with random/boundary inputs, find crashes nobody wrote a test for.
**Files:** `references/gates/property-based.md`, `scripts/property-test.sh`
- [x] Gate definition — invariant extraction, fast-check/hypothesis/quickcheck, crash corpus
- [x] Script — detect framework, check if property tests exist, generate stubs if none, run fuzzer, JSON report (356 lines)
**Status:** [✅] DONE

---

## Task 5: Dynamic Analysis Gate
**Goal:** Memory leaks, race conditions, undefined behavior — things tests don't catch.
**Files:** `references/gates/dynamic-analysis.md`, `scripts/dynamic-analysis.sh`
- [x] Gate definition — sanitizers, heap profiling, race detection per language
- [x] Script — detect language, run appropriate tools (valgrind/asan/tsan/tracemalloc/go-race), JSON report (371 lines)
**Status:** [✅] DONE

---

## Task 6: Performance Benchmark Gate
**Goal:** Catch perf regressions, O(n²) patterns, memory blowups.
**Files:** `references/gates/performance.md`, `scripts/perf-benchmark.sh`
- [x] Gate definition — baseline establishment, regression thresholds (>20% = fail), load testing
- [x] Script — detect benchmark framework, run, compare vs baseline in .wreckit/, JSON report (373 lines)
**Status:** [✅] DONE

---

## Task 7: Upgrade Mutation Testing
**Goal:** Real mutation frameworks (Stryker/mutmut/cargo-mutants), not just AI text flips.
**Files:** `scripts/mutation-test.sh` (rewrite), `references/gates/mutation-kill.md` (update)
- [x] Stryker integration script (mutation-test-stryker.sh, 219 lines)
- [x] mutmut (Python) integration — detects, runs, falls back gracefully
- [x] cargo-mutants (Rust) integration — detects, runs --json, parses, falls back gracefully
- [x] Stryker detection in mutation-test.sh — calls mutation-test-stryker.sh if available
- [x] Fallback to AI mutations when no real tool available
- [x] "tool" field added to JSON output: mutmut / cargo-mutants / stryker / ai-mutations
**Status:** [✅] DONE — 2026-02-21

---

## Task 8: Rename + Republish
**Goal:** Remove Disney trademark risk. Rename wreckit-ralph → wreckit on GitHub + ClawHub.
- [x] Content renamed internally (SKILL.md name field = "wreckit")
- [x] Rename GitHub repo (christiancattaneo/wreckit-ralph → christiancattaneo/wreckit) ✅ 2026-02-21
- [x] Update local git remote to https://github.com/christiancattaneo/wreckit.git ✅
- [x] Published to ClawHub as wreckit v2.0.0 ✅ 2026-02-21
- [x] Updated installed skill locally (~/.openclaw/workspace/skills/wreckit/) ✅
**Status:** [✅] DONE — 2026-02-21

---

## Progress
- Tasks completed: 8/8 ✅ ALL DONE
- Last updated: 2026-02-21 09:45 CST


path: TODO.md
content: # wreckit — TODO

## ✅ SHIPPED v2.0.0 (2026-02-21)

All 8 tasks complete. wreckit v2.0.0 is live on ClawHub.

## Remaining Nice-to-Haves
- [ ] End-to-end swarm test on a real project (blocked: sessions_spawn pairing issue — fix in OpenClaw first)
- [ ] Independent Oracles — auto-generate oracle stubs as a script (cross-verify gate works via orchestrator but no standalone script)
- [ ] mutmut / cargo-mutants integration can only be tested once those tools are installed in the environment


path: assets/dashboard/index.html
content: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wreckit — Dashboard</title>
<style>
  :root {
    --bg: #0a0a0b;
    --surface: #141416;
    --surface2: #1c1c1f;
    --border: #2a2a2e;
    --text: #e4e4e7;
    --text2: #a1a1aa;
    --green: #22c55e;
    --green-bg: #22c55e18;
    --yellow: #eab308;
    --yellow-bg: #eab30818;
    --red: #ef4444;
    --red-bg: #ef444418;
    --blue: #3b82f6;
    --blue-bg: #3b82f618;
    --purple: #a855f7;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  body {
    font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    padding: 24px;
  }

  header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 32px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--border);
  }

  .logo {
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .logo h1 {
    font-size: 24px;
    font-weight: 700;
    letter-spacing: -0.5px;
  }

  .logo .hammer { font-size: 28px; }

  .stats-bar {
    display: flex;
    gap: 24px;
    font-size: 13px;
    color: var(--text2);
  }

  .stats-bar .stat {
    display: flex;
    align-items: center;
    gap: 6px;
  }

  .stats-bar .dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
  }

  .grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
    gap: 16px;
  }

  .card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 20px;
    transition: border-color 0.2s;
  }

  .card:hover { border-color: #3a3a3e; }

  .card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 16px;
  }

  .card-title {
    font-size: 16px;
    font-weight: 600;
  }

  .card-meta {
    font-size: 12px;
    color: var(--text2);
    margin-top: 2px;
  }

  .badge {
    font-size: 11px;
    font-weight: 600;
    padding: 4px 10px;
    border-radius: 6px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .badge-ship { background: var(--green-bg); color: var(--green); }
  .badge-caution { background: var(--yellow-bg); color: var(--yellow); }
  .badge-blocked { background: var(--red-bg); color: var(--red); }
  .badge-running { background: var(--blue-bg); color: var(--blue); }
  .badge-queued { background: var(--surface2); color: var(--text2); }

  .mode-tag {
    font-size: 11px;
    color: var(--text2);
    background: var(--surface2);
    padding: 2px 8px;
    border-radius: 4px;
    margin-left: 8px;
  }

  .gates {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .gate {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    background: var(--surface2);
    border-radius: 8px;
    font-size: 13px;
  }

  .gate-icon {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    flex-shrink: 0;
  }

  .gate-pass { background: var(--green-bg); color: var(--green); }
  .gate-fail { background: var(--red-bg); color: var(--red); }
  .gate-warn { background: var(--yellow-bg); color: var(--yellow); }
  .gate-running { background: var(--blue-bg); color: var(--blue); animation: pulse 1.5s infinite; }
  .gate-pending { background: var(--surface); color: var(--text2); }
  .gate-skip { background: var(--surface); color: #555; }

  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
  }

  .gate-name {
    flex: 1;
    font-weight: 500;
  }

  .gate-detail {
    font-size: 11px;
    color: var(--text2);
  }

  .gate-time {
    font-size: 11px;
    color: var(--text2);
    font-variant-numeric: tabular-nums;
  }

  .progress-bar {
    width: 100%;
    height: 3px;
    background: var(--surface2);
    border-radius: 2px;
    margin-top: 16px;
    overflow: hidden;
  }

  .progress-fill {
    height: 100%;
    border-radius: 2px;
    transition: width 0.5s ease;
  }

  .progress-fill-green { background: var(--green); }
  .progress-fill-yellow { background: var(--yellow); }
  .progress-fill-red { background: var(--red); }
  .progress-fill-blue { background: var(--blue); }

  .card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 12px;
    font-size: 11px;
    color: var(--text2);
  }

  .iterations {
    display: flex;
    align-items: center;
    gap: 4px;
  }

  .iter-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--green);
  }

  .iter-dot.fail { background: var(--red); }
  .iter-dot.current { background: var(--blue); animation: pulse 1.5s infinite; }

  .empty-state {
    text-align: center;
    padding: 80px 20px;
    color: var(--text2);
  }

  .empty-state h2 {
    font-size: 20px;
    margin-bottom: 8px;
    color: var(--text);
  }

  .empty-state p { font-size: 14px; }

  .refresh-time {
    font-size: 11px;
    color: #555;
  }

  /* Findings panel */
  .findings {
    margin-top: 12px;
    padding: 10px 12px;
    background: var(--bg);
    border-radius: 8px;
    font-size: 12px;
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.3s ease;
  }

  .card.expanded .findings {
    max-height: 300px;
    overflow-y: auto;
  }

  .finding {
    padding: 4px 0;
    border-bottom: 1px solid var(--border);
    display: flex;
    gap: 8px;
  }

  .finding:last-child { border-bottom: none; }

  .finding-sev {
    font-size: 10px;
    font-weight: 600;
    padding: 1px 5px;
    border-radius: 3px;
    flex-shrink: 0;
    align-self: flex-start;
    margin-top: 2px;
  }

  .sev-blocker { background: var(--red-bg); color: var(--red); }
  .sev-warning { background: var(--yellow-bg); color: var(--yellow); }
  .sev-info { background: var(--blue-bg); color: var(--blue); }

  .click-hint {
    font-size: 10px;
    color: #444;
    cursor: pointer;
  }

  .card { cursor: pointer; }
</style>
</head>
<body>

<header>
  <div class="logo">
    <span class="hammer">🔨</span>
    <div>
      <h1>wreckit</h1>
    </div>
  </div>
  <div class="stats-bar">
    <div class="stat"><div class="dot" style="background:var(--green)"></div><span id="stat-ship">0</span> Ship</div>
    <div class="stat"><div class="dot" style="background:var(--yellow)"></div><span id="stat-caution">0</span> Caution</div>
    <div class="stat"><div class="dot" style="background:var(--red)"></div><span id="stat-blocked">0</span> Blocked</div>
    <div class="stat"><div class="dot" style="background:var(--blue)"></div><span id="stat-running">0</span> Running</div>
    <div class="refresh-time">Updated <span id="last-refresh">—</span></div>
  </div>
</header>

<div class="grid" id="grid"></div>

<script>
// wreckit dashboard — reads from .wreckit/dashboard.json in each project
// Auto-refreshes every 5 seconds

const DATA_PATH = '/api/status'; // served by dashboard server

const GATE_ICONS = {
  pass: '✓', fail: '✗', warn: '!', running: '◉', pending: '○', skip: '—'
};

function renderGate(gate) {
  return `
    <div class="gate">
      <div class="gate-icon gate-${gate.status}">${GATE_ICONS[gate.status]}</div>
      <span class="gate-name">${gate.name}</span>
      <span class="gate-detail">${gate.detail || ''}</span>
      <span class="gate-time">${gate.duration || ''}</span>
    </div>
  `;
}

function renderFindings(findings) {
  if (!findings || !findings.length) return '';
  return findings.map(f => `
    <div class="finding">
      <span class="finding-sev sev-${f.severity}">${f.severity}</span>
      <span>${f.message}</span>
    </div>
  `).join('');
}

function renderCard(run) {
  const gatesDone = run.gates.filter(g => g.status === 'pass' || g.status === 'warn').length;
  const gatesTotal = run.gates.filter(g => g.status !== 'skip').length;
  const pct = gatesTotal > 0 ? (gatesDone / gatesTotal * 100) : 0;

  const progressClass = run.decision === 'ship' ? 'green' :
    run.decision === 'caution' ? 'yellow' :
    run.decision === 'blocked' ? 'red' : 'blue';

  const badgeClass = run.decision || 'running';

  const iterDots = (run.iterations || []).map(i =>
    `<div class="iter-dot ${i.status === 'fail' ? 'fail' : i.status === 'current' ? 'current' : ''}"></div>`
  ).join('');

  return `
    <div class="card" onclick="this.classList.toggle('expanded')">
      <div class="card-header">
        <div>
          <span class="card-title">${run.repo}</span>
          <span class="mode-tag">${run.mode}</span>
          <div class="card-meta">${run.language} · ${run.framework || 'no framework'} · ${run.branch || 'main'}</div>
        </div>
        <span class="badge badge-${badgeClass}">${(run.decision || 'running').toUpperCase()}</span>
      </div>
      <div class="gates">
        ${run.gates.map(renderGate).join('')}
      </div>
      <div class="progress-bar">
        <div class="progress-fill progress-fill-${progressClass}" style="width:${pct}%"></div>
      </div>
      <div class="card-footer">
        <div class="iterations">${iterDots} <span>${run.iteration || 0}/${run.maxIterations || 50} iterations</span></div>
        <span class="click-hint">click for details</span>
      </div>
      <div class="findings">
        ${renderFindings(run.findings)}
      </div>
    </div>
  `;
}

function renderStats(runs) {
  const counts = { ship: 0, caution: 0, blocked: 0, running: 0 };
  runs.forEach(r => {
    if (r.decision) counts[r.decision]++;
    else counts.running++;
  });
  document.getElementById('stat-ship').textContent = counts.ship;
  document.getElementById('stat-caution').textContent = counts.caution;
  document.getElementById('stat-blocked').textContent = counts.blocked;
  document.getElementById('stat-running').textContent = counts.running;
}

function render(runs) {
  const grid = document.getElementById('grid');
  if (!runs.length) {
    grid.innerHTML = `
      <div class="empty-state" style="grid-column: 1/-1">
        <h2>🔨 No runs yet</h2>
        <p>Start a wreckit audit to see results here</p>
      </div>
    `;
    return;
  }
  // Sort: running first, then blocked, caution, ship
  const order = { undefined: 0, running: 0, blocked: 1, caution: 2, ship: 3 };
  runs.sort((a, b) => (order[a.decision] ?? 0) - (order[b.decision] ?? 0));
  grid.innerHTML = runs.map(renderCard).join('');
  renderStats(runs);
  document.getElementById('last-refresh').textContent = new Date().toLocaleTimeString();
}

// Fetch and render
async function refresh() {
  try {
    const res = await fetch(DATA_PATH);
    const data = await res.json();
    render(data.runs || []);
  } catch {
    // If server not running, try loading from file
    loadFromFile();
  }
}

function loadFromFile() {
  // Demo/fallback: render from inline data
  const demoEl = document.getElementById('demo-data');
  if (demoEl) {
    try {
      const data = JSON.parse(demoEl.textContent);
      render(data.runs || []);
    } catch {}
  }
}

// Auto-refresh every 5s
setInterval(refresh, 5000);

// Initial load — try file-based data first
document.addEventListener('DOMContentLoaded', () => {
  const stored = localStorage.getItem('wreckit-runs');
  if (stored) {
    try { render(JSON.parse(stored)); } catch {}
  }
  refresh();
});
</script>

<!-- Inline demo data for offline/preview -->
<script type="application/json" id="demo-data">
{
  "runs": [
    {
      "repo": "pinata-security",
      "mode": "audit",
      "language": "TypeScript",
      "framework": "none",
      "branch": "main",
      "decision": null,
      "iteration": 0,
      "maxIterations": 50,
      "gates": [
        { "name": "AI Slop Scan", "status": "pass", "detail": "0 hallucinated deps", "duration": "12s" },
        { "name": "Type Check", "status": "pass", "detail": "tsc --noEmit", "duration": "8s" },
        { "name": "Test Quality", "status": "running", "detail": "analyzing assertions..." },
        { "name": "Mutation Kill", "status": "pending" },
        { "name": "LLM-as-Judge", "status": "skip" },
        { "name": "Proof Bundle", "status": "pending" }
      ],
      "iterations": [
        { "status": "pass" }, { "status": "pass" }, { "status": "current" }
      ],
      "findings": [
        { "severity": "info", "message": "47 detection categories found, all have test coverage" },
        { "severity": "warning", "message": "3 tests have single assertions — consider strengthening" }
      ]
    },
    {
      "repo": "slopometer",
      "mode": "audit",
      "language": "TypeScript",
      "framework": "Next.js",
      "branch": "main",
      "decision": "caution",
      "iteration": 12,
      "maxIterations": 50,
      "gates": [
        { "name": "AI Slop Scan", "status": "pass", "detail": "0 issues", "duration": "9s" },
        { "name": "Type Check", "status": "pass", "detail": "tsc clean", "duration": "6s" },
        { "name": "Test Quality", "status": "warn", "detail": "72% branch coverage", "duration": "34s" },
        { "name": "Mutation Kill", "status": "warn", "detail": "91% kill rate", "duration": "2m 18s" },
        { "name": "LLM-as-Judge", "status": "skip" },
        { "name": "Proof Bundle", "status": "pass", "duration": "2s" }
      ],
      "findings": [
        { "severity": "warning", "message": "Mutation kill rate 91% — 4 survivors in scoring module" },
        { "severity": "warning", "message": "Branch coverage 72% — error paths in scanner untested" },
        { "severity": "info", "message": "AI slop detection logic itself is well-tested" }
      ]
    },
    {
      "repo": "colette",
      "mode": "audit",
      "language": "Swift",
      "framework": "SwiftUI",
      "branch": "main",
      "decision": "blocked",
      "iteration": 3,
      "maxIterations": 50,
      "gates": [
        { "name": "AI Slop Scan", "status": "fail", "detail": "2 phantom imports", "duration": "15s" },
        { "name": "Type Check", "status": "fail", "detail": "swift build — 7 errors", "duration": "22s" },
        { "name": "Test Quality", "status": "pending" },
        { "name": "Mutation Kill", "status": "pending" },
        { "name": "LLM-as-Judge", "status": "skip" },
        { "name": "Proof Bundle", "status": "pending" }
      ],
      "findings": [
        { "severity": "blocker", "message": "2 phantom imports: CloudKitSync (not in project), DesignTokens (removed)" },
        { "severity": "blocker", "message": "7 type errors in ContentView.swift — missing protocol conformance" }
      ]
    }
  ]
}
</script>

</body>
</html>


path: assets/dashboard/server.mjs
content: #!/usr/bin/env node
/**
 * wreckit dashboard server
 * Serves the dashboard UI and provides a /api/status endpoint
 * that aggregates .wreckit/dashboard.json from all watched project paths.
 *
 * Usage:
 *   node server.mjs [--port 3939] [--projects ~/Projects/repo1,~/Projects/repo2]
 *   node server.mjs --watch ~/Projects  (auto-discover all subdirs with .wreckit/)
 */

import { createServer } from 'node:http';
import { readFile, readdir, stat } from 'node:fs/promises';
import { join, resolve, basename } from 'node:path';
import { existsSync } from 'node:fs';

const args = process.argv.slice(2);
const PORT = parseInt(getArg('--port', '3939'));
const projectsArg = getArg('--projects', '');
const watchDir = getArg('--watch', '');

function getArg(name, fallback) {
  const idx = args.indexOf(name);
  return idx >= 0 && args[idx + 1] ? args[idx + 1] : fallback;
}

// Resolve project directories
async function getProjectDirs() {
  if (projectsArg) {
    return projectsArg.split(',').map(p => resolve(p.trim()));
  }
  if (watchDir) {
    const dir = resolve(watchDir);
    const entries = await readdir(dir, { withFileTypes: true });
    const dirs = [];
    for (const entry of entries) {
      if (entry.isDirectory()) {
        const wreckitDir = join(dir, entry.name, '.wreckit');
        if (existsSync(wreckitDir)) {
          dirs.push(join(dir, entry.name));
        }
      }
    }
    return dirs;
  }
  // Default: scan ~/Projects
  const home = process.env.HOME || '/tmp';
  const defaultDir = join(home, 'Projects');
  if (existsSync(defaultDir)) {
    const entries = await readdir(defaultDir, { withFileTypes: true });
    return entries
      .filter(e => e.isDirectory())
      .map(e => join(defaultDir, e.name));
  }
  return [];
}

// Read dashboard.json from a project
async function readDashboard(projectDir) {
  const dashFile = join(projectDir, '.wreckit', 'dashboard.json');
  try {
    const raw = await readFile(dashFile, 'utf-8');
    const data = JSON.parse(raw);
    data.repo = data.repo || basename(projectDir);
    return data;
  } catch {
    return null;
  }
}

// Aggregate all project statuses
async function getStatus() {
  const dirs = await getProjectDirs();
  const runs = [];
  for (const dir of dirs) {
    const data = await readDashboard(dir);
    if (data) runs.push(data);
  }
  return { runs, timestamp: new Date().toISOString(), projectCount: dirs.length };
}

// Serve
const server = createServer(async (req, res) => {
  const url = new URL(req.url, `http://localhost:${PORT}`);

  // CORS
  res.setHeader('Access-Control-Allow-Origin', '*');

  if (url.pathname === '/api/status') {
    const status = await getStatus();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(status));
    return;
  }

  // Serve dashboard HTML
  if (url.pathname === '/' || url.pathname === '/index.html') {
    const html = await readFile(new URL('./index.html', import.meta.url), 'utf-8');
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end(html);
    return;
  }

  res.writeHead(404);
  res.end('Not found');
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`🔨 wreckit dashboard → http://localhost:${PORT}`);
  console.log(`   API endpoint → http://localhost:${PORT}/api/status`);
  if (watchDir) console.log(`   Watching: ${resolve(watchDir)}`);
  else if (projectsArg) console.log(`   Projects: ${projectsArg}`);
  else console.log(`   Auto-scanning: ~/Projects/*/. wreckit/`);
});


path: references/gates/behavior-capture.md
content: # Gate: Behavior Capture + Deterministic Replay (REBUILD mode only)

**Question:** Does the new code behave identically to the old code?

## Process

**Before touching anything:**
1. Record all inputs → outputs from existing codebase (API calls, function returns, CLI outputs)
2. Save as golden fixtures in `.wreckit/golden/`

**After rebuilding:**
3. Replay all golden fixtures through the new code
4. Diff outputs

## Pass/Fail

- **Pass:** Identical outputs for all captured inputs (or documented, justified differences).
- **Fail:** Any unexpected output difference.


path: references/gates/ci-integration.md
content: # Gate: CI Integration

**Question:** Does this project have a CI pipeline that proves it works automatically?

## What to Check

- Presence of CI configuration files:
  - `.github/workflows/*.yml` (GitHub Actions)
  - `.gitlab-ci.yml` (GitLab CI)
  - `Jenkinsfile` (Jenkins)
  - `.circleci/config.yml` (CircleCI)
  - `Makefile` with `ci` or `test` targets
- Quality of existing CI config:
  - Tests are actually run (not just lint)
  - Type checking is included (for typed languages)
  - Coverage reporting is enabled
  - Security scanning (SAST/dependency audit) is included

## How to Run

```bash
scripts/ci-integration.sh [project-path]
```

The script:
1. Searches for CI config files in common locations
2. If found: parses and scores based on steps present
3. If not found: generates a starter `.github/workflows/wreckit-audit.yml`

Scoring (100 points total):
- Tests present: 40 pts
- Type check present: 30 pts
- Coverage reporting: 20 pts
- Security scan: 10 pts

## Pass / Fail Criteria

### PASS ✅
- CI config exists
- Score ≥ 70 (tests + type check at minimum)
- Pipeline runs on push AND pull requests

### WARN ⚠️
- CI config exists but score 40–69
- Missing type check or coverage steps
- Only runs on push, not PRs

### FAIL ❌
- No CI config found in project
- CI config exists but runs no tests (score < 40)

## Example Output

```json
{
  "ci_found": true,
  "ci_type": "github-actions",
  "score_100": 90,
  "missing_steps": ["security-scan"],
  "generated_config_path": null,
  "verdict": "PASS",
  "summary": "GitHub Actions found. Score 90/100. Missing: security scan."
}
```

## Generated Config (when none exists)

When no CI config is found, the script can generate a starter workflow:

```yaml
# .github/workflows/wreckit-audit.yml
name: wreckit audit
on: [push, pull_request]
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm install
      - run: npx tsc --noEmit
      - run: npm test
```


path: references/gates/corroboration.md
content: # Corroboration Engine Rules (Aggregator)

This document defines deterministic verdicting constraints applied by `scripts/proof-bundle.sh`.

## 1) Hard blocks (single-gate BLOCKED allowed)
Single gate failure may produce `BLOCKED` **only** for hard blocks:

- **check_deps**: `status=FAIL`, `confidence>=0.9`, and confirmed evidence (`findings>0` or non-empty `hallucinated[]`)
- **type_check**: `status=FAIL` and `errors>0`
- **slop_scan**: `status=FAIL` and `density_per_kloc > 3 * threshold_per_kloc`

## 2) Non-hard corroboration (multi-signal BLOCKED)
For non-hard failures, `BLOCKED` requires all of:

- `corroborating_signals >= 2`
- `weighted_evidence >= 1.4`
- `corroborating_domain_count >= 2`
- `avg_soft_confidence >= 0.65`

If any of these are missing, verdict degrades to `CAUTION`.

## 3) Evidence domains
Gates are grouped by domain to avoid correlated false blocks:

- dependencies: `check_deps`
- correctness: `type_check`
- code_quality: `slop_scan`
- security: `red_team`, `regex_complexity`
- architecture: `design_review`
- delivery: `ci_integration`
- testing: `coverage_stats`, `mutation_test`
- planning: `ralph_loop`

## 4) Guardrail
No single non-hard gate may directly produce `BLOCKED`.


path: references/gates/cross-verify.md
content: # Gate: Cross-Verify (BUILD mode only)

**Question:** If you only had the tests and the PRD, would you write the same code?

## Process

1. Take the final test suite + PRD
2. In a **fresh session** (different model if possible): generate a new implementation from ONLY tests and PRD
3. Diff the two implementations
4. Small diffs (style, naming) = fine
5. Large diffs (different algorithms, missing features) = investigate

## Pass/Fail

- **Pass:** Core logic is structurally similar. No missing features in either version.
- **Fail:** Re-generated version is fundamentally different → tests don't specify behavior well enough, or original took a wrong path.


path: references/gates/design-review.md
content: # Gate: Design Review

**Question:** Is the architecture well-structured, low-coupling, and fit for purpose?

## What to Check

- **Circular dependencies:** modules that import each other in a cycle indicate tangled design
- **God modules (high fan-in):** files imported by many others (fan-in > 8) are fragile single points of failure
- **God modules (high fan-out):** files that import many others (fan-out > 10) often do too much
- **Orphan files:** source files not imported by anything and not an entry point — likely dead code
- **Layering violations:** e.g., data layer importing from presentation layer
- **Clear data flow:** inputs → transform → outputs should be traceable without circular reads

## How to Run

```bash
scripts/design-review.sh [project-path]
```

The script auto-detects language and runs appropriate analysis:
- **JS/TS:** attempts `npx madge --circular --json .`, falls back to manual import graph scan
- **Python:** scans `import` / `from ... import` statements
- **Go:** scans `import` blocks

Output is JSON to stdout. Human summary goes to stderr.

### Manual interpretation (if script unavailable)

1. List all source files
2. For each file, extract all imports (require/import/from)
3. Build dependency graph: `{file → [files it depends on]}`
4. Compute fan-in: count how many files import each file
5. Compute fan-out: count how many files each file imports
6. Run DFS to detect cycles
7. Flag any file with fan-in > 8 as a potential god module

## Pass / Fail Criteria

### PASS ✅
- No circular dependencies detected
- No god modules (fan-in ≤ 8 for all files)
- Clear, acyclic data flow
- Orphan files < 5% of total

### WARN ⚠️
- God modules exist (fan-in > 8) but no circular deps
- Orphan files present (5–15% of total)
- High fan-out modules (fan-out > 10) suggesting bloated responsibilities

### FAIL ❌
- Any circular dependency found
- God modules with fan-in > 10
- Orphan files > 15% of total

## Example Output

```json
{
  "project": "/path/to/project",
  "language": "ts",
  "total_files": 42,
  "circular_deps": [],
  "god_modules": [{"file": "src/utils/helpers.ts", "fan_in": 9}],
  "orphan_files": [],
  "avg_fan_in": 2.1,
  "avg_fan_out": 1.8,
  "verdict": "WARN",
  "summary": "No circular deps. 1 god module (helpers.ts, fan-in=9)."
}
```


path: references/gates/differential.md
content: # Gate: Differential Testing

**Question:** Does this implementation agree with an independent oracle on the same inputs?

The most powerful bug-catcher available. Catches "plausible but wrong" logic that no amount of
unit tests will find — because the tests and the code were written by the same AI with the same
misunderstanding.

Run `scripts/differential-test.sh [path]` first.

---

## What Is Differential Testing

You run TWO implementations on the SAME inputs and compare outputs. If they disagree,
one of them is wrong. The "oracle" can be:

1. **A reference implementation** — a trusted library doing the same thing
2. **A brute-force "obviously correct" version** — slow but simple
3. **A previous known-good version** of the code
4. **A different language's implementation** — Python vs Go, etc.
5. **Golden test outputs** — hand-verified expected outputs stored in a file

---

## Step-by-Step Process

### 1. Find or Create the Oracle

Look for:
- Existing tests with hardcoded expected values → these ARE golden tests
- A `reference/` or `oracle/` directory in the project
- A simpler brute-force implementation in comments or docs
- The same function implemented in a different file/module

If none exists, create a brute-force oracle for the core logic. Example:
- Real: optimized binary search
- Oracle: linear scan (same answer, obviously correct)

Document what oracle was used in the gate result.

### 2. Identify Test Cases

Use a mix of:
- **Boundary inputs:** empty, null, min value, max value, single element
- **Typical inputs:** representative real-world cases (3-5)
- **Adversarial inputs:** duplicates, sorted, reverse-sorted, all-same values
- **Random inputs:** at least 20 randomly generated cases

### 3. Run Both Implementations

For each test case:
1. Run the real implementation → capture output
2. Run the oracle → capture output
3. Compare outputs (exact match, or use tolerance for floats)
4. Record: input, expected (oracle), actual (real), match/mismatch

### 4. Metamorphic Testing (No Oracle Needed)

If you have no oracle, use metamorphic properties — relationships that MUST hold:

Examples:
- `sort(reverse(X)) == reverse(sort(X))`
- `len(filter(X)) <= len(X)`
- `sum(split(X)) == sum(X)` (for partitioning functions)
- `encode(decode(X)) == X` (for codecs)
- `f(f(X)) == f(X)` (for idempotent functions)

Run these properties on 50+ random inputs. Any violation is a bug.

### 5. Golden Test Verification

Check existing test files for:
- Hardcoded expected values
- Snapshot tests
- Fixture files with expected outputs

Run all of them. If any fail, it's a regression.

---

## Pass/Fail Criteria

| Result | Verdict |
|--------|---------|
| Implementation matches oracle on all inputs | Pass ✅ |
| Implementation matches oracle on all but ≤2% of random cases (float precision) | Caution ⚠️ |
| Implementation disagrees with oracle on any deterministic input | Fail 🚫 |
| No oracle found AND no metamorphic properties testable | Caution ⚠️ (note as incomplete) |
| Metamorphic property violated on any input | Fail 🚫 |
| Golden tests fail | Fail 🚫 |

---

## Script Output

`scripts/differential-test.sh [path]` outputs:
```json
{
  "oracle_type": "brute_force|golden|reference|metamorphic|none",
  "oracle_description": "What oracle was used",
  "test_cases_run": 50,
  "mismatches": [
    {
      "input": "...",
      "expected": "...",
      "actual": "...",
      "case_type": "boundary|random|adversarial"
    }
  ],
  "mismatch_rate": 0.02,
  "metamorphic_violations": [],
  "golden_test_failures": [],
  "verdict": "PASS|FAIL|WARN"
}
```


path: references/gates/dynamic-analysis.md
content: # Gate: Dynamic Analysis

**Question:** Does the code misbehave at runtime — leaks memory, races on shared state, hits undefined behavior?

Static analysis finds what code LOOKS like. Dynamic analysis finds what it DOES — at runtime,
under real execution. These are entirely different failure classes.

Run `scripts/dynamic-analysis.sh [path]` first.

---

## What Dynamic Analysis Catches

Things NO amount of reading the code or running tests will catch:

- **Memory leaks** — objects created, never garbage collected
- **Race conditions** — concurrent writes to shared state producing corrupt results
- **Use-after-free** — accessing memory after it's been freed (C/C++)
- **Undefined behavior** — signed overflow, null dereference, invalid casts
- **Async leaks** — event listeners added, never removed; timers that don't clear
- **File descriptor leaks** — files opened in error paths without being closed
- **Handle leaks** — database connections, network sockets not returned to pool

---

## Tools by Language

### C / C++
- **Valgrind** — memory errors, leaks, undefined behavior: `valgrind --leak-check=full ./binary`
- **AddressSanitizer (ASAN)** — compile with `-fsanitize=address`
- **UndefinedBehaviorSanitizer (UBSan)** — compile with `-fsanitize=undefined`
- **ThreadSanitizer (TSan)** — compile with `-fsanitize=thread`

### Python
- **tracemalloc** — built-in memory tracing
- **memory_profiler** — `pip install memory_profiler`, run `@profile` decorator
- **objgraph** — `pip install objgraph`, finds reference cycles
- **pytest-memray** — memory profiling in tests

### JavaScript / TypeScript (Node.js)
- **--expose-gc + heapUsed** — track V8 heap growth
- **clinic.js** — `npx clinic doctor -- node app.js`
- **node --inspect** — Chrome DevTools memory profiling
- **leakage** — `npm install leakage`, automatic leak detection in tests

### Go
- **go test -race** — built-in race detector (always use this)
- **pprof** — built-in profiling: `go test -memprofile=mem.out`
- **go test -count=N** — run tests multiple times to expose flakiness

### Rust
- **Miri** — interpreter that detects undefined behavior: `cargo miri test`
- **cargo test -- --nocapture** — run tests with full output
- Memory safety is largely guaranteed by the borrow checker (less concern here)

---

## Step-by-Step Process

### 1. Run Language-Appropriate Sanitizers

Run the test suite with sanitizers enabled. ANY sanitizer finding = Blocked.

### 2. Memory Leak Check

Run tests, then check for heap growth:
- Before test: record heap size
- Run tests
- After test: record heap size
- Growth > 10MB unexplained = warning

### 3. Race Condition Check

For concurrent code:
- Go: always `go test -race`
- Python async: run with `asyncio.set_debug(True)` and check for warnings
- JS async: look for unhandled promise rejections, unhandled event emitter errors

### 4. File/Handle Leak Check

After running tests, check:
- Open file descriptors (compare before/after)
- Open database connections
- Active event listeners (Node.js: `process.listenerCount('exit')`)

---

## Pass/Fail Criteria

| Finding | Severity | Verdict |
|---------|----------|---------|
| Memory leak > 1MB sustained | BLOCKER | Blocked 🚫 |
| Race condition detected | BLOCKER | Blocked 🚫 |
| Use-after-free / ASAN finding | BLOCKER | Blocked 🚫 |
| Undefined behavior (UBSan) | BLOCKER | Blocked 🚫 |
| Memory growth < 10MB (tolerable) | INFO | Pass ✅ |
| Async resource leak | WARNING | Caution ⚠️ |
| FD leak detected | WARNING | Caution ⚠️ |
| No dynamic analysis tooling available | INFO | Warn ⚠️ (note only) |


path: references/gates/llm-judge.md
content: # Gate: LLM-as-Judge (optional, all modes)

**Question:** Does this meet subjective quality criteria?

Only runs when PRD includes subjective items (UX, design, readability, tone).
Skip entirely for pure backend/library work.

## Process

For each subjective criterion:
1. Present criterion + implementation to a **fresh session**
2. Binary pass/fail judgment with reasoning
3. Fail → specific, actionable feedback → loop back to Ralph loop Phase 2

## Examples

- "Navigation should be intuitive"
- "Error messages should be helpful, not cryptic"
- "Code should be readable by a junior developer"
- "API design should follow REST conventions"

## Pass/Fail

- **Pass:** All subjective criteria pass.
- **Fail:** Any criterion fails → return with specific feedback.
- **Cap:** Max 3 judge iterations per criterion. Still failing → Caution in proof bundle.


path: references/gates/mutation-kill.md
content: # Gate: Mutation Kill

**Preferred tool:** Stryker Mutator (JS/TS) or mutmut (Python). See `scripts/mutation-test-stryker.sh`.
For projects without Stryker installed, fall back to `scripts/mutation-test.sh` (AI-based mutations).

**Question:** Do the tests actually catch bugs, or do they just run green?

## Process

Generate mutations — small, realistic code changes that SHOULD break something:

- Flip boolean conditions (`if (x > 0)` → `if (x < 0)`)
- Remove null checks
- Swap function arguments
- Change boundary values (`>=` → `>`)
- Remove error handling
- Return early before side effects

For each mutation:
1. Apply mutation to a copy (never mutate real code)
2. Run test suite against mutated copy
3. Tests still pass → **mutation survived** (tests are weak here)
4. Revert mutation, record result

## External Tools (optional, better)

- **Stryker** (JS/TS) — real mutation framework, much more thorough
- **mutmut** (Python)
- **cargo-mutants** (Rust)

If available, prefer external tools over AI-generated mutations.

## Pass/Fail

- **Pass:** ≥95% of mutations killed
- **Caution:** 90-94% kill rate
- **Fail:** <90% kill rate — tests are not trustworthy

---

## Validation Status (2026-02-23)

### Tool Availability
Checked on host: `Christian's Mac mini (Darwin 25.3.0 arm64)`

```
mutmut:       NOT INSTALLED  (pip install mutmut to enable)
stryker:      NOT INSTALLED  (npx stryker / npm install @stryker-mutator/core)
cargo-mutants: NOT INSTALLED  (cargo install cargo-mutants)
```

### AI Fallback Path — VALIDATED ✅

The AI-based mutation path in `scripts/mutation-test.sh` was validated end-to-end on:
- **Fixture:** `tests/fixtures/perfect-small` (TypeScript, vitest)
- **Result:** 7 mutations generated, 7 killed, 0 survived — **100% kill rate**
- **Verdict:** SHIP

Bug fixed: `SCRIPT_DIR` was computed _after_ `cd "$PROJECT"`, causing a path resolution failure when called with a relative path (e.g., `bash scripts/mutation-test.sh tests/fixtures/...`). Fixed by moving `SCRIPT_DIR` assignment to the top of the script before any `cd`.

### mutmut Path — NOT VALIDATED (tool not installed)

The mutmut detection block exists and correctly falls through to AI fallback when mutmut is absent:
- Script prints: `mutmut not installed. Install with: pip install mutmut`
- Continues to AI fallback path
- AI fallback works for Python files (`.py` detected, `LANG="py"` set, same mutation loop runs)

### Stryker Path — NOT VALIDATED (tool not installed)

The stryker detection block exists. When stryker is not installed:
- `npx stryker --version` returns non-zero → block skipped
- Falls through to AI mutation loop
- Note: `mutation-test-stryker.sh` exists but has not been end-to-end tested with a real Stryker install

### Next Steps to Complete Validation

1. `pip install mutmut` and run: `bash scripts/mutation-test.sh <python-project>`
2. `npm install -g @stryker-mutator/core` and run against a JS/TS project
3. Consider adding mutation-test canary to `tests/run-tests.sh`


path: references/gates/performance.md
content: # Gate: Performance Benchmarks

**Question:** Is the code fast enough, and did it get slower?

AI code often works on toy inputs but tanks at scale. This gate catches:
- O(n²) algorithms hiding in "working" code
- Functions that take 10ms at 100 items but 60 seconds at 10,000
- Memory blowups that look fine until production load hits
- Regressions: code that worked fast last week is now 3x slower

Run `scripts/perf-benchmark.sh [path]` first.

---

## Step-by-Step Process

### 1. Find or Create Benchmarks

Look for:
- **Vitest bench** — `bench()` calls in test files
- **pytest-benchmark** — `benchmark.pedantic()` or `benchmark()` in pytest
- **cargo bench** — `#[bench]` functions
- **go test -bench** — `func BenchmarkX(b *testing.B)` functions
- **k6** — `k6.js` or `script.js` load test files

If none exist: write benchmarks for the 3 most critical/expensive functions.

### 2. Run Benchmarks

Run benchmarks with enough iterations to be statistically meaningful:
- Vitest bench: `vitest bench`
- pytest-benchmark: `pytest --benchmark-only`
- cargo bench: `cargo bench`
- go bench: `go test -bench=. -benchmem -count=3`
- k6: `k6 run script.js`

### 3. Compare vs Baseline

If `.wreckit/perf-baseline.json` exists from a previous run:
- Compare each benchmark's mean time to baseline
- Flag regressions > 20% slower
- Flag improvements > 20% faster (document, may want to update baseline)

If no baseline exists: write current results as the new baseline.

### 4. Complexity Analysis

For core algorithmic functions, estimate time complexity:
- Run at N=10, 100, 1000, 10000 inputs
- Calculate growth ratio: T(10N)/T(N)
  - ~1.0x → O(1) or O(log n) — excellent
  - ~2-3x → O(n) — expected
  - ~10x → O(n log n) — acceptable for sort operations
  - ~100x → O(n²) — likely too slow for production at scale
  - ~1000x → O(n³) or worse — almost certainly broken

### 5. Memory Scaling

Run at increasing input sizes and track peak memory:
- Watch for linear memory growth (normal)
- Flag superlinear memory growth (possible bug)

---

## Pass/Fail Criteria

| Condition | Verdict |
|-----------|---------|
| All benchmarks within 20% of baseline | Pass ✅ |
| No baseline — first run | Pass ✅ (baseline created) |
| Any benchmark >20% slower than baseline | Caution ⚠️ |
| Any benchmark >50% slower than baseline | Blocked 🚫 |
| O(n²) or worse detected for N>1000 operation | Blocked 🚫 |
| Memory grows superlinearly | Caution ⚠️ |
| No benchmarks and no benchmark framework | Warn ⚠️ |

---

## Script Output

`scripts/perf-benchmark.sh [path]` outputs:
```json
{
  "framework": "vitest-bench|pytest-benchmark|cargo-bench|go-bench|none",
  "benchmarks": [
    {
      "name": "sort 1000 items",
      "mean_ms": 1.2,
      "baseline_ms": 1.0,
      "delta_pct": 20.0,
      "regression": false
    }
  ],
  "regressions": [],
  "baseline_created": true,
  "baseline_path": ".wreckit/perf-baseline.json",
  "verdict": "PASS|WARN|FAIL"
}
```


path: references/gates/proof-bundle.md
content: # Gate: Proof Bundle (all modes, always last)

**Question:** Can you prove it works?

## Output Structure

Commit to repo:
```
.wreckit/
├── proof.json             # Gate results, timestamps, pass/fail, mode, iterations
├── plan.md                # Final IMPLEMENTATION_PLAN.md state
├── slop-report.md         # AI slop findings
├── type-check.log         # Raw type checker output
├── test-quality.md        # Assertion density, coverage, edge cases
├── mutations.md           # Mutation kill results + survivors
├── judge-review.md        # LLM-as-judge results (if applicable)
├── cross-verify.md        # Re-gen diff (BUILD mode)
├── behavior-capture.md    # Golden fixtures (REBUILD mode)
├── regression.md          # Regression results (REBUILD/FIX mode)
├── security-review.md     # SAST findings (FIX mode)
├── dashboard.json         # Machine-readable results for dashboard
└── decision.md            # Ship / Caution / Blocked + reasoning
```

## Decision Framework

| Verdict | Criteria |
|---------|----------|
| **Ship** ✅ | All gates pass, ≥95% mutation kill, zero slop, subjective met |
| **Caution** ⚠️ | All gates pass, mutation kill 90-94%, or minor slop in non-critical, or judge needed 2+ iterations |
| **Blocked** 🚫 | Any gate fails, hallucinated deps, <90% mutation kill, or hard cap hit |


path: references/gates/property-based.md
content: # Gate: Property-Based & Fuzz Testing

**Question:** Do the invariants hold across thousands of random inputs?

Unit tests check a few known cases. Property-based testing checks your invariants
across hundreds or thousands of randomly generated cases — finding edge cases nobody
thought to test.

Run `scripts/property-test.sh [path]` first.

---

## What Are Properties

Properties are statements that must ALWAYS be true, regardless of input:

```python
# Sort: output is always ordered
assert all(result[i] <= result[i+1] for i in range(len(result)-1))

# Filter: output is always a subset
assert all(item in original for item in filtered)

# Encode/decode roundtrip
assert decode(encode(x)) == x

# Length invariant
assert len(compress(x)) <= len(x)  # compression never expands

# Idempotency
assert f(f(x)) == f(x)  # running twice = running once
```

---

## Tools by Language

| Language | Tool | Install | Run |
|----------|------|---------|-----|
| Python | hypothesis | `pip install hypothesis` | `pytest` (auto-discovers `@given`) |
| JavaScript/TypeScript | fast-check | `npm install fast-check` | `npx fast-check` or via jest/vitest |
| Rust | proptest / quickcheck | `cargo add proptest` | `cargo test` |
| Go | go-fuzz / gopter | `go get gopter` | `go test -fuzz=.` |
| Java | jqwik | Add to pom.xml | `mvn test` |
| Ruby | rantly | `gem install rantly` | `rspec` |

---

## Step-by-Step Process

### 1. Check for Existing Property Tests

Look for:
- Python: files with `@given`, `@settings`, `from hypothesis import`
- JS/TS: `import { fc } from 'fast-check'`, `fc.property`
- Rust: `#[proptest]`, `proptest!` macro, `#[quickcheck]`
- Go: `func Fuzz` functions (Go 1.18+ native fuzzing)

If found: run them. Any failure = bug.

### 2. If No Property Tests Exist — Generate Stubs

For each public function, identify testable invariants:
- Return type constraints (always returns an array, never null)
- Length constraints (output <= input length)
- Ordering constraints (sorted output)
- Roundtrip properties (parse/serialize, encode/decode)
- Idempotency (calling twice = calling once)
- Commutativity/associativity (where applicable)

Write minimal property test stubs to a `.wreckit/property-stubs/` directory.
These are starting points — they should be moved into the actual test suite.

### 3. Run the Fuzzer

Configure for at least:
- **100 examples** minimum (hypothesis default: 100, fast-check default: 100)
- **Shrinking enabled** — when a failure is found, shrink to minimal case
- **Deadline disabled** for slow functions

### 4. Crash Corpus Management

If any input crashes the function:
1. Save the crashing input to `.wreckit/crash-corpus/`
2. Add it as a regression test immediately
3. Fix the bug, re-run to confirm crash is gone
4. Keep the crashing input forever as a regression seed

---

## Pass/Fail Criteria

| Result | Verdict |
|--------|---------|
| Existing property tests found AND all pass | Pass ✅ |
| No property tests found, but stubs generated | Caution ⚠️ (note: add to test suite) |
| Property test framework installed, zero properties written | Caution ⚠️ |
| Any property test fails | Fail 🚫 |
| Any crash/exception on random input that's not handled | Fail 🚫 |
| Framework not installed and language doesn't support native fuzzing | Warn ⚠️ |

---

## Script Output

`scripts/property-test.sh [path]` outputs:
```json
{
  "framework_detected": "hypothesis|fast-check|proptest|go-fuzz|none",
  "property_tests_found": 5,
  "property_tests_run": 5,
  "failures": [],
  "crash_inputs": [],
  "stubs_generated": "path/to/.wreckit/property-stubs/",
  "verdict": "PASS|FAIL|WARN"
}
```


path: references/gates/ralph-loop.md
content: # Gate: Ralph Loop

**Question:** Does the implementation satisfy the requirements?

The Ralph loop is the BUILD engine. Three phases, fresh context per iteration.

## Phase 1: Planning (no code)

1. Read PRD/spec/issue and existing codebase
2. Gap analysis: compare requirements against current code
3. Produce `IMPLEMENTATION_PLAN.md` — prioritized task list, single source of truth
4. Each task: one sentence, no "and"
5. **No code changes in this phase.**

Usually 1-2 iterations. Regenerate plan if it drifts — cheaper than salvaging.

## Phase 2: Building (one task per iteration, fresh context)

Each iteration is a **fresh session**. Context accumulation causes hallucination.

1. Read `IMPLEMENTATION_PLAN.md` + relevant code (nothing else)
2. Pick highest-priority incomplete task
3. Investigate codebase (don't assume)
4. Implement ONE task only
5. Run backpressure gates: slop scan + type check on diff
6. Gates pass → run tests, commit, update plan, exit
7. Gates fail → fix in same iteration, re-run
8. Exit → fresh context → next iteration

**Why fresh context:** No accumulated mistakes. Full token budget for THIS task. Reduced hallucination. Natural checkpoints.

**Backpressure:** Loop CANNOT advance past a failing gate. Environment rejects bad work automatically.

## Phase 3: Verification (after all tasks complete)

1. All tasks in plan marked complete
2. All programmatic gates passing
3. Run remaining gates (test quality, mutation kill, etc.)
4. Verification fails → loop back to Phase 2 with fix tasks

## Worker Roles

| Role | Use For |
|------|---------|
| **Architect** | Phase 1: design, data modeling, API contracts |
| **Implementer** | Phase 2: write code, implement features |
| **Tester** | Phase 2: test authoring, edge cases |
| **Reviewer** | Phase 3: code review, quality assessment |

Each role is a prompt prefix. One worker, one role, one task.

## Stopping Conditions

- ✅ All tasks complete + all gates pass
- ⚠️ Hard cap hit (default: 50 iterations) → Blocked with all evidence
- 🛑 Manual stop (`/stop` or `/subagents kill all`)


path: references/gates/red-team.md
content: # Gate: Adversarial Red-Team

**Question:** Can an attacker break, bypass, or exploit this code?

Run `scripts/red-team.sh [path]` first. Then manually review the findings.

---

## Categories

### 1. Injection Attacks

Check for **SQL injection:**
- String concatenation in queries (`"SELECT * FROM " + userInput`)
- f-strings or template literals building raw SQL
- Missing parameterized queries

Check for **Command injection:**
- `exec()`, `eval()`, `spawn()`, `popen()`, `system()` with user input
- Shell=True in subprocess calls
- Unescaped user data in shell commands

Check for **Path traversal:**
- `../`, `%2e%2e%2f`, `..%5c` in file path construction
- Missing path normalization before file operations
- File reads/writes using user-supplied filenames without validation

Check for **Template injection:**
- User input rendered directly in templates (Jinja2, Handlebars, etc.)
- `render(template=userInput)` patterns
- Unsafe `eval` of template strings

Check for **XSS (if web-facing):**
- Unescaped user input in HTML responses
- `innerHTML = userInput` patterns
- Missing Content-Security-Policy headers

### 2. Authentication & Authorization Bypasses

Check for:
- Routes/endpoints without auth middleware applied
- Auth checks that can be bypassed by changing HTTP method
- JWT algorithms set to `none`
- Hardcoded credentials or API keys in source
- Secrets in environment variable names visible in error messages
- Missing rate limiting on auth endpoints
- Predictable session tokens or reset tokens

### 3. Race Conditions & TOCTOU

Check for:
- Time-Of-Check-Time-Of-Use (check file exists, then use it — window in between)
- Non-atomic read-modify-write operations on shared state
- Double-spend patterns (balance checked, then decremented in separate operations)
- Missing database transactions around multi-step operations
- File operations without locking

### 4. Input Abuse

Test with adversarial inputs:
- **Null bytes**: `\x00` in strings (can truncate strings in some languages)
- **Unicode attacks**: Homoglyph substitution, overlong UTF-8, RTL override characters
- **Oversized payloads**: Empty string, single char, 1MB string, 1GB string
- **Negative/boundary numbers**: -1, 0, MAX_INT, MIN_INT, NaN, Infinity
- **Type confusion**: Pass string where int expected, array where string expected
- **Empty collections**: Empty array, empty object, null vs undefined
- **Special characters**: `'`, `"`, `;`, `\n`, `\r\n`, `\0`, `<`, `>`, `&`

### 5. Resource Exhaustion

Check for:
- **Regex DoS (ReDoS)**: Exponential-time regexes on user input (`(a+)+`, `([a-zA-Z]+)*`)
- **Recursive structures**: Missing depth limits on recursive parsing/traversal
- **Memory bombs**: Allocating large buffers based on user-controlled size
- **Zip bombs**: Compressed files expanded without size limits
- **Infinite loops**: User input that can cause loops to never terminate
- **File descriptor exhaustion**: Opening files without closing them in error paths

### 6. Cryptographic Weaknesses

Check for:
- Weak algorithms: MD5, SHA1 for security purposes, DES, 3DES, RC4
- ECB mode block cipher usage
- Hardcoded IVs or nonces
- Predictable random number generation (`Math.random()`, `rand()` for security)
- Missing certificate validation (`verify=False`, `checkServerIdentity: () => {}`)
- Passwords stored as plain text or weak hash (MD5/SHA1)
- Timing attacks in credential comparison (use constant-time compare)

### 7. Error Handling & Information Leaks

Check for:
- Stack traces returned to clients in production
- Verbose error messages exposing internal paths, DB schemas, library versions
- Exception handlers that swallow errors silently
- Debug modes left on in production config
- Sensitive data (passwords, tokens, PII) in log output
- Missing error handling in async code (unhandled promise rejections)

### 8. Deserialization & Prototype Pollution

Check for:
- Unsafe deserialization: `pickle.loads(userInput)`, `YAML.load(userInput)` (vs `safe_load`)
- `JSON.parse` on untrusted input fed directly to eval
- Prototype pollution: `Object.assign({}, userInput)` where keys aren't validated
- `__proto__`, `constructor`, `prototype` keys in user-controlled objects
- Node.js `require()` with user-controlled paths

---

## Pass/Fail Criteria

| Finding | Severity | Effect on Verdict |
|---------|----------|------------------|
| SQL/Command/Path injection | BLOCKER | Blocked 🚫 |
| Auth bypass | BLOCKER | Blocked 🚫 |
| Hardcoded secret in source | BLOCKER | Blocked 🚫 |
| Unsafe deserialization | BLOCKER | Blocked 🚫 |
| ReDoS with user input | BLOCKER | Blocked 🚫 |
| Weak crypto for security | WARNING | Caution ⚠️ |
| Info leak in errors | WARNING | Caution ⚠️ |
| Missing rate limiting | WARNING | Caution ⚠️ |
| Race condition | WARNING | Caution ⚠️ |
| TOCTOU | WARNING | Caution ⚠️ |
| Debug flag on | INFO | Note only |
| Missing input validation | Depends on context | BLOCKER if injection possible, WARNING otherwise |

**Pass:** Zero BLOCKER findings  
**Caution:** WARNING findings only, no BLOCKERs  
**Fail/Blocked:** Any BLOCKER finding

---

## Script Output

`scripts/red-team.sh [path]` outputs:
```json
{
  "project": "/path/to/project",
  "findings": [
    {
      "id": "SQL_CONCAT",
      "category": "injection",
      "pattern": "SQL string concatenation",
      "severity": "blocker",
      "file": "src/db.js",
      "line": 42,
      "evidence": "query = \"SELECT * FROM users WHERE id = \" + userId",
      "recommendation": "Use parameterized queries"
    }
  ],
  "summary": {
    "total": 3,
    "blockers": 1,
    "warnings": 2,
    "info": 0
  },
  "verdict": "FAIL"
}
```


path: references/gates/regression.md
content: # Gate: Regression (REBUILD + FIX modes)

**Question:** Did we break anything that was working?

## Process

Run the ENTIRE existing test suite (not just new tests).

## Pass/Fail

- **Pass:** All previously-passing tests still pass.
- **Fail:** Any previously-passing test now fails.


path: references/gates/sast.md
content: # Gate: SAST (FIX + AUDIT modes)

**Question:** Did the fix introduce new vulnerabilities?

## Checks

Review the diff (not the whole codebase) for:
- SQL injection, XSS, command injection, path traversal
- Hardcoded secrets or credentials
- Missing input validation
- Missing error handling
- Auth/authz bypass
- Insecure defaults

## Pass/Fail

- **Pass:** Diff introduces zero new security concerns.
- **Fail:** Any new vulnerability in the diff.


path: references/gates/slop-scan.md
content: # Gate: AI Slop Scan

**Question:** Is this real code or confident-sounding garbage?

## Checks

- **Hallucinated dependencies** — imports/requires for packages that don't exist in any registry
- **Phantom imports** — importing from modules that don't exist in the project
- **Placeholder code** — `TODO`, `FIXME`, `implement this`, `pass`, `...`, empty function bodies
- **Template artifacts** — lorem ipsum, example.com URLs, hardcoded test credentials, `YOUR_API_KEY_HERE`
- **Dead code** — functions defined but never called, unreachable branches
- **Confident nonsense** — syntactically valid code that does nothing meaningful (e.g., try/catch that catches and ignores)

## Scripts

Run `scripts/check-deps.sh` for deterministic dependency verification against registries.

## Pass/Fail

- **Pass:** Zero hallucinated deps, zero phantom imports, no placeholder code in production paths.
- **Fail:** Any hallucinated dependency is an automatic fail. Placeholders in production code = fail.

**Severity:** Hallucinated deps = BLOCKER (stop everything). Placeholders = FAIL. Dead code = WARN.


path: references/gates/test-quality.md
content: # Gate: Test Quality

**Question:** Are the tests actually testing anything?

## Checks (NOT just line coverage)

- **Assertion density per test** — tests with zero or one assertion are suspect
- **Branch coverage** — did both sides of every if/else execute?
- **Edge case presence** — tests for: empty input, null, boundary values, error paths?
- **Independence** — does each test fail independently or share mutable state?
- **Naming** — do test names describe behavior or just method names?

## Scripts

Run `scripts/coverage-stats.sh` for raw coverage numbers, then AI-analyze the test files.

## Pass/Fail

- **Pass:** Every public function has ≥1 test with ≥2 meaningful assertions. Edge cases covered for critical paths.
- **Fail:** Any public function with zero tests. Any test that's just `expect(true).toBe(true)`.


path: references/gates/type-check.md
content: # Gate: Type Check

**Question:** Does it compile?

## Process

Run `scripts/detect-stack.sh` to auto-detect language, then execute:

| Language | Command |
|----------|---------|
| TypeScript | `tsc --noEmit` |
| Python | `mypy --strict` (or `pyright`) |
| Rust | `cargo check` |
| Go | `go vet ./...` |
| Java/Kotlin | `./gradlew compileJava` or `mvn compile` |
| Swift | `swift build` |
| No type system | Skip gate, note in proof bundle |

## Pass/Fail

- **Pass:** Zero type errors.
- **Fail:** Any type error. No exceptions.


path: references/swarm/collect.md
content: # Worker Completion Protocol (Collect Pattern)

## The Problem

The orchestrator spawns parallel workers via `sessions_spawn`. Worker results arrive
asynchronously via announce messages. If the orchestrator doesn't wait, it will
**fabricate results** — this happened on the first midas-mcp audit (Feb 16, 2026).

---

## Anti-Fabrication Rule

> **NEVER write the proof bundle until ALL spawned workers have explicitly reported back.**
> **NEVER make up results for workers that haven't responded.**
> **NEVER assume a worker succeeded or failed without its actual announce message.**
> **If a worker output does not contain a `gate_result` field → mark it FAILED, not assumed-passed.**
> **If a worker is silent for > 10 min → mark it TIMED_OUT, not assumed-passed.**

---

## Collect Protocol

### Step 1: Before spawning — declare your checklist

Output this BEFORE spawning any workers:

```
VERIFICATION WORKERS CHECKLIST:
- wreckit-slop: PENDING
- wreckit-typecheck: PENDING
- wreckit-testquality: PENDING
- wreckit-mutation: PENDING
- wreckit-security: PENDING

Spawning now. Will not write proof bundle until all are ✅ or ❌.
```

### Step 2: Spawn all parallel workers simultaneously

Each worker task MUST end with:
```
"Your first line of response must be: WRECKIT-[LABEL]: [PASS|FAIL|WARN|ERROR]"
```

### Step 3: After each announce arrives — update checklist

When a worker's announce comes in:
1. Check its first line for `WRECKIT-[LABEL]:` status
2. Update the checklist
3. Re-output the full updated checklist

Example after 2 of 5 report back:
```
VERIFICATION WORKERS CHECKLIST (updated 14:23):
- wreckit-slop: PASS ✅ (received 14:21)
- wreckit-typecheck: PASS ✅ (received 14:23)
- wreckit-testquality: PENDING ⏳
- wreckit-mutation: PENDING ⏳
- wreckit-security: PENDING ⏳

3 workers still running. Waiting...
```

### Step 4: Check completion gate

After each announce, check: are ALL workers complete?
- **YES** → proceed to proof bundle
- **NO** → output updated checklist and wait

**This is the only condition under which you may write the proof bundle.**

---

## Timeout Handling

| Time elapsed | Action |
|-------------|--------|
| 0–5 min | Wait normally; do NOT poll yet |
| 5 min | Check `sessions_list` to confirm still running |
| 10 min | Mark silent worker as `TIMED_OUT` — do NOT fabricate results |
| After timeout | Proof bundle notes incomplete gate; verdict ≤ CAUTION |

### Timeout Pseudocode

```
spawn_time = now()

for worker in pending:
  elapsed = now() - spawn_time
  if elapsed > 10 minutes AND worker NOT in results:
    results[worker] = {
      status:     "TIMED_OUT",
      gate_result: null,
      note:       "Worker silent for > 10min. Result NOT fabricated."
    }
```

### Handoff Validation

Before accepting a worker result, check for `gate_result` field:

```
def validate_worker_output(worker_name, output):
  if "gate_result" not in output:
    return {
      status:     "FAILED",
      gate_result: null,
      note:       f"{worker_name} output missing required gate_result field"
    }
  return output  # valid
```

### On timeout example:
```
VERIFICATION WORKERS CHECKLIST (14:45 — TIMEOUT):
- wreckit-slop: PASS ✅
- wreckit-typecheck: PASS ✅
- wreckit-testquality: PASS ✅
- wreckit-mutation: TIMED_OUT ❌ (silent > 10min — result NOT fabricated)
- wreckit-security: PASS ✅

wreckit-mutation timed out. Proof bundle will note this as an incomplete gate.
Verdict CANNOT be Ship with an incomplete gate — must be Caution at minimum.
```

---

## Verification Checklist (Pre-Proof-Bundle)

Before writing the proof bundle, you MUST output this checklist and confirm all boxes:

```
PRE-PROOF-BUNDLE VERIFICATION:
[ ] All workers have reported (or timed out with ERROR status)
[ ] No results are assumed or fabricated
[ ] Each result has an actual announce message to reference
[ ] Checklist shows final status for all workers
[ ] Decision is based ONLY on confirmed results

If any box is unchecked: DO NOT write the proof bundle.
```

---

## What WRONG Looks Like

```
❌ Orchestrator spawns 5 workers
❌ Only 1 announces back
❌ Orchestrator writes proof bundle with results for all 5
❌ 4 results are "likely passed" or assumed
❌ Kill rate is a made-up number
```

## What RIGHT Looks Like

```
✅ Orchestrator spawns 5 workers
✅ Declares checklist with all PENDING
✅ Updates checklist as each announces
✅ 4 complete ✅, 1 times out ❌ (ERROR)
✅ Pre-proof-bundle checklist confirms no fabrication
✅ Proof bundle shows 4 real results + 1 ERROR (timed out after 15min)
✅ Verdict: Caution ⚠️ (incomplete gate = can't Ship)
```


path: references/swarm/handoff.md
content: # Worker Handoff Format

Every worker reports back with this structured format. No exceptions.

## Template

```
## [Gate Name] Result

**Status:** PASS | FAIL | WARN | ERROR
**Label:** wreckit-[gate-name]
**Duration:** Xs
**Summary:** One-line description

### What Was Done
[specific actions taken, files checked, commands run]

### Findings
- Finding 1 (severity: blocker/warning/info)
- Finding 2 ...

### Evidence
[raw output, file paths, test results]

### Known Issues
[anything incomplete or uncertain]

### Recommendation
[what to do about it — specific, actionable]
```

## Rules

- **First line of announce must include label + status** (for orchestrator parsing)
- **Bad handoff:** "Looks fine." 
- **Good handoff:** "Slop scan found 2 phantom imports in `src/utils.ts` (lines 3, 17) — `import { foo } from './nonexistent'`. phantom-pkg returns 404 on npm. Recommendation: remove both."
- **If the gate errors out**, still report with Status: ERROR and explain what went wrong
- **Never omit the Status field** — orchestrator depends on it for the collect pattern


path: references/swarm/orchestrator-prompt.md
content: # Orchestrator System Prompt (Copy-Paste Template)

Use this as the `task` when spawning the wreckit orchestrator via sessions_spawn.
Replace all [BRACKETED] values before pasting.

---

## Template

```
You are the wreckit orchestrator for a code verification run.

PROJECT: [/absolute/path/to/project]
MODE: [BUILD|REBUILD|FIX|AUDIT]
SPEC/PRD: [path to PRD, bug report, or migration spec — or paste inline]
WRECKIT SKILL: ~/Projects/wreckit-ralph/

Your job is to run all wreckit gates and produce a proof bundle with a Ship/Caution/Blocked verdict.

## MANDATORY: Read These Before Anything Else
1. ~/Projects/wreckit-ralph/SKILL.md — full gate list and decision framework
2. ~/Projects/wreckit-ralph/references/swarm/orchestrator.md — exact spawning protocol
3. ~/Projects/wreckit-ralph/references/swarm/collect.md — anti-fabrication rules
4. ~/Projects/wreckit-ralph/references/swarm/handoff.md — worker report format

## ANTI-FABRICATION OATH
Before starting, output this literally:
"I will not write the proof bundle until ALL workers have announced back.
I will not fabricate any results. If a worker times out, I will mark it ERROR.
My checklist will be updated after every single announce."

## PHASE 1: Validate Config
Check that maxSpawnDepth >= 2 and maxChildrenPerAgent >= 8.
If not: stop and tell the user to update their config.

## PHASE 2: Planning (if BUILD/REBUILD/FIX mode)
Spawn wreckit-architect worker. Wait for "ARCHITECT COMPLETE" before proceeding.
Skip for AUDIT mode.

## PHASE 3: Building (if BUILD/REBUILD/FIX mode)
For each task in IMPLEMENTATION_PLAN.md, spawn one implementer.
Wait for each IMPLEMENTER-N COMPLETE before spawning the next.
Skip for AUDIT mode.

## PHASE 4: Parallel Verification — CRITICAL SECTION
BEFORE SPAWNING: Output the full verification checklist with all workers as PENDING.
THEN: Spawn all 11 verification workers simultaneously:
  - wreckit-slop (all modes)
  - wreckit-typecheck (all modes)
  - wreckit-testquality (all modes)
  - wreckit-mutation (all modes)
  - wreckit-security (all modes)
  - wreckit-dynamic (all modes)
  - wreckit-perf (all modes)
  - wreckit-property (all modes)
  - wreckit-design (AUDIT + REBUILD only)
  - wreckit-ci (BUILD + REBUILD + AUDIT)
  - wreckit-differential (BUILD + REBUILD only)

Use these scripts (~/Projects/wreckit-ralph/scripts/):
  - slop-scan.sh, coverage-stats.sh, mutation-test.sh, red-team.sh
  - dynamic-analysis.sh, perf-benchmark.sh, property-test.sh
  - design-review.sh, ci-integration.sh, differential-test.sh
  - ralph-loop.sh [path] — validates IMPLEMENTATION_PLAN.md structure (BUILD/REBUILD/FIX)
  - type-check.sh [path] — runs type checker, JSON output
  - behavior-capture.sh [path] — captures golden fixtures before rebuild (REBUILD only)
  - proof-bundle.sh [path] [mode] — deterministic proof writer (pipe gate results JSON array to stdin)

THEN: STOP. Do not proceed until you have received announces from ALL workers.
Update the checklist after every announce.
Only after ALL workers are ✅ or ❌: proceed to Phase 5.

## PHASE 5: Sequential Verification
Spawn cross-verify (BUILD) or regression (REBUILD/FIX) worker. Wait for completion.

## PHASE 6: Proof Bundle
Output the pre-proof-bundle verification checklist (all boxes must be checked).
Only then write the proof bundle to [PROJECT]/.wreckit/
Final verdict: Ship ✅ / Caution ⚠️ / Blocked 🚫

Output your final verdict as the last line of your response.
```

---

## Minimal AUDIT-only Template

```
You are the wreckit orchestrator for a code audit (no changes).

PROJECT: [/absolute/path/to/project]
WRECKIT SKILL: ~/Projects/wreckit-ralph/

Read: SKILL.md, references/swarm/orchestrator.md, references/swarm/collect.md

Anti-fabrication oath first. Then:
1. Run scripts/detect-stack.sh [path] to understand the project
2. Spawn all 11 parallel verification workers:
   slop, typecheck, testquality, mutation, security,
   dynamic, perf, property, design, ci, differential
3. Declare checklist BEFORE spawning. Update after every announce. Do not proceed until all complete.
4. Write proof bundle to [path]/.wreckit/
5. Final verdict: Ship ✅ / Caution ⚠️ / Blocked 🚫
```


path: references/swarm/orchestrator.md
content: # Swarm Orchestrator Pattern

> **CRITICAL:** Read `collect.md` completely before spawning any workers.
> The #1 failure mode is writing the proof bundle before all workers report back.
> This happened on midas-mcp (Feb 16, 2026). Never again.

---

## Architecture

```
Main agent → wreckit orchestrator (depth 1)
  ├─ PHASE 1: Planning
  │   └→ Architect worker (depth 2)
  ├─ PHASE 2: Building (sequential, one worker per task)
  │   ├→ Implementer-1 (depth 2)
  │   ├→ Implementer-2 (depth 2)
  │   └→ ...
  ├─ PHASE 3: Parallel Verification
  │   ├→ slop-scan worker (depth 2)
  │   ├→ type-check worker (depth 2)
  │   ├→ test-quality worker (depth 2)
  │   ├→ mutation-kill worker (depth 2)
  │   ├→ security worker (depth 2)
  │   ├→ dynamic-analysis worker (depth 2)
  │   ├→ perf-benchmark worker (depth 2)
  │   ├→ property-test worker (depth 2)
  │   ├→ design-review worker (depth 2) [AUDIT + REBUILD only]
  │   ├→ ci-integration worker (depth 2) [BUILD + REBUILD + AUDIT]
  │   └→ differential-test worker (depth 2) [BUILD + REBUILD only]
  ├─ PHASE 4: Sequential Verification
  │   ├→ cross-verify (if BUILD mode)
  │   ├→ regression (if REBUILD/FIX mode)
  │   └→ llm-judge (optional)
  └─ PHASE 5: Decision
      └→ Proof bundle → Ship / Caution / Blocked
```

---

## Config Required

```json
{
  "agents": {
    "defaults": {
      "subagents": {
        "maxSpawnDepth": 2,
        "maxChildrenPerAgent": 12,
        "maxConcurrent": 12
      }
    }
  }
}
```

If these aren't set, abort and tell the user to update their config.

---

## Model Selection Per Role

| Role | Model | Reason |
|------|-------|--------|
| Orchestrator | Opus | Ship/Caution/Blocked decisions require judgment |
| Architect | Opus | Planning + gap analysis needs quality |
| Implementer | Opus | Code quality matters for implementation |
| Slop scan | Sonnet | Pattern analysis, not generation |
| Type check | Haiku | Just runs a CLI command |
| Test quality | Sonnet | Analysis |
| Mutation kill | Sonnet | Simple mutations + test running |
| Security (SAST) | Sonnet | Pattern recognition |
| Dynamic Analysis | Sonnet | Script execution + analysis |
| Perf Benchmark | Sonnet | Script execution + analysis |
| Property-Based Testing | Sonnet | Test execution + analysis |
| Design Review | Sonnet | Pattern analysis (AUDIT + REBUILD only) |
| CI Integration | Sonnet | Pipeline execution (BUILD + REBUILD + AUDIT) |
| Differential Testing | Sonnet | Comparison analysis (BUILD + REBUILD only) |
| Cross-verify | Opus | Regenerating code for comparison needs quality |
| LLM-as-judge | Opus | Subjective judgment |

---

## Phase 1: Planning

Spawn ONE architect worker. Wait for it to complete before spawning builders.

```
sessions_spawn:
  task: "You are the Architect for a wreckit verification run.
    Project: [path]
    Mode: [BUILD/REBUILD/FIX/AUDIT]
    PRD/spec: [contents or path]

    1. Read the PRD or bug report
    2. Identify all tasks needed (list each discrete unit of work)
    3. Write IMPLEMENTATION_PLAN.md to the project root with:
       - Task list (numbered)
       - Acceptance criteria per task
       - Risk flags (potential failure modes)
       - Suggested test approach
    4. When done, your first line of response must be: ARCHITECT COMPLETE"
  label: wreckit-architect
  model: anthropic/claude-opus-4-6
```

**Wait for ARCHITECT COMPLETE before proceeding.**

---

## Phase 2: Building (Sequential)

Before building tasks (BUILD/REBUILD/FIX modes only), run these workers:

**Behavior Capture (REBUILD mode only):**
```
sessions_spawn:
  task: "wreckit gate: Behavior Capture.
    Project: [path]
    Run: ~/Projects/wreckit-ralph/scripts/behavior-capture.sh [path]
    Read references/gates/behavior-capture.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-BEHAVIOR: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-behavior
  model: anthropic/claude-sonnet-4-6
```

**Ralph Loop (BUILD/REBUILD/FIX modes only):**
```
sessions_spawn:
  task: "wreckit gate: Ralph Loop.
    Project: [path]
    Run: ~/Projects/wreckit-ralph/scripts/ralph-loop.sh [path]
    Then read references/gates/ralph-loop.md for additional AI-driven checks.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-RALPH: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-ralph
  model: anthropic/claude-sonnet-4-6
```

For each task in IMPLEMENTATION_PLAN.md, spawn ONE implementer. Wait for each to complete before spawning the next.

```
sessions_spawn:
  task: "You are Implementer [N] for a wreckit verification run.
    Project: [path]
    Your task: [task description from IMPLEMENTATION_PLAN.md]
    Acceptance criteria: [criteria]

    1. Implement ONLY this one task
    2. Run type checker immediately after
    3. Run slop-scan.sh immediately after
    4. If either fails, fix before reporting back
    5. Commit your work: git add -A && git commit -m '[task name]'
    6. First line of response: IMPLEMENTER-[N] COMPLETE: [pass/fail] [brief summary]"
  label: wreckit-build-N
  model: anthropic/claude-opus-4-6
```

**Wait for each IMPLEMENTER-N COMPLETE before spawning the next.**

---

## Phase 3: Parallel Verification

Spawn ALL verification workers simultaneously. Then apply the collect pattern from `collect.md`.

### Pre-Spawn Checklist
Before spawning, write this checklist in your response:

```
VERIFICATION WORKERS CHECKLIST:
- wreckit-slop: PENDING
- wreckit-typecheck: PENDING
- wreckit-testquality: PENDING
- wreckit-mutation: PENDING
- wreckit-security: PENDING
- wreckit-dynamic: PENDING
- wreckit-perf: PENDING
- wreckit-property: PENDING
- wreckit-design: PENDING  [AUDIT + REBUILD only]
- wreckit-ci: PENDING  [BUILD + REBUILD + AUDIT]
- wreckit-differential: PENDING  [BUILD + REBUILD only]
```

### Spawn All Workers

**Slop Scan:**
```
sessions_spawn:
  task: "wreckit gate: AI Slop Scan.
    Project: [path]
    Run: ~/Projects/wreckit-ralph/scripts/slop-scan.sh [path]
    Then read references/gates/slop-scan.md and apply additional checks.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-SLOP: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-slop
  model: anthropic/claude-sonnet-4-6
```

**Type Check:**
```
sessions_spawn:
  task: "wreckit gate: Type Check.
    Project: [path]
    Run: ~/Projects/wreckit-ralph/scripts/type-check.sh [path]
    (falls back to detect-stack.sh type checker command if needed)
    Read references/gates/type-check.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-TYPECHECK: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-typecheck
  model: anthropic/claude-haiku-3.5
```

**Test Quality:**
```
sessions_spawn:
  task: "wreckit gate: Test Quality.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/coverage-stats.sh [path]
    Read references/gates/test-quality.md for checks.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-TESTQUALITY: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-testquality
  model: anthropic/claude-sonnet-4-6
```

**Mutation Kill:**
```
sessions_spawn:
  task: "wreckit gate: Mutation Kill.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/mutation-test.sh [path]
    Read references/gates/mutation-kill.md for thresholds (>=95% ship, 90-94 caution, <90 blocked).
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-MUTATION: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-mutation
  model: anthropic/claude-sonnet-4-6
```

**Security (SAST):**
```
sessions_spawn:
  task: "wreckit gate: Security / SAST.
    Project: [path]
    Read references/gates/sast.md and execute all checks.
    Run ~/Projects/wreckit-ralph/scripts/red-team.sh [path] if it exists.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-SECURITY: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-security
  model: anthropic/claude-sonnet-4-6
```

**Dynamic Analysis:**
```
sessions_spawn:
  task: "wreckit gate: Dynamic Analysis.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/dynamic-analysis.sh [path]
    Read references/gates/dynamic-analysis.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-DYNAMIC: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-dynamic
  model: anthropic/claude-sonnet-4-6
```

**Performance Benchmark:**
```
sessions_spawn:
  task: "wreckit gate: Performance Benchmark.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/perf-benchmark.sh [path]
    Read references/gates/performance.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-PERF: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-perf
  model: anthropic/claude-sonnet-4-6
```

**Property-Based Testing:**
```
sessions_spawn:
  task: "wreckit gate: Property-Based Testing.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/property-test.sh [path]
    Read references/gates/property-based.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-PROPERTY: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-property
  model: anthropic/claude-sonnet-4-6
```

**Design Review (AUDIT + REBUILD only):**
```
sessions_spawn:
  task: "wreckit gate: Design Review.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/design-review.sh [path]
    Read references/gates/design-review.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-DESIGN: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-design
  model: anthropic/claude-sonnet-4-6
```

**CI Integration (BUILD + REBUILD + AUDIT):**
```
sessions_spawn:
  task: "wreckit gate: CI Integration.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/ci-integration.sh [path]
    Read references/gates/ci-integration.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-CI: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-ci
  model: anthropic/claude-sonnet-4-6
```

**Differential Testing (BUILD + REBUILD only):**
```
sessions_spawn:
  task: "wreckit gate: Differential Testing.
    Project: [path]
    Run ~/Projects/wreckit-ralph/scripts/differential-test.sh [path]
    Read references/gates/differential.md for pass/fail criteria.
    Report format: follow references/swarm/handoff.md exactly.
    First line: WRECKIT-DIFFERENTIAL: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-differential
  model: anthropic/claude-sonnet-4-6
```

### After Spawning: STOP AND WAIT — Explicit Collect Loop

**DO NOT proceed until all 11 workers report back.**
See `collect.md` for the full collect protocol.

#### Collect Loop Pseudocode

```
spawned = ["wreckit-slop", "wreckit-typecheck", "wreckit-testquality",
           "wreckit-mutation", "wreckit-security",
           "wreckit-dynamic", "wreckit-perf", "wreckit-property",
           "wreckit-design", "wreckit-ci", "wreckit-differential"]
# Note: wreckit-design spawned for AUDIT + REBUILD only
# Note: wreckit-ci spawned for BUILD + REBUILD + AUDIT
# Note: wreckit-differential spawned for BUILD + REBUILD only
results = {}
spawn_time = now()

WAIT_LOOP:
  completed = count(results)
  pending   = spawned - completed

  if completed == len(spawned):
    break  # all done → proceed to Phase 4

  # Poll sessions_list every 30 seconds
  sleep(30)
  active_sessions = sessions_list()

  for worker in pending:
    if worker NOT in active_sessions:
      # Worker exited — check if it sent a result
      if worker NOT in results:
        elapsed = now() - spawn_time
        if elapsed > 10 minutes:
          results[worker] = { status: "TIMED_OUT", gate_result: null }
          # DO NOT fabricate — mark explicitly as TIMED_OUT
        # else: keep waiting, may still announce

  # Validate each received result
  for worker, output in results:
    if "gate_result" NOT in output:
      results[worker].status = "FAILED"  # missing required field

  # Re-output updated checklist
  print_checklist(spawned, results)

# On timeout (worker silent > 10 min):
#   Mark status = TIMED_OUT, NOT fabricated
#   Proof bundle MUST note the incomplete gate
#   Verdict CANNOT be Ship with a TIMED_OUT gate
```

#### Timeout Rules

| Time since spawn | Action |
|-----------------|--------|
| 0–5 min | Wait normally |
| 5 min | Check `sessions_list` to confirm still running |
| 10 min | Mark silent worker as `TIMED_OUT` — do NOT fabricate |
| After timeout | Note in proof bundle; verdict ≤ CAUTION |

#### Worker Output Validation

Every worker result MUST contain `gate_result` field. If missing:
- Mark worker status as `FAILED` (not fabricated, explicitly failed)
- Record the missing field in the proof bundle
- Do NOT assume the gate passed

---

## Phase 4: Sequential Verification

After all parallel workers complete:

**Cross-verify (BUILD mode only):**
```
sessions_spawn:
  task: "wreckit gate: Cross-Verify (Independent Regeneration).
    Project: [path]
    Original code: [path]
    Read references/gates/cross-verify.md.
    Regenerate the core logic from scratch WITHOUT reading the implementation.
    Compare outputs on test cases. Report discrepancies.
    First line: WRECKIT-CROSSVERIFY: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-crossverify
  model: anthropic/claude-opus-4-6
```

**Regression (REBUILD/FIX mode only):**
```
sessions_spawn:
  task: "wreckit gate: Regression.
    Project: [path]
    Behavior snapshots: [path to snapshots]
    Read references/gates/regression.md.
    Run captured behavior tests against new implementation.
    First line: WRECKIT-REGRESSION: [PASS|FAIL|WARN|ERROR]"
  label: wreckit-regression
  model: anthropic/claude-sonnet-4-6
```

---

## Phase 5: Decision

Only after ALL workers from phases 3 and 4 have reported:

1. Collect all results
2. Apply decision framework from SKILL.md
3. Run: echo '[...gate-results-json-array...]' | ~/Projects/wreckit-ralph/scripts/proof-bundle.sh [path] [mode]
   (pipe the collected gate results JSON array to proof-bundle.sh — it calculates verdict deterministically)
4. Output final verdict: **Ship ✅ / Caution ⚠️ / Blocked 🚫**

---

## Failure Handling

| Scenario | Action |
|----------|--------|
| Worker hallucinated deps in slop scan | STOP everything → Blocked |
| One gate fails, others pass | Complete all gates, report all results |
| Mutation kill fails | Loop back to Ralph loop with "strengthen tests" |
| Worker times out (5 min) | Check subagents list, wait up to 10 min |
| Worker crashes/gone | Mark as ERROR, note in proof bundle, continue |
| Hard cap: 50 iterations | Still failing → Blocked with all evidence |

---

## ANTI-FABRICATION RULES (Never Do These)

❌ NEVER write the proof bundle before all workers report  
❌ NEVER assume a worker passed because it was spawned successfully  
❌ NEVER fill in missing results with "likely passed" or "expected pass"  
❌ NEVER skip a worker because you think it's unnecessary  
❌ NEVER report a kill rate you didn't actually measure  
❌ NEVER copy results from a previous run if this is a new run  

---

## Correct vs Wrong Behavior

### ✅ CORRECT
```
Spawned 11 workers.

VERIFICATION WORKERS CHECKLIST:
- wreckit-slop: PENDING
- wreckit-typecheck: PENDING
- wreckit-testquality: PENDING
- wreckit-mutation: PENDING
- wreckit-security: PENDING
- wreckit-dynamic: PENDING
- wreckit-perf: PENDING
- wreckit-property: PENDING
- wreckit-design: PENDING
- wreckit-ci: PENDING
- wreckit-differential: PENDING

[waiting...]

Received: WRECKIT-SLOP: PASS
Updated checklist:
- wreckit-slop: PASS ✅
- wreckit-typecheck: PENDING ⏳
- wreckit-testquality: PENDING ⏳
- wreckit-mutation: PENDING ⏳
- wreckit-security: PENDING ⏳
- wreckit-dynamic: PENDING ⏳
- wreckit-perf: PENDING ⏳
- wreckit-property: PENDING ⏳
- wreckit-design: PENDING ⏳
- wreckit-ci: PENDING ⏳
- wreckit-differential: PENDING ⏳

[still waiting for 10 more...]

Received: WRECKIT-TYPECHECK: PASS
...

All 11 workers complete. Writing proof bundle now.
```

### ❌ WRONG (what happened on midas-mcp)
```
Spawned 11 workers.

[1 worker responded]

All workers complete! Results:
- Slop scan: PASS (assumed)
- Type check: PASS (assumed)
- Test quality: PASS (assumed)
- Mutation kill: 97% kill rate (fabricated)
- Security: PASS (assumed)
- Dynamic Analysis: PASS (assumed)
- Perf Benchmark: PASS (assumed)
- Property-Based Testing: PASS (assumed)
- Design Review: PASS (assumed)
- CI Integration: PASS (assumed)
- Differential Testing: PASS (assumed)

Verdict: Ship ✅
```

## Adaptive Gate Behavior (added 2026-02-22)

Based on live runs against zod and chatbot-ui, the orchestrator should adapt:

### Skip gates when irrelevant:
- Skip `coverage_stats` and `mutation_kill` if `detect-stack.sh` returns `testRunner: none` or no test script
- Skip `ralph_loop` in AUDIT mode unless `IMPLEMENTATION_PLAN.md` exists
- Skip `behavior_capture` unless mode == REBUILD

### Early exit on critical failures:
- `check_deps` FAIL → BLOCKED immediately (hallucinated deps = unshippable)
- `slop_scan` finds >50 artifacts → BLOCKED immediately
- `type_check` > 10 errors → BLOCKED immediately
- Log reason in proof bundle

### Project size adaptation:
- >100 source files: set `WRECKIT_LARGE_PROJECT=1` (scripts should use sampling)
- Monorepos (multiple `package.json`): run detect-stack per sub-package

### Telemetry-driven gate ordering (fast-fail first):
After runs accumulate in `.wreckit/metrics.json`, reorder gates by:
1. check_deps (fast, catches hallucinated deps)
2. slop_scan (fast, catches obvious problems)
3. type_check (catches type errors)
4. coverage_stats / ci_integration (medium speed)
5. design_review / red_team (slower, run after fast gates pass)
6. mutation_kill (slowest — only if above all pass)


path: references/verification-framework.md
content: # wreckit Verification Framework — Harmonized Checklist

_The "closest-to-bulletproof" AI code verification pipeline. AI tests + scripts, but with independent checks._

**Core principle:** Use AI aggressively, but structure it so it **can't silently agree with itself.**

---

## The 14-Step Pipeline

### 1. Freeze a Formal Spec (Source of Truth)
- Write: expected behavior, error handling, constraints, security assumptions
- Extract invariants ("must always hold") + forbidden behaviors ("must never happen")
- Performance expectations included
- AI can draft this; a human should sanity-check it

### 2. Establish Independent Oracles (Anchor to Reality)
Pick at least one:
- Reference implementation (trusted library / known-good version)
- Brute-force "slow but obviously correct" checker for small inputs
- Golden tests with hand-verified expected outputs
- Metamorphic properties (relationships that must hold under transformations)

**Why:** Prevents "AI wrote code, AI wrote tests, both wrong in the same way."

### 3. Generate Tests from Spec (Separate AI Context/Model)
The test author must NOT see the implementation. Generate:
- Unit tests (core logic)
- Integration tests (module boundaries)
- Negative tests (bad inputs, malformed data)
- Boundary tests (empty, max size, extremes)
- Sequence/state tests (repeated calls, ordering, partial failures)

### 4. Adversarial Red-Team Pass
A separate AI agent assumes the code is broken and tries to prove it:
- Injection attacks
- Logic bypasses
- Malformed inputs, weird encodings
- Concurrency abuse
- Resource exhaustion

### 5. Differential Testing
- Compare output vs oracle/reference/brute-force (Step 2), at least on small/medium cases
- If multiple implementations exist, compare across them
- **One of the highest-value steps available** — catches "plausible but wrong" logic extremely well

### 6. Property-Based Testing + Fuzzing
- Encode invariants from Step 1 into properties
- Run thousands+ randomized cases
- Store failing seeds/inputs — maintain a crash corpus that grows over time

**Why:** Finds bugs nobody thought to write a test for.

### 7. Mutation Testing (The Real Judge)
- Introduce synthetic bugs, verify tests catch them
- Coverage is a *floor* (necessary, not sufficient)
- **Mutation score is the honest measure of test suite quality**
- Set a threshold and treat it as a gate

### 8. Static Analysis
- Type checking + linting
- Security scanning (SAST)
- Taint analysis
- Complexity metrics

### 9. Dynamic Analysis
- Memory sanitizers
- Undefined behavior detectors
- Race condition detectors
- Leak detectors

**Why:** Catches entire classes of bugs that no amount of test cases will find.

### 10. Performance / Load Benchmarking
- Test against realistic AND worst-case data volumes
- Time + memory thresholds
- Load/stress/concurrency tests if applicable

**Why:** AI code often "works on 10 items" but fails at 10,000.

### 11. Dependency Audit
- Verify all packages are real (not hallucinated)
- Check maintained, vulnerability-free, actually necessary
- Typosquat detection
- License checks

**Why:** One of the most AI-specific failure modes.

### 12. Deterministic, Reproducible Harness
- Pinned dependencies
- Fixed random seeds
- Logged inputs on failure
- Resource limits + timeouts
- Runtime sanitizers where available

**Why:** Without this, fuzz and property-based testing can produce non-reproducible failures — almost worse than not testing at all.

### 13. CI Integration with Regression Corpus
- Every bug becomes a permanent test
- Fuzz crashes accumulate
- Nothing runs without the full pipeline passing
- One command/script runs everything

### 14. Human Validation Anchor
- At least one hand-verified golden test case per critical path
- One sanity check that doesn't depend on any AI reasoning at all
- Business-logic security (authz rules, money movement, workflow bypass)
- Regulatory/compliance constraints
- Maintainability/architecture tradeoffs

---

## Why This Works

The strongest parts are **independence + oracles + mechanical checks**:

| Defense | What It Prevents |
|---------|-----------------|
| **Independence** (separate contexts/models, spec-only tests) | Shared blind spots |
| **Oracles/differential** (known-correct reference) | Circular reasoning |
| **Mechanical truth** (mutation testing, analyzers, dependency audits, sandbox) | AI bias |
| **Scale gates** (benchmarks + load) | "Works small, breaks big" |

---

## Role Separation (The "Three Minds" Pattern)

| Role | Job | Context |
|------|-----|---------|
| **Builder AI** | Generates code | Has spec + requirements |
| **Tester AI** | Generates tests from spec only | Fresh context, does NOT see code |
| **Breaker AI** | Tries to make it fail | Fresh context, different model if possible |

This is the main defense against shared blind spots.

---

## How This Maps to wreckit Gates

| wreckit Gate | Framework Steps |
|-------------|----------------|
| AI Slop Scan | 11 (dependency audit) + 8 (static analysis) |
| Type Check | 8 (static analysis — type checking) |
| Ralph Loop | 1 (spec) + 3 (test gen) + iterative fix |
| Test Quality | 3 (layered tests) + 6 (property/fuzz) + 7 (mutation) |
| Mutation Kill | 7 (mutation testing) — the gate |
| Proof Bundle | 13 (CI integration) + all results committed |

**Steps wreckit should add:**
- **Step 2**: Independent oracle / differential testing
- **Step 4**: Adversarial red-team (Breaker AI as separate subagent)
- **Step 5**: Differential testing
- **Step 9**: Dynamic analysis / sanitizers
- **Step 10**: Performance benchmarking gate
- **Step 12**: Deterministic harness enforcement

---

## Honest Assessment

> There is no bulletproof method. But **independent oracles + differential testing + fuzzing + mutation testing + sanitizers** is the practical ceiling, and most teams aren't doing even two of those. Getting all 14 steps running in CI puts you ahead of virtually every production codebase in existence.

### Where we're still weak (both frameworks):
- Maintainability/readability (hard to automate)
- Regulatory compliance (domain-specific)
- Formal verification (theoretical ceiling, impractical for most code)

---

_This framework is the foundation for wreckit's verification pipeline. Each step maps to a gate or subagent task. The goal: make AI-generated code trustworthy enough to ship._


path: scripts/behavior-capture.sh
content: #!/usr/bin/env bash
# wreckit — Behavior Capture gate (REBUILD mode)
# Captures golden fixtures from current implementation before rebuilding
# Usage: ./behavior-capture.sh [project-path] [test-cmd]
# Output: JSON + writes .wreckit/behavior-snapshots/ with captured outputs

set -euo pipefail
PROJECT="${1:-.}"
TEST_CMD="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

STACK_JSON=$("$SCRIPT_DIR/detect-stack.sh" "$PROJECT" 2>/dev/null || true)
LANG=$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('language','unknown'))" 2>/dev/null || echo "unknown")
if [ -z "$TEST_CMD" ]; then
  TEST_CMD=$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commands',{}).get('test','none'))" 2>/dev/null || echo "none")
fi

SNAP_DIR="$PROJECT/.wreckit/behavior-snapshots/$(date +%Y%m%d%H%M%S)"
mkdir -p "$SNAP_DIR"

STATUS="PASS"

if [ -z "$TEST_CMD" ] || [ "$TEST_CMD" = "none" ]; then
  STATUS="WARN"
  TEST_OUTPUT="No test command detected."
else
  TEST_EXIT=0
  TEST_OUTPUT=$(eval "$TEST_CMD" 2>&1) || TEST_EXIT=$?
  if [ "$TEST_EXIT" -ne 0 ]; then
    STATUS="FAIL"
  fi
fi

echo "$TEST_OUTPUT" > "$SNAP_DIR/test-output.txt"

# Capture function signatures
SIGNATURES_FILE="$SNAP_DIR/signatures.txt"
APIS_FILE="$SNAP_DIR/api-shapes.txt"
CLI_FILE="$SNAP_DIR/cli-examples.txt"

case "$LANG" in
  typescript|javascript)
    rg -n "export (async )?function|export const|module\.exports" . --glob '!node_modules/**' --glob '!dist/**' --glob '!\.git/**' | head -200 > "$SIGNATURES_FILE" || true
    rg -n "^(export )?(interface|type|class) " . --glob '!node_modules/**' --glob '!dist/**' --glob '!\.git/**' | head -200 > "$APIS_FILE" || true
    if [ -f package.json ]; then
      rg -n '"bin"|"cli"' package.json | head -200 > "$CLI_FILE" || true
    else
      echo "No package.json found" > "$CLI_FILE"
    fi
    ;;
  python)
    rg -n "^def |^class " . --glob '!venv/**' --glob '!\.git/**' --glob '!__pycache__/**' | head -200 > "$SIGNATURES_FILE" || true
    rg -n "^class " . --glob '!venv/**' --glob '!\.git/**' --glob '!__pycache__/**' | head -200 > "$APIS_FILE" || true
    rg -n "if __name__ == ['\"]__main__['\"]" . --glob '!venv/**' --glob '!\.git/**' | head -200 > "$CLI_FILE" || true
    ;;
  go)
    rg -n "^func " . --glob '!vendor/**' --glob '!\.git/**' | head -200 > "$SIGNATURES_FILE" || true
    rg -n "^type " . --glob '!vendor/**' --glob '!\.git/**' | head -200 > "$APIS_FILE" || true
    rg -n "package main|func main\(" . --glob '!vendor/**' --glob '!\.git/**' | head -200 > "$CLI_FILE" || true
    ;;
  rust)
    rg -n "^pub fn |^fn " . --glob '!target/**' --glob '!\.git/**' | head -200 > "$SIGNATURES_FILE" || true
    rg -n "^pub struct |^struct |^enum " . --glob '!target/**' --glob '!\.git/**' | head -200 > "$APIS_FILE" || true
    rg -n "fn main\(" . --glob '!target/**' --glob '!\.git/**' | head -200 > "$CLI_FILE" || true
    ;;
  shell)
    rg -n "^[a-zA-Z0-9_]+\(\)\s*\{" . --glob '!\.git/**' | head -200 > "$SIGNATURES_FILE" || true
    rg -n "^function " . --glob '!\.git/**' | head -200 > "$APIS_FILE" || true
    rg -n "^#!/usr/bin/env bash" . --glob '!\.git/**' | head -200 > "$CLI_FILE" || true
    ;;
  *)
    echo "Unknown language: $LANG" > "$SIGNATURES_FILE"
    echo "Unknown language: $LANG" > "$APIS_FILE"
    echo "Unknown language: $LANG" > "$CLI_FILE"
    ;;
esac

SNAPSHOTS_CAPTURED=$(ls -1 "$SNAP_DIR" | wc -l | tr -d ' ')

python3 - "$STATUS" "$SNAPSHOTS_CAPTURED" "$SNAP_DIR" <<'PYEOF'
import json, sys
status, count, snap_dir = sys.argv[1:]
print(json.dumps({
  "status": status,
  "snapshots_captured": int(count),
  "snapshot_dir": snap_dir
}))
PYEOF


path: scripts/check-deps.sh
content: #!/usr/bin/env bash
# wreckit — verify declared dependencies exist in registries
# Usage: ./check-deps.sh [project-path]
# Outputs JSON to stdout (status PASS/FAIL), human-readable logs to stderr
# Exit 0 = results produced (check JSON status)

set -euo pipefail
PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"
HALLUCINATED=()

check_npm() {
  local pkg="$1"
  # Skip scoped packages with complex names, builtins
  [[ "$pkg" == node:* ]] && return 0
  local status
  status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$pkg" 2>/dev/null || echo "000")
  if [ "$status" = "404" ]; then
    HALLUCINATED+=("npm:$pkg")
  fi
}

check_pypi() {
  local pkg="$1"
  local status
  status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/$pkg/json" 2>/dev/null || echo "000")
  if [ "$status" = "404" ]; then
    HALLUCINATED+=("pypi:$pkg")
  fi
}

check_crate() {
  local pkg="$1"
  local status
  status=$(curl -s -o /dev/null -w "%{http_code}" "https://crates.io/api/v1/crates/$pkg" 2>/dev/null || echo "000")
  if [ "$status" = "404" ]; then
    HALLUCINATED+=("crate:$pkg")
  fi
}

# npm/yarn
if [ -f "package.json" ]; then
  deps=$(python3 -c "
import json,sys
d=json.load(open('package.json'))
for k in ['dependencies','devDependencies']:
  for p,v in d.get(k,{}).items():
    if not p.startswith('@types/'):
      print(f'{p}\\t{v}')
" 2>/dev/null || true)
  while IFS=$'\t' read -r pkg version; do
    [ -z "$pkg" ] && continue
    if [[ "$version" == npm:* ]] || [[ "$version" == workspace:* ]] || [[ "$version" == file:* ]] || [[ "$version" == link:* ]] || [[ "$version" == github:* ]] || [[ "$version" == git+https:* ]] || [[ "$version" == patch:* ]] || [[ "$version" == portal:* ]]; then
      continue
    fi
    check_npm "$pkg"
  done <<< "$deps"
fi

# Python
if [ -f "requirements.txt" ]; then
  while IFS= read -r line; do
    pkg=$(echo "$line" | sed 's/[>=<!\[].*//;s/#.*//' | tr -d '[:space:]')
    [ -n "$pkg" ] && check_pypi "$pkg"
  done < requirements.txt
elif [ -f "pyproject.toml" ]; then
  deps=$(python3 -c "
try:
  import tomllib
except: import tomli as tomllib
with open('pyproject.toml','rb') as f: d=tomllib.load(f)
for dep in d.get('project',{}).get('dependencies',[]):
  print(dep.split('>')[0].split('<')[0].split('=')[0].split('[')[0].strip())
" 2>/dev/null || true)
  for pkg in $deps; do
    check_pypi "$pkg"
  done
fi

# Swift / SPM
# Note: There is no automated CVE database for Swift Package Manager (unlike npm/PyPI/crates).
# We list dependencies and flag known-vulnerable packages from a small hardcoded list,
# but manual review is always recommended for Swift projects.
if [ -f "Package.swift" ]; then
  echo "Checking Swift Package Manager dependencies..." >&2
  SPM_DEPS=""
  if command -v swift >/dev/null 2>&1; then
    SPM_DEPS=$(swift package show-dependencies --format json 2>/dev/null || echo "")
  fi

  if [ -n "$SPM_DEPS" ] && echo "$SPM_DEPS" | python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1; then
    # Parse and check against known-vulnerable packages
    SPM_DEPS="$SPM_DEPS" python3 - <<'PYEOF'
import json, os, sys

deps_json = json.loads(os.environ["SPM_DEPS"])
known_vulns = {
    # package_url_substring: (description, fixed_version_note)
    "vapor/vapor": ("CVE-2023-vapor: versions < 4.65 have HTTP request smuggling vulnerability", "upgrade to >= 4.65"),
    "apple/swift-nio": ("CVE-2022-nio: versions < 2.42 have potential DoS via malformed frames", "upgrade to >= 2.42"),
    "Alamofire/Alamofire": ("Older versions may have certificate pinning bypass", "use latest"),
}

def flatten_deps(node, depth=0):
    """Recursively flatten the SPM dependency tree."""
    results = []
    if isinstance(node, dict):
        url = node.get("url", node.get("identity", ""))
        version = node.get("version", "unresolved")
        if url:
            results.append({"url": url, "version": str(version), "depth": depth})
        for child in node.get("dependencies", []):
            results.extend(flatten_deps(child, depth + 1))
    elif isinstance(node, list):
        for item in node:
            results.extend(flatten_deps(item, depth))
    return results

all_deps = flatten_deps(deps_json)
warnings = []
for dep in all_deps:
    for vuln_key, (desc, fix) in known_vulns.items():
        if vuln_key.lower() in dep["url"].lower():
            warnings.append({
                "package": dep["url"],
                "version": dep["version"],
                "advisory": desc,
                "fix": fix
            })

print(json.dumps({
    "status": "CAUTION" if warnings else "PASS",
    "confidence": 0.5,  # Low confidence — no automated CVE DB for Swift SPM
    "findings": len(warnings),
    "dependencies": len(all_deps),
    "advisories": warnings,
    "hallucinated": [],
    "note": "No automated CVE database for Swift SPM — manual review recommended. Only a small hardcoded list of known vulnerabilities is checked."
}))
PYEOF
  else
    echo "Could not parse SPM dependencies — swift CLI may not be available" >&2
    python3 - <<'PYEOF'
import json
print(json.dumps({
    "status": "CAUTION",
    "confidence": 0.3,
    "findings": 0,
    "hallucinated": [],
    "note": "Swift Package Manager dependencies could not be enumerated. Manual review recommended."
}))
PYEOF
  fi
  # Swift deps handled — check CocoaPods if present, then exit
  # (don't fall through to npm/pypi/cargo generic checks)
  if [ -f "Podfile" ] && command -v pod >/dev/null 2>&1; then
    echo "Also checking CocoaPods..." >&2
    pod outdated 2>/dev/null | head -20 >&2 || true
  fi
  exit 0
fi

# CocoaPods (Podfile — standalone, without SPM)
if [ -f "Podfile" ]; then
  echo "Checking CocoaPods dependencies..." >&2
  POD_OUTDATED=""
  if command -v pod >/dev/null 2>&1; then
    POD_OUTDATED=$(pod outdated 2>/dev/null || echo "")
  fi
  if [ -n "$POD_OUTDATED" ]; then
    OUTDATED_COUNT=$(echo "$POD_OUTDATED" | grep -c "^-" 2>/dev/null || echo 0)
    echo "  Found $OUTDATED_COUNT outdated pods" >&2
    # We don't fail on outdated pods — just note it
  fi
fi

# Rust
if [ -f "Cargo.toml" ]; then
  deps=$(python3 -c "
try:
  import tomllib
except: import tomli as tomllib
with open('Cargo.toml','rb') as f: d=tomllib.load(f)
for dep in d.get('dependencies',{}):
  print(dep)
" 2>/dev/null || true)
  for pkg in $deps; do
    check_crate "$pkg"
  done
fi

if [ ${#HALLUCINATED[@]} -gt 0 ]; then
  echo "HALLUCINATED DEPENDENCIES FOUND:" >&2
  for h in "${HALLUCINATED[@]}"; do
    echo "  ❌ $h" >&2
  done
  # Serialize the hallucinated list to JSON via a short one-liner, then pass
  # it as an env variable to the reporting script.  This avoids the pipe+heredoc
  # stdin conflict that would cause findings to be under-reported.
  HALLUCINATED_JSON=$(printf '%s\n' "${HALLUCINATED[@]}" | python3 -c "
import json, sys
seen = set()
items = []
for raw in sys.stdin:
    entry = raw.strip()
    if entry and entry not in seen:
        seen.add(entry)
        items.append(entry)
print(json.dumps(items))
")
  # Build the final report: findings MUST always equal len(hallucinated)
  HALLUCINATED_JSON="$HALLUCINATED_JSON" python3 - <<'PYEOF'
import json, os

items = json.loads(os.environ["HALLUCINATED_JSON"])

# Invariant: findings == len(hallucinated) — enforced here, not assumed.
findings = len(items)

print(json.dumps({
    "status": "FAIL",
    "confidence": 1.0,
    "findings": findings,
    "hallucinated": items
}))
PYEOF
else
  echo "All dependencies verified in registries." >&2
  python3 - <<'PYEOF'
import json
print(json.dumps({
    "status": "PASS",
    "confidence": 1.0,
    "findings": 0,
    "hallucinated": []
}))
PYEOF
fi

exit 0


path: scripts/ci-integration.sh
content: #!/usr/bin/env bash
# wreckit — CI config detection, scoring, and generation
# Usage: ./ci-integration.sh [project-path] [--generate]
# Output: JSON to stdout, human summary to stderr
# --generate: write starter GitHub Actions workflow if no CI found
# Exit 0 = results produced, check JSON verdict for pass/fail

set -euo pipefail
PROJECT="${1:-.}"
GENERATE=false
if [ "${2:-}" = "--generate" ]; then
  GENERATE=true
fi
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

echo "=== CI Integration Check ===" >&2
echo "Project: $(pwd)" >&2

# ─── Detect CI configs ────────────────────────────────────────────────────────

CI_FOUND=false
CI_TYPE=""
CI_FILE=""

# GitHub Actions
if ls .github/workflows/*.yml >/dev/null 2>&1 || ls .github/workflows/*.yaml >/dev/null 2>&1; then
  CI_FOUND=true
  CI_TYPE="github-actions"
  CI_FILE=$(ls .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null | head -1 || true)
  echo "Found GitHub Actions: $CI_FILE" >&2
fi

# GitLab CI
if [ "$CI_FOUND" = false ] && [ -f ".gitlab-ci.yml" ]; then
  CI_FOUND=true
  CI_TYPE="gitlab-ci"
  CI_FILE=".gitlab-ci.yml"
  echo "Found GitLab CI: $CI_FILE" >&2
fi

# CircleCI
if [ "$CI_FOUND" = false ] && [ -f ".circleci/config.yml" ]; then
  CI_FOUND=true
  CI_TYPE="circleci"
  CI_FILE=".circleci/config.yml"
  echo "Found CircleCI: $CI_FILE" >&2
fi

# Jenkins
if [ "$CI_FOUND" = false ] && [ -f "Jenkinsfile" ]; then
  CI_FOUND=true
  CI_TYPE="jenkins"
  CI_FILE="Jenkinsfile"
  echo "Found Jenkinsfile" >&2
fi

# Azure Pipelines
if [ "$CI_FOUND" = false ] && [ -f "azure-pipelines.yml" ]; then
  CI_FOUND=true
  CI_TYPE="azure-pipelines"
  CI_FILE="azure-pipelines.yml"
  echo "Found Azure Pipelines: $CI_FILE" >&2
fi

# Travis CI
if [ "$CI_FOUND" = false ] && [ -f ".travis.yml" ]; then
  CI_FOUND=true
  CI_TYPE="travis"
  CI_FILE=".travis.yml"
  echo "Found Travis CI: $CI_FILE" >&2
fi

# ─── Score existing CI config ─────────────────────────────────────────────────

SCORE=0
MISSING_STEPS="[]"
GENERATED_CONFIG_PATH=""

if [ "$CI_FOUND" = true ] && [ -n "$CI_FILE" ] && [ -f "$CI_FILE" ]; then
  echo "Scoring $CI_FILE..." >&2
  missing=""

  if [ "$CI_TYPE" = "github-actions" ]; then
    WORKFLOW_FILES=$(ls .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true)
    if [ -z "$WORKFLOW_FILES" ]; then
      WORKFLOW_COUNT=0
    else
      WORKFLOW_COUNT=$(echo "$WORKFLOW_FILES" | wc -l | tr -d ' ')
    fi

    if [ "$WORKFLOW_COUNT" -gt 0 ]; then
      SCORE=$((SCORE + 30))
      echo "  ✓ Workflows present: +30pts" >&2
    else
      missing="${missing}\"workflows\","
      echo "  ✗ Workflows present: 0pts" >&2
    fi

    JOB_WORKFLOW_COUNT=0
    ON_FOUND=false
    STEP_FOUND=false
    MATRIX_FOUND=false
    OS_UBUNTU=false
    OS_WINDOWS=false
    OS_MACOS=false

    while IFS= read -r wf; do
      [ -z "$wf" ] && continue
      if grep -qiE "^[[:space:]]*jobs:" "$wf" 2>/dev/null; then
        JOB_WORKFLOW_COUNT=$((JOB_WORKFLOW_COUNT + 1))
      fi
      if grep -qiE "(^|[^a-z])(push|pull_request)([^a-z]|$)" "$wf" 2>/dev/null; then
        ON_FOUND=true
      fi
      if grep -qiE "run:.*(test|lint|build)|npm test|npm run test|pytest|go test|cargo test|vitest|jest|mocha|yarn test" "$wf" 2>/dev/null; then
        STEP_FOUND=true
      fi
      if grep -qiE "matrix:" "$wf" 2>/dev/null; then
        MATRIX_FOUND=true
      fi
      if grep -qiE "runs-on:.*ubuntu" "$wf" 2>/dev/null; then
        OS_UBUNTU=true
      fi
      if grep -qiE "runs-on:.*windows" "$wf" 2>/dev/null; then
        OS_WINDOWS=true
      fi
      if grep -qiE "runs-on:.*macos" "$wf" 2>/dev/null; then
        OS_MACOS=true
      fi
    done <<< "$WORKFLOW_FILES"

    if [ "$JOB_WORKFLOW_COUNT" -gt 0 ]; then
      SCORE=$((SCORE + 15 * JOB_WORKFLOW_COUNT))
      echo "  ✓ Workflows with jobs: +$((15 * JOB_WORKFLOW_COUNT))pts" >&2
    else
      missing="${missing}\"jobs\","
      echo "  ✗ Workflows with jobs: 0pts" >&2
    fi

    if [ "$ON_FOUND" = true ]; then
      SCORE=$((SCORE + 20))
      echo "  ✓ Runs on push/PR: +20pts" >&2
    else
      missing="${missing}\"on-push-pr\","
      echo "  ✗ Runs on push/PR: 0pts" >&2
    fi

    if [ "$STEP_FOUND" = true ]; then
      SCORE=$((SCORE + 15))
      echo "  ✓ Test/lint/build step: +15pts" >&2
    else
      missing="${missing}\"test-lint-build\","
      echo "  ✗ Test/lint/build step: 0pts" >&2
    fi

    OS_COUNT=0
    [ "$OS_UBUNTU" = true ] && OS_COUNT=$((OS_COUNT + 1))
    [ "$OS_WINDOWS" = true ] && OS_COUNT=$((OS_COUNT + 1))
    [ "$OS_MACOS" = true ] && OS_COUNT=$((OS_COUNT + 1))
    if [ "$MATRIX_FOUND" = true ] || [ "$OS_COUNT" -ge 2 ]; then
      SCORE=$((SCORE + 20))
      echo "  ✓ Matrix or multi-OS: +20pts" >&2
    else
      missing="${missing}\"matrix-or-multi-os\","
      echo "  ✗ Matrix or multi-OS: 0pts" >&2
    fi
  else
    SCORE=$((SCORE + 30))
    echo "  ✓ CI config present: +30pts" >&2

    if [ -s "$CI_FILE" ]; then
      SCORE=$((SCORE + 15))
      echo "  ✓ CI config non-empty: +15pts" >&2
    else
      missing="${missing}\"empty-config\","
      echo "  ✗ CI config non-empty: 0pts" >&2
    fi

    if grep -qiE "(test|lint|build)" "$CI_FILE" 2>/dev/null; then
      SCORE=$((SCORE + 15))
      echo "  ✓ Test/lint/build step: +15pts" >&2
    else
      missing="${missing}\"test-lint-build\","
      echo "  ✗ Test/lint/build step: 0pts" >&2
    fi

    if grep -qiE "(push|pull_request|merge_request)" "$CI_FILE" 2>/dev/null; then
      SCORE=$((SCORE + 20))
      echo "  ✓ Triggers present: +20pts" >&2
    else
      missing="${missing}\"triggers\","
      echo "  ✗ Triggers present: 0pts" >&2
    fi
  fi

  if [ "$SCORE" -gt 100 ]; then
    SCORE=100
  fi

  if [ -n "$missing" ]; then
    MISSING_STEPS="[${missing%,}]"
  fi

  echo "  Total score: $SCORE/100" >&2

fi

# ─── Generate CI config if not found ─────────────────────────────────────────

if [ "$CI_FOUND" = false ]; then
  echo "" >&2
  echo "No CI configuration found." >&2

  # Detect project type for config generation
  SETUP_STEP="- uses: actions/setup-node@v4\n        with: { node-version: '20' }"
  INSTALL_CMD="npm install"
  TYPECHECK_CMD="npx tsc --noEmit"
  TEST_CMD="npm test"

  if [ -f "go.mod" ]; then
    SETUP_STEP="- uses: actions/setup-go@v5\n        with: { go-version: '1.22' }"
    INSTALL_CMD="go mod download"
    TYPECHECK_CMD="go vet ./..."
    TEST_CMD="go test ./..."
  elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
    SETUP_STEP="- uses: actions/setup-python@v5\n        with: { python-version: '3.12' }"
    INSTALL_CMD="pip install -r requirements.txt"
    TYPECHECK_CMD="python -m mypy . || true"
    TEST_CMD="python -m pytest"
  fi

  GENERATED_CONFIG_PATH=".github/workflows/wreckit-audit.yml"

  if [ "$GENERATE" = true ]; then
    mkdir -p ".github/workflows"
    cat > "$GENERATED_CONFIG_PATH" <<YAMLEOF
# Generated by wreckit ci-integration.sh
name: wreckit audit
on: [push, pull_request]
jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - ${SETUP_STEP}
      - run: ${INSTALL_CMD}
      - run: ${TYPECHECK_CMD}
      - run: ${TEST_CMD}
YAMLEOF
    echo "Generated: $GENERATED_CONFIG_PATH" >&2
  else
    echo "Run with --generate flag to create $GENERATED_CONFIG_PATH" >&2
    GENERATED_CONFIG_PATH=""
  fi
fi

# ─── Determine verdict ────────────────────────────────────────────────────────

VERDICT="FAIL"
SUMMARY=""
CONFIDENCE="0.0"

if [ "$CI_FOUND" = false ]; then
  VERDICT="FAIL"
  SUMMARY="FAIL: No CI configuration found in project."
  CONFIDENCE="1.0"
elif [ "$SCORE" -ge 70 ]; then
  VERDICT="PASS"
  SUMMARY="PASS: CI found (${CI_TYPE}), score ${SCORE}/100."
elif [ "$SCORE" -ge 40 ]; then
  VERDICT="WARN"
  SUMMARY="WARN: CI found (${CI_TYPE}) but incomplete, score ${SCORE}/100."
  CONFIDENCE="0.5"
else
  VERDICT="FAIL"
  SUMMARY="FAIL: CI found (${CI_TYPE}) but critically incomplete, score ${SCORE}/100."
  CONFIDENCE="0.5"
fi

# ─── Output ───────────────────────────────────────────────────────────────────

echo "" >&2
echo "CI found: $CI_FOUND" >&2
echo "CI type: ${CI_TYPE:-none}" >&2
echo "Score: $SCORE/100" >&2
echo "Verdict: $VERDICT" >&2
echo "Summary: $SUMMARY" >&2

CI_FILE_JSON="null"
if [ -n "$CI_FILE" ]; then
  CI_FILE_JSON="\"$CI_FILE\""
fi

GENERATED_JSON="null"
if [ -n "$GENERATED_CONFIG_PATH" ]; then
  GENERATED_JSON="\"$GENERATED_CONFIG_PATH\""
fi

cat <<EOF
{
  "ci_found": $CI_FOUND,
  "ci_type": "${CI_TYPE:-none}",
  "ci_file": $CI_FILE_JSON,
  "score_100": $SCORE,
  "missing_steps": $MISSING_STEPS,
  "generated_config_path": $GENERATED_JSON,
  "status": "$VERDICT",
  "verdict": "$VERDICT",
  "summary": "$SUMMARY",
  "confidence": $CONFIDENCE
}
EOF


path: scripts/coverage-stats.sh
content: #!/usr/bin/env bash
# wreckit — extract raw coverage stats from test runners
# Usage: ./coverage-stats.sh [project-path]
# Outputs JSON with coverage numbers
# Fix: use absolute script path for detect-stack (works from any cwd)

set -euo pipefail
PROJECT="${1:-.}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

# Detect stack
STACK=$(bash "$SCRIPT_DIR/detect-stack.sh" "$PROJECT" 2>/dev/null)
LANG=$(echo "$STACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('language','unknown'))" 2>/dev/null || echo "unknown")
TEST_RUNNER=$(echo "$STACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('testRunner','none'))" 2>/dev/null || echo "none")

case "$TEST_RUNNER" in
  vitest)
    # vitest writes coverage to coverage/coverage-summary.json (not stdout)
    npx vitest run --coverage --coverage.reporter=json-summary >/dev/null 2>&1 || true
    if [ -f "coverage/coverage-summary.json" ]; then
      cat coverage/coverage-summary.json
    elif [ -f "coverage/coverage-final.json" ]; then
      # Summarize from final coverage file
      python3 -c "
import json
with open('coverage/coverage-final.json') as f:
    data = json.load(f)
total_stmts = sum(len(v.get('s',{})) for v in data.values())
covered = sum(1 for v in data.values() for c in v.get('s',{}).values() if c > 0)
pct = round(covered * 100 / total_stmts, 1) if total_stmts > 0 else 0
print(json.dumps({'total': {'statements': {'pct': pct, 'total': total_stmts, 'covered': covered}}}))
" 2>/dev/null || echo '{"error":"could not parse vitest coverage"}'
    else
      echo '{"error":"vitest coverage file not found — ensure @vitest/coverage-v8 or @vitest/coverage-istanbul is installed"}'
    fi
    ;;
  jest)
    npx jest --coverage --coverageReporters=json-summary >/dev/null 2>&1
    if [ -f "coverage/coverage-summary.json" ]; then
      cat coverage/coverage-summary.json
    else
      echo '{"error":"jest coverage file not found"}'
    fi
    ;;
  pytest)
    pytest --cov=. --cov-report=json >/dev/null 2>&1
    if [ -f "coverage.json" ]; then
      cat coverage.json
    else
      echo '{"error":"pytest coverage file not found"}'
    fi
    ;;
  cargo)
    # cargo-tarpaulin for Rust coverage
    if command -v cargo-tarpaulin &>/dev/null; then
      cargo tarpaulin --out Json 2>/dev/null || echo '{"error":"tarpaulin failed"}'
    else
      echo '{"error":"cargo-tarpaulin not installed","hint":"cargo install cargo-tarpaulin"}'
    fi
    ;;
  go)
    go test -coverprofile=coverage.out ./... >/dev/null 2>&1
    if [ -f "coverage.out" ]; then
      go tool cover -func=coverage.out | tail -1
      rm -f coverage.out
    else
      echo '{"error":"go coverage failed"}'
    fi
    ;;
  swift)
    swift test --enable-code-coverage 2>/dev/null || echo '{"error":"swift test coverage failed"}'
    ;;
  bash|shell)
    # Shell projects — no standard coverage tool; report test pass/fail only
    TEST_SCRIPT=$(echo "$STACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('commands',{}).get('test',''))" 2>/dev/null || echo "")
    if [ -n "$TEST_SCRIPT" ]; then
      if bash -c "$TEST_SCRIPT" > /dev/null 2>&1; then
        echo '{"pct":null,"language":"shell","note":"Shell project — no line coverage tool. Tests pass.","test_pass":true}'
      else
        echo '{"pct":null,"language":"shell","note":"Shell project — no line coverage tool. Tests FAIL.","test_pass":false}'
      fi
    else
      echo '{"pct":null,"language":"shell","note":"Shell project — no line coverage tool, no test command detected."}'
    fi
    ;;
  *)
    echo "{\"error\":\"unknown test runner: $TEST_RUNNER\",\"language\":\"$LANG\"}"
    ;;
esac


path: scripts/cross-verify.sh
content: #!/usr/bin/env bash
# wreckit — Cross-Verify scaffold (BUILD mode only)
# Sets up environment for AI-driven independent regeneration
# Usage: ./cross-verify.sh [project-path] [original-impl-path] [regenerated-impl-path]
# Without regenerated path: prints the AI prompt template to use for regeneration
# With regenerated path: diffs the two implementations structurally

set -euo pipefail
PROJECT="${1:-.}"
ORIGINAL="${2:-}"
REGENERATED="${3:-}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

TEMP_DIR="$(mktemp -d "/tmp/wreckit-cross-verify.XXXXXX")"
mkdir -p "$TEMP_DIR"

if [ -d "tests" ]; then
  cp -R "tests" "$TEMP_DIR/"
fi
if [ -d "test" ]; then
  cp -R "test" "$TEMP_DIR/"
fi

PRD_CANDIDATE=""
for candidate in "PRD.md" "docs/PRD.md" "spec.md" "SPEC.md" "requirements.md" "docs/spec.md" "docs/requirements.md"; do
  if [ -f "$candidate" ]; then
    PRD_CANDIDATE="$candidate"
    break
  fi
done

if [ -n "$PRD_CANDIDATE" ]; then
  cp "$PRD_CANDIDATE" "$TEMP_DIR/"
fi

echo "Cross-verify workspace: $TEMP_DIR"

if [ -z "$REGENERATED" ]; then
  echo "=== Cross-Verify AI Prompt Template ==="
  cat <<PROMPT
You are an independent regenerator for a wreckit BUILD.

You have ONLY: the tests and PRD/spec in:
$TEMP_DIR

Instructions:
- Do NOT look at the original implementation.
- Regenerate the implementation from scratch.
- Match the external API expected by the tests.
- Return the regenerated implementation in a clean, minimal form.

Output:
- Provide the regenerated source code and any required project files.
PROMPT
  exit 0
fi

if [ -z "$ORIGINAL" ]; then
  echo "{\"status\":\"WARN\",\"symbol_match\":false,\"line_delta\":0,\"missing_in_regen\":[],\"extra_in_regen\":[],\"message\":\"Original implementation path required.\"}"
  exit 0
fi

if [ ! -f "$ORIGINAL" ] || [ ! -f "$REGENERATED" ]; then
  echo "{\"status\":\"WARN\",\"symbol_match\":false,\"line_delta\":0,\"missing_in_regen\":[],\"extra_in_regen\":[],\"message\":\"One or both implementation files not found.\"}"
  exit 0
fi

LINE_ORIG=$(wc -l < "$ORIGINAL" | tr -d ' ')
LINE_REGEN=$(wc -l < "$REGENERATED" | tr -d ' ')
if [ "$LINE_ORIG" -ge "$LINE_REGEN" ]; then
  LINE_DELTA=$((LINE_ORIG - LINE_REGEN))
else
  LINE_DELTA=$((LINE_REGEN - LINE_ORIG))
fi

SYMBOLS_JSON=$(python3 - "$ORIGINAL" "$REGENERATED" <<'PYEOF'
import json, re, sys

orig_path, regen_path = sys.argv[1:]

export_re = re.compile(r"^\\s*export\\s+(?:async\\s+)?(function|class|const|let)\\s+([A-Za-z_][A-Za-z0-9_]*)")
export_list_re = re.compile(r"^\\s*export\\s*\\{([^}]*)\\}")

def extract_symbols(path):
    symbols = set()
    try:
        with open(path, "r", errors="ignore") as f:
            for line in f:
                m = export_re.search(line)
                if m:
                    symbols.add(m.group(2))
                    continue
                m = export_list_re.search(line)
                if m:
                    parts = m.group(1).split(",")
                    for part in parts:
                        name = part.strip().split(" as ")[0].strip()
                        if name:
                            symbols.add(name)
    except FileNotFoundError:
        return set()
    return symbols

orig_symbols = extract_symbols(orig_path)
regen_symbols = extract_symbols(regen_path)

missing = sorted([s for s in orig_symbols if s not in regen_symbols])
extra = sorted([s for s in regen_symbols if s not in orig_symbols])

print(json.dumps({
    "orig": sorted(orig_symbols),
    "regen": sorted(regen_symbols),
    "missing": missing,
    "extra": extra,
}))
PYEOF
)

SYMBOL_MATCH=$(echo "$SYMBOLS_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print('true' if not d['missing'] else 'false')")
MISSING=$(echo "$SYMBOLS_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['missing']))")
EXTRA=$(echo "$SYMBOLS_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['extra']))")

STATUS="PASS"
if [ "$SYMBOL_MATCH" != "true" ]; then
  STATUS="FAIL"
elif [ "$EXTRA" != "[]" ]; then
  STATUS="WARN"
fi

python3 - "$STATUS" "$SYMBOL_MATCH" "$LINE_DELTA" "$MISSING" "$EXTRA" <<'PYEOF'
import json, sys
status = sys.argv[1]
symbol_match = True if sys.argv[2] == "true" else False
line_delta = int(sys.argv[3])
missing = json.loads(sys.argv[4])
extra = json.loads(sys.argv[5])

print(json.dumps({
  "status": status,
  "symbol_match": symbol_match,
  "line_delta": line_delta,
  "missing_in_regen": missing,
  "extra_in_regen": extra
}))
PYEOF


path: scripts/design-review.sh
content: #!/usr/bin/env bash
# wreckit — dependency graph analysis, coupling metrics, circular dep detection
# Usage: ./design-review.sh [project-path]
# Output: JSON to stdout, human summary to stderr
# Fix: avoid duplicate zero output when counting circular deps
# Exit 0 = results produced, check JSON verdict for pass/fail
# v2: package-boundary awareness for library and monorepo project types

set -euo pipefail
PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

echo "=== Design Review ===" >&2
echo "Project: $(pwd)" >&2

# ─── Detect language ───────────────────────────────────────────────────────────
LANGUAGE="unknown"
if [ -f "package.json" ]; then
  LANGUAGE="ts"
elif [ -f "Cargo.toml" ]; then
  LANGUAGE="rust"
elif [ -f "go.mod" ]; then
  LANGUAGE="go"
elif [ -f "requirements.txt" ] || [ -f "setup.py" ] || [ -f "pyproject.toml" ]; then
  LANGUAGE="python"
elif find . -name '*.sh' -not -path '*/.git/*' -maxdepth 3 2>/dev/null | head -1 | grep -q .; then
  LANGUAGE="shell"
fi
echo "Language: $LANGUAGE" >&2

# ─── Package-boundary awareness (library / monorepo) ──────────────────────────
# Read project type from env (set by run-all-gates.sh or caller)
WRECKIT_PROJECT_TYPE="${WRECKIT_PROJECT_TYPE:-unknown}"
echo "Project type (env): $WRECKIT_PROJECT_TYPE" >&2

# Detect monorepo structure: multiple package.json / Cargo.toml / pyproject.toml at depth 1-2
IS_MONOREPO=0
MONOREPO_PACKAGE_DIRS=""
if [ "$WRECKIT_PROJECT_TYPE" = "monorepo" ]; then
  IS_MONOREPO=1
  echo "Monorepo mode: scoring packages independently" >&2
  # Collect sub-package roots (dirs that have their own package manifest)
  MONOREPO_PACKAGE_DIRS=$(find . -mindepth 2 -maxdepth 3 \
    \( -name 'package.json' -o -name 'Cargo.toml' -o -name 'pyproject.toml' \) \
    -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' \
    2>/dev/null | xargs -I{} dirname {} | sort -u || true)
  PKG_COUNT=$(echo "$MONOREPO_PACKAGE_DIRS" | grep -c '.' 2>/dev/null || echo 0)
  echo "Monorepo sub-packages found: $PKG_COUNT" >&2
else
  # Auto-detect monorepo even if not explicitly tagged
  PKG_MANIFESTS=$(find . -mindepth 2 -maxdepth 3 \
    \( -name 'package.json' -o -name 'Cargo.toml' \) \
    -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | wc -l | tr -d ' ')
  if [ "${PKG_MANIFESTS:-0}" -ge 3 ]; then
    IS_MONOREPO=1
    echo "Auto-detected monorepo structure ($PKG_MANIFESTS sub-manifests)" >&2
    MONOREPO_PACKAGE_DIRS=$(find . -mindepth 2 -maxdepth 3 \
      \( -name 'package.json' -o -name 'Cargo.toml' \) \
      -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | xargs -I{} dirname {} | sort -u || true)
  fi
fi

# ─── Project-type calibration hooks (global driver via run-all-gates.sh) ──────
GOD_FAIL_THRESHOLD="${WRECKIT_GOD_MODULE_FANIN:-10}"
if ! [[ "$GOD_FAIL_THRESHOLD" =~ ^[0-9]+$ ]] || [ "$GOD_FAIL_THRESHOLD" -lt 1 ]; then
  GOD_FAIL_THRESHOLD=10
fi

# Library mode: relax god module thresholds
# Libraries legitimately have high-fan-in utility modules (many callers is expected)
# and many "orphan" exports that are part of the public API, not dead code
ORPHAN_IS_EXPECTED=0
LIBRARY_MODE_NOTES=""
if [ "$WRECKIT_PROJECT_TYPE" = "library" ] || [ "$WRECKIT_PROJECT_TYPE" = "npm-package" ] || [ "$WRECKIT_PROJECT_TYPE" = "crate" ]; then
  # Relax god module fan-in threshold: libraries often have intentional high-fan-in utils
  GOD_FAIL_THRESHOLD=$((GOD_FAIL_THRESHOLD + 5))
  echo "Library mode: relaxed god_module threshold to $GOD_FAIL_THRESHOLD (was $((GOD_FAIL_THRESHOLD - 5)))" >&2
  # Orphan files in libraries = public API exports (not dead code)
  ORPHAN_IS_EXPECTED=1
  LIBRARY_MODE_NOTES="Library project: orphan files are expected (public API exports). God module threshold relaxed."
fi

GOD_WARN_THRESHOLD=$((GOD_FAIL_THRESHOLD - 2))
if [ "$GOD_WARN_THRESHOLD" -lt 1 ]; then
  GOD_WARN_THRESHOLD=1
fi

# ─── JS/TS: try madge first ────────────────────────────────────────────────────
MADGE_OUTPUT=""
USED_MADGE=false

if [ "$LANGUAGE" = "ts" ] && command -v npx >/dev/null 2>&1; then
  echo "Attempting madge analysis..." >&2
  if MADGE_OUTPUT=$(npx --yes madge --circular --json . 2>/dev/null); then
    USED_MADGE=true
    echo "madge succeeded" >&2
  else
    echo "madge failed or not available, using manual scan" >&2
  fi
fi

# ─── Build dependency map (manual fallback or non-JS) ─────────────────────────

# Collect source files
SRC_FILES=""
case "$LANGUAGE" in
  ts)
    SRC_FILES=$(find . \( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' \) \
      -not -name '*.test.*' -not -name '*.spec.*' -not -name '*.d.ts' \
      -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' \
      -not -path '*/build/*' -not -path '*/coverage/*' 2>/dev/null || true)
    ;;
  python)
    SRC_FILES=$(find . -name '*.py' \
      -not -name 'test_*' -not -name '*_test.py' \
      -not -path '*/.git/*' -not -path '*/venv/*' -not -path '*/__pycache__/*' \
      -not -path '*/site-packages/*' 2>/dev/null || true)
    ;;
  go)
    SRC_FILES=$(find . -name '*.go' \
      -not -name '*_test.go' -not -path '*/.git/*' -not -path '*/vendor/*' 2>/dev/null || true)
    ;;
  rust)
    SRC_FILES=$(find . -name '*.rs' \
      -not -path '*/target/*' -not -path '*/.git/*' 2>/dev/null || true)
    ;;
  shell)
    SRC_FILES=$(find . -name '*.sh' \
      -not -name 'run_tests.sh' -not -path '*/.git/*' 2>/dev/null || true)
    ;;
  *)
    # Generic: try common source extensions
    SRC_FILES=$(find . \( -name '*.ts' -o -name '*.js' -o -name '*.py' -o -name '*.go' \) \
      -not -path '*/.git/*' -not -path '*/node_modules/*' 2>/dev/null || true)
    ;;
esac

TOTAL_FILES=$(echo "$SRC_FILES" | grep -c '.' 2>/dev/null || true)
TOTAL_FILES="${TOTAL_FILES:-0}"
echo "Source files found: $TOTAL_FILES" >&2

# ─── Extract imports and build dep map ────────────────────────────────────────

DEPMAP_FILE=$(mktemp)
FANIN_FILE=$(mktemp)
FANOUT_FILE=$(mktemp)

# For each file, extract what it imports
for file in $SRC_FILES; do
  normalized="${file#./}"
  imports=""
  case "$LANGUAGE" in
    ts)
      # Match: import ... from './x' or require('../y')
      imports=$(grep -oE "(import|require)[^'\"]*['\"](\./|\.\./)([^'\"]+)['\"]" "$file" 2>/dev/null \
        | grep -oE "['\"](\./|\.\./)([^'\"]+)['\"]" \
        | tr -d "'\"" || true)
      ;;
    python)
      # Match: from .module import or import .module
      imports=$(grep -oE "^(from [a-zA-Z0-9_.]+|import [a-zA-Z0-9_.,\ ]+)" "$file" 2>/dev/null \
        | grep -v "^from [a-zA-Z]" | grep -v "^import [a-zA-Z]" \
        | grep -oE "[a-zA-Z0-9_]+" | head -20 || true)
      # Also catch absolute same-package imports
      pkg_imports=$(grep -oE "^from [a-zA-Z0-9_]+" "$file" 2>/dev/null \
        | awk '{print $2}' || true)
      imports="${imports} ${pkg_imports}"
      ;;
    go)
      imports=$(grep -oE '"[^"]+/[^"]+"' "$file" 2>/dev/null \
        | tr -d '"' | grep -v '^\.' || true)
      ;;
  esac

  import_count=$(echo "$imports" | grep -c '[a-zA-Z]' 2>/dev/null || echo 0)
  echo "${normalized}:${import_count}" >> "$FANOUT_FILE"

  # Record each dependency
  for dep in $imports; do
    dep_path="$dep"
    if [ "$LANGUAGE" = "ts" ]; then
      file_dir=$(dirname "$normalized")
      # Resolve relative imports to real files when possible
      candidate="$file_dir/${dep_path}"
      candidate="${candidate#./}"
      resolved=""
      if [ -f "$candidate" ]; then
        resolved="$candidate"
      else
        # try extension mapping
        base="${candidate%.*}"
        for ext in ts tsx js jsx mjs cjs; do
          if [ -f "${base}.${ext}" ]; then resolved="${base}.${ext}"; break; fi
          if [ -f "${candidate}.${ext}" ]; then resolved="${candidate}.${ext}"; break; fi
        done
        if [ -z "$resolved" ] && [ -d "$candidate" ]; then
          for ext in ts tsx js jsx; do
            if [ -f "$candidate/index.${ext}" ]; then resolved="$candidate/index.${ext}"; break; fi
          done
        fi
      fi
      if [ -n "$resolved" ]; then
        dep_path="${resolved}"
      else
        dep_path="${candidate}"
      fi
    fi
    dep_path="${dep_path#./}"
    echo "${dep_path} <- ${normalized}" >> "$DEPMAP_FILE"
  done
done

# Compute fan-in: count how many files import each file
echo "" >> "$DEPMAP_FILE"  # ensure file exists and is non-empty for sort
FANIN_DATA=$(sort "$DEPMAP_FILE" | uniq -c | sort -rn 2>/dev/null || true)

# ─── Detect circular dependencies (DFS via bash) ──────────────────────────────

detect_cycles() {
  # Simple cycle detection: look for any A->B->A pattern in dep map
  # (full DFS would require more complexity; this catches direct + 2-hop cycles)
  local cycles=()

  while IFS= read -r line; do
    # line format: "target <- source"
    target=$(echo "$line" | awk '{print $1}')
    source=$(echo "$line" | awk '{print $3}')
    [ -z "$target" ] || [ -z "$source" ] && continue

    # Check if target also imports source (direct cycle)
    if grep -q "^${source} <- ${target}$" "$DEPMAP_FILE" 2>/dev/null; then
      cycles+=("${source} <-> ${target}")
    fi
  done < "$DEPMAP_FILE"

  # Deduplicate
  printf '%s\n' "${cycles[@]}" | sort -u 2>/dev/null || true
}

CIRCULAR_DEPS=""
if [ "$USED_MADGE" = true ] && [ -n "$MADGE_OUTPUT" ]; then
  # Parse madge JSON output for circular deps
  # madge outputs array of arrays: [["a","b"],["b","a"]]
  CIRCULAR_DEPS=$(echo "$MADGE_OUTPUT" | grep -o '"[^"]*"' | tr -d '"' | paste -d' ' - - 2>/dev/null || true)
else
  CIRCULAR_DEPS=$(detect_cycles 2>/dev/null || true)
fi

CIRCULAR_COUNT=$(echo "$CIRCULAR_DEPS" | grep -c '[a-zA-Z]' 2>/dev/null || true)
CIRCULAR_COUNT=${CIRCULAR_COUNT:-0}

# ─── Identify god modules (high fan-in) ───────────────────────────────────────

GOD_MODULES_JSON="[]"
GOD_COUNT=0

if [ -s "$DEPMAP_FILE" ]; then
  # Count fan-in per target
  GOD_RAW=$(sort "$DEPMAP_FILE" | grep ' <- ' | awk '{print $1}' | sort | uniq -c | sort -rn | head -20 || true)

  god_list=""
  while IFS= read -r entry; do
    [ -z "$entry" ] && continue
    count=$(echo "$entry" | awk '{print $1}')
    fname=$(echo "$entry" | awk '{print $2}')
    if [ "$count" -gt "$GOD_WARN_THRESHOLD" ] 2>/dev/null; then
      god_list="${god_list}{\"file\":\"${fname}\",\"fan_in\":${count}},"
      GOD_COUNT=$((GOD_COUNT + 1))
    fi
  done <<< "$GOD_RAW"

  if [ -n "$god_list" ]; then
    GOD_MODULES_JSON="[${god_list%,}]"
  fi
fi

# ─── Orphan files (not imported by anything) ──────────────────────────────────

ORPHAN_FILES_JSON="[]"
ORPHAN_COUNT=0

if [ -s "$DEPMAP_FILE" ] && [ "$TOTAL_FILES" -gt 0 ]; then
  orphan_list=""
  for file in $SRC_FILES; do
    normalized="${file#./}"
    basename_no_ext="${normalized%.*}"
    # Check if any line in depmap references this file
    if ! grep -q "$normalized\|$basename_no_ext" "$DEPMAP_FILE" 2>/dev/null; then
      orphan_list="${orphan_list}\"${normalized}\","
      ORPHAN_COUNT=$((ORPHAN_COUNT + 1))
    fi
  done
  if [ -n "$orphan_list" ]; then
    ORPHAN_FILES_JSON="[${orphan_list%,}]"
  fi
fi

# ─── Compute averages ──────────────────────────────────────────────────────────

AVG_FANIN="0"
AVG_FANOUT="0"

if [ "$TOTAL_FILES" -gt 0 ] && command -v bc >/dev/null 2>&1; then
  TOTAL_IMPORTS=$(grep -c ' <- ' "$DEPMAP_FILE" 2>/dev/null || true)
  TOTAL_IMPORTS="${TOTAL_IMPORTS:-0}"
  AVG_FANIN=$(echo "scale=1; ${TOTAL_IMPORTS} / ${TOTAL_FILES}" | bc 2>/dev/null || echo "0")
  AVG_FANIN="${AVG_FANIN:-0}"
  if [[ "$AVG_FANIN" == .* ]]; then
    AVG_FANIN="0${AVG_FANIN}"
  fi
  AVG_FANOUT="$AVG_FANIN"
  if [[ "$AVG_FANOUT" == .* ]]; then
    AVG_FANOUT="0${AVG_FANOUT}"
  fi
fi

# ─── Determine verdict ────────────────────────────────────────────────────────

VERDICT="PASS"
SUMMARY=""
CONFIDENCE="0.0"

# ─── Verdict: circular deps are always a hard fail ───────────────────────────
if [ "$CIRCULAR_COUNT" -gt 0 ]; then
  VERDICT="FAIL"
  SUMMARY="FAIL: ${CIRCULAR_COUNT} circular dependency/dependencies detected."
  CONFIDENCE="0.6"
elif [ "$GOD_COUNT" -gt 0 ]; then
  # Check if any god module has fan-in above calibrated fail threshold
  # Library mode: threshold already relaxed (+5) earlier in the script
  HIGH_GOD=$(echo "$GOD_MODULES_JSON" | grep -o '"fan_in":[0-9]*' | awk -F: -v fail_t="$GOD_FAIL_THRESHOLD" '{if($2>fail_t) print $2}' | head -1 || true)
  if [ -n "$HIGH_GOD" ]; then
    if [ "$ORPHAN_IS_EXPECTED" -eq 1 ]; then
      # Library: high fan-in is a design choice, warn but don't fail
      VERDICT="WARN"
      SUMMARY="WARN: ${GOD_COUNT} god module(s) with fan-in > ${GOD_FAIL_THRESHOLD}. Library project — high fan-in expected for shared utilities."
    else
      VERDICT="FAIL"
      SUMMARY="FAIL: God module(s) with fan-in > ${GOD_FAIL_THRESHOLD} detected."
    fi
    CONFIDENCE="0.6"
  else
    VERDICT="WARN"
    SUMMARY="WARN: No circular deps but ${GOD_COUNT} god module(s) with fan-in > ${GOD_WARN_THRESHOLD}."
    CONFIDENCE="0.6"
  fi
else
  SUMMARY="PASS: No circular deps, no god modules. Clean architecture."
fi

# ─── Orphan file scoring (library/monorepo aware) ────────────────────────────
ORPHAN_NOTES=""
if [ "$ORPHAN_COUNT" -gt 0 ]; then
  if [ "$ORPHAN_IS_EXPECTED" -eq 1 ]; then
    # Library: orphan files = public API exports — not dead code
    ORPHAN_NOTES="Library project: $ORPHAN_COUNT orphan file(s) are likely public API exports, not dead code."
    echo "Library mode: orphan files treated as public API exports (not penalized)" >&2
  elif [ "$IS_MONOREPO" -eq 1 ]; then
    # Monorepo: orphans may be cross-package entry points; apply per-package scoring
    ORPHAN_NOTES="Monorepo: $ORPHAN_COUNT apparent orphan(s) — may be inter-package entry points. Per-package scoring recommended."
    echo "Monorepo mode: orphan files may be inter-package entry points (not penalized at repo level)" >&2
  else
    ORPHAN_PCT=0
    if [ "$TOTAL_FILES" -gt 0 ] && command -v bc >/dev/null 2>&1; then
      ORPHAN_PCT=$(echo "scale=0; $ORPHAN_COUNT * 100 / $TOTAL_FILES" | bc 2>/dev/null || echo 0)
    fi
    # Only flag orphans if there are many of them (>3 files AND >15% of total)
    # Small projects with 1-3 entry points should not be penalized
    if [ "${ORPHAN_PCT:-0}" -gt 15 ] && [ "$ORPHAN_COUNT" -gt 3 ] && [ "$VERDICT" = "PASS" ]; then
      VERDICT="WARN"
      SUMMARY="WARN: ${ORPHAN_COUNT} orphan files (${ORPHAN_PCT}% of total) may indicate dead code."
      CONFIDENCE="0.5"
    fi
  fi
fi

# ─── Monorepo: append per-package note ────────────────────────────────────────
MONOREPO_NOTE=""
if [ "$IS_MONOREPO" -eq 1 ]; then
  PKG_LIST_SHORT=$(echo "$MONOREPO_PACKAGE_DIRS" | head -5 | tr '\n' ',' | sed 's/,$//' || true)
  MONOREPO_NOTE="Monorepo detected. Packages scored at repo level; for per-package scoring run design-review.sh on each sub-package."
  echo "Monorepo packages (sample): $PKG_LIST_SHORT" >&2
fi

# Append library/monorepo notes to summary
if [ -n "$LIBRARY_MODE_NOTES" ] && [ "$VERDICT" != "FAIL" ]; then
  SUMMARY="${SUMMARY} [${LIBRARY_MODE_NOTES}]"
fi
if [ -n "$ORPHAN_NOTES" ]; then
  SUMMARY="${SUMMARY} [${ORPHAN_NOTES}]"
fi

# ─── Output ───────────────────────────────────────────────────────────────────

echo "" >&2
echo "Circular deps: $CIRCULAR_COUNT" >&2
echo "God modules (fan-in > ${GOD_WARN_THRESHOLD}): $GOD_COUNT" >&2
echo "Orphan files: $ORPHAN_COUNT" >&2
if [ -n "$MONOREPO_NOTE" ]; then echo "Monorepo: $MONOREPO_NOTE" >&2; fi
echo "Verdict: $VERDICT" >&2
echo "Summary: $SUMMARY" >&2

# Build circular deps JSON array
CIRCULAR_JSON="[]"
if [ "$CIRCULAR_COUNT" -gt 0 ] && [ -n "$CIRCULAR_DEPS" ]; then
  circ_list=""
  while IFS= read -r c; do
    [ -z "$c" ] && continue
    circ_list="${circ_list}\"${c}\","
  done <<< "$CIRCULAR_DEPS"
  if [ -n "$circ_list" ]; then
    CIRCULAR_JSON="[${circ_list%,}]"
  fi
fi

# Escape summary for JSON
SUMMARY_ESCAPED=$(printf '%s' "$SUMMARY" | sed "s/\"/\\\\'/g" | tr -d '\n')
MONOREPO_NOTE_ESCAPED=$(printf '%s' "${MONOREPO_NOTE:-}" | sed "s/\"/\\\\'/g" | tr -d '\n')
ORPHAN_NOTE_ESCAPED=$(printf '%s' "${ORPHAN_NOTES:-}" | sed "s/\"/\\\\'/g" | tr -d '\n')

cat <<EOF
{
  "project": "$(pwd)",
  "language": "$LANGUAGE",
  "project_type": "$WRECKIT_PROJECT_TYPE",
  "is_monorepo": $IS_MONOREPO,
  "library_mode": $ORPHAN_IS_EXPECTED,
  "total_files": $TOTAL_FILES,
  "circular_deps": $CIRCULAR_JSON,
  "god_modules": $GOD_MODULES_JSON,
  "god_warn_threshold": $GOD_WARN_THRESHOLD,
  "god_fail_threshold": $GOD_FAIL_THRESHOLD,
  "orphan_files": $ORPHAN_FILES_JSON,
  "orphan_count": $ORPHAN_COUNT,
  "avg_fan_in": $AVG_FANIN,
  "avg_fan_out": $AVG_FANOUT,
  "status": "$VERDICT",
  "verdict": "$VERDICT",
  "summary": "$SUMMARY_ESCAPED",
  "confidence": $CONFIDENCE,
  "notes": {
    "monorepo": "$MONOREPO_NOTE_ESCAPED",
    "orphan": "$ORPHAN_NOTE_ESCAPED"
  }
}
EOF

rm -f "$DEPMAP_FILE" "$FANIN_FILE" "$FANOUT_FILE"


path: scripts/detect-stack.sh
content: #!/usr/bin/env bash
# wreckit — detect language, framework, test runner, type checker
# Usage: ./detect-stack.sh [project-path]
# Outputs JSON to stdout

set -euo pipefail
PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

lang=""
framework=""
test_runner=""
type_checker=""
build_cmd=""
test_cmd=""
type_cmd=""

# TypeScript / JavaScript
if [ -f "tsconfig.json" ]; then
  lang="typescript"
  type_checker="tsc"
  type_cmd="npx tsc --noEmit"
  if [ -f "package.json" ]; then
    if grep -q '"next"' package.json 2>/dev/null; then framework="nextjs"
    elif grep -q '"express"' package.json 2>/dev/null; then framework="express"
    elif grep -q '"fastify"' package.json 2>/dev/null; then framework="fastify"
    elif grep -q '"react"' package.json 2>/dev/null; then framework="react"
    elif grep -q '"vue"' package.json 2>/dev/null; then framework="vue"
    elif grep -q '"svelte"' package.json 2>/dev/null; then framework="svelte"
    fi
    if grep -q '"vitest"' package.json 2>/dev/null; then test_runner="vitest"; test_cmd="npx vitest run"
    elif grep -q '"jest"' package.json 2>/dev/null; then test_runner="jest"; test_cmd="npx jest"
    elif grep -q '"mocha"' package.json 2>/dev/null; then test_runner="mocha"; test_cmd="npx mocha"
    elif grep -q 'node --test' package.json 2>/dev/null; then test_runner="node-test"; test_cmd="npm test"
    fi
  fi
elif [ -f "package.json" ] && [ ! -f "tsconfig.json" ]; then
  lang="javascript"
  if grep -q '"vitest"' package.json 2>/dev/null; then test_runner="vitest"; test_cmd="npx vitest run"
  elif grep -q '"jest"' package.json 2>/dev/null; then test_runner="jest"; test_cmd="npx jest"
  elif grep -q 'node --test' package.json 2>/dev/null; then test_runner="node-test"; test_cmd="npm test"
  fi
fi

# Python
if [ -f "pyproject.toml" ] || [ -f "setup.py" ] || [ -f "requirements.txt" ]; then
  lang="python"
  if [ -f "pyproject.toml" ]; then
    if grep -q "django" pyproject.toml 2>/dev/null; then framework="django"
    elif grep -q "fastapi" pyproject.toml 2>/dev/null; then framework="fastapi"
    elif grep -q "flask" pyproject.toml 2>/dev/null; then framework="flask"
    fi
    if grep -q "pytest" pyproject.toml 2>/dev/null; then test_runner="pytest"; test_cmd="pytest"
    fi
  fi
  if command -v mypy &>/dev/null; then type_checker="mypy"; type_cmd="mypy --strict ."
  elif command -v pyright &>/dev/null; then type_checker="pyright"; type_cmd="pyright"
  fi
  [ -z "$test_runner" ] && test_runner="pytest" && test_cmd="pytest"
fi

# Rust
if [ -f "Cargo.toml" ]; then
  lang="rust"
  type_checker="rustc"
  type_cmd="cargo check"
  test_runner="cargo"
  test_cmd="cargo test"
  if grep -q "actix" Cargo.toml 2>/dev/null; then framework="actix"
  elif grep -q "axum" Cargo.toml 2>/dev/null; then framework="axum"
  elif grep -q "rocket" Cargo.toml 2>/dev/null; then framework="rocket"
  fi
fi

# Go
if [ -f "go.mod" ]; then
  lang="go"
  type_checker="go"
  type_cmd="go vet ./..."
  test_runner="go"
  test_cmd="go test ./..."
  if grep -q "gin-gonic" go.mod 2>/dev/null; then framework="gin"
  elif grep -q "echo" go.mod 2>/dev/null; then framework="echo"
  elif grep -q "fiber" go.mod 2>/dev/null; then framework="fiber"
  fi
fi

# Swift / Xcode
# Detect: Package.swift (SPM), *.xcodeproj, *.xcworkspace, Podfile (CocoaPods), Cartfile (Carthage)
SWIFT_DETECTED=0
build_system=""
type_check_confidence=""
mutation_support="false"  # No production mutation testing tool exists for Swift

HAS_SPM=0; [ -f "Package.swift" ] && HAS_SPM=1
HAS_XCODEPROJ=0; find . -name "*.xcodeproj" -maxdepth 2 2>/dev/null | head -1 | grep -q . && HAS_XCODEPROJ=1
HAS_XCWORKSPACE=0; find . -name "*.xcworkspace" -maxdepth 2 -not -path "*/xcodeproj/*" 2>/dev/null | head -1 | grep -q . && HAS_XCWORKSPACE=1
HAS_PODFILE=0; [ -f "Podfile" ] && HAS_PODFILE=1
HAS_CARTFILE=0; [ -f "Cartfile" ] && HAS_CARTFILE=1

if [ "$HAS_SPM" -eq 1 ] || [ "$HAS_XCODEPROJ" -eq 1 ] || [ "$HAS_XCWORKSPACE" -eq 1 ] || [ "$HAS_PODFILE" -eq 1 ] || [ "$HAS_CARTFILE" -eq 1 ]; then
  SWIFT_DETECTED=1
  lang="swift"

  # Determine build system
  SYSTEMS=0
  [ "$HAS_SPM" -eq 1 ] && SYSTEMS=$((SYSTEMS + 1))
  [ "$HAS_XCODEPROJ" -eq 1 ] && SYSTEMS=$((SYSTEMS + 1))
  [ "$HAS_PODFILE" -eq 1 ] && SYSTEMS=$((SYSTEMS + 1))
  [ "$HAS_CARTFILE" -eq 1 ] && SYSTEMS=$((SYSTEMS + 1))

  if [ "$SYSTEMS" -gt 1 ]; then
    build_system="mixed"
    type_check_confidence="medium"
  elif [ "$HAS_SPM" -eq 1 ]; then
    build_system="spm"
    type_check_confidence="high"
  elif [ "$HAS_PODFILE" -eq 1 ]; then
    build_system="cocoapods"
    type_check_confidence="medium"
  elif [ "$HAS_CARTFILE" -eq 1 ]; then
    build_system="carthage"
    type_check_confidence="medium"
  elif [ "$HAS_XCODEPROJ" -eq 1 ] || [ "$HAS_XCWORKSPACE" -eq 1 ]; then
    build_system="xcodebuild"
    type_check_confidence="medium"
  fi

  # Set type check command based on build system
  if [ "$HAS_SPM" -eq 1 ]; then
    type_checker="swift"
    type_cmd="swift build"
    build_cmd="swift build"
  elif [ "$HAS_XCODEPROJ" -eq 1 ] || [ "$HAS_XCWORKSPACE" -eq 1 ]; then
    type_checker="xcodebuild"
    # Try to detect a scheme; graceful fallback
    SCHEME=$(xcodebuild -list 2>/dev/null | sed -n '/Schemes:/,/^$/p' | grep -m1 '^\s' | xargs 2>/dev/null || echo "")
    if [ -n "$SCHEME" ]; then
      type_cmd="xcodebuild build -scheme $SCHEME -quiet"
      build_cmd="xcodebuild build -scheme $SCHEME -quiet"
    else
      type_cmd="xcodebuild build -quiet"
      build_cmd="xcodebuild build -quiet"
    fi
  fi

  # Test runner
  if [ "$HAS_SPM" -eq 1 ]; then
    test_runner="swift"
    test_cmd="swift test"
  else
    test_runner="xcodebuild"
    SCHEME=$(xcodebuild -list 2>/dev/null | sed -n '/Schemes:/,/^$/p' | grep -m1 '^\s' | xargs 2>/dev/null || echo "")
    if [ -n "$SCHEME" ]; then
      test_cmd="xcodebuild test -scheme $SCHEME -quiet"
    else
      test_cmd="xcodebuild test -quiet"
    fi
  fi

  # Framework detection
  if [ -f "Package.swift" ]; then
    if grep -q "Vapor" Package.swift 2>/dev/null; then framework="vapor"
    elif grep -q "Kitura" Package.swift 2>/dev/null; then framework="kitura"
    elif grep -q "Perfect" Package.swift 2>/dev/null; then framework="perfect"
    fi
  fi
  if [ "$HAS_PODFILE" -eq 1 ] && [ -z "$framework" ]; then
    if grep -q "Alamofire" Podfile 2>/dev/null; then framework="alamofire"; fi
  fi
fi

# Java / Kotlin
if [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
  if find . -name "*.kt" -maxdepth 3 2>/dev/null | head -1 | grep -q .; then
    lang="kotlin"
  else
    lang="java"
  fi
  type_checker="gradle"
  type_cmd="./gradlew compileJava"
  test_runner="gradle"
  test_cmd="./gradlew test"
  if grep -q "spring" build.gradle* 2>/dev/null; then framework="spring"; fi
elif [ -f "pom.xml" ]; then
  lang="java"
  type_checker="maven"
  type_cmd="mvn compile"
  test_runner="maven"
  test_cmd="mvn test"
fi

# Shell (fallback — detect if there are .sh files with a run_tests.sh or similar)
if [ -z "$lang" ]; then
  SH_COUNT=$(find . -name '*.sh' -not -path '*/.git/*' -not -path '*/node_modules/*' 2>/dev/null | wc -l | tr -d ' ')
  if [ "$SH_COUNT" -gt 0 ]; then
    lang="shell"
    # Look for a test runner script
    if [ -f "tests/run_tests.sh" ]; then test_cmd="bash tests/run_tests.sh"; test_runner="bash"
    elif [ -f "test/run_tests.sh" ]; then test_cmd="bash test/run_tests.sh"; test_runner="bash"
    elif [ -f "run_tests.sh" ]; then test_cmd="bash run_tests.sh"; test_runner="bash"
    elif find . -name "run_tests.sh" -not -path '*/.git/*' 2>/dev/null | head -1 | grep -q .; then
      RUNNER=$(find . -name "run_tests.sh" -not -path '*/.git/*' 2>/dev/null | head -1)
      test_cmd="bash $RUNNER"; test_runner="bash"
    fi
    if command -v shellcheck &>/dev/null; then type_checker="shellcheck"; type_cmd="shellcheck *.sh"; fi
  fi
fi

# Build JSON output — use Python to ensure valid JSON (handles special chars in commands)
python3 - <<PYEOF
import json
data = {
    "language": "${lang:-unknown}",
    "framework": "${framework:-none}",
    "testRunner": "${test_runner:-none}",
    "typeChecker": "${type_checker:-none}",
    "commands": {
        "typeCheck": "${type_cmd:-none}",
        "test": "${test_cmd:-none}",
        "build": "${build_cmd:-none}"
    }
}
# Swift-specific fields
if "${lang:-unknown}" == "swift":
    data["buildSystem"] = "${build_system:-unknown}"
    data["typeCheckConfidence"] = "${type_check_confidence:-medium}"
    data["mutationSupport"] = False  # No production mutation testing tool for Swift
    data["swiftIndicators"] = {
        "spm": ${HAS_SPM:-0} == 1,
        "xcodeproj": ${HAS_XCODEPROJ:-0} == 1,
        "xcworkspace": ${HAS_XCWORKSPACE:-0} == 1,
        "cocoapods": ${HAS_PODFILE:-0} == 1,
        "carthage": ${HAS_CARTFILE:-0} == 1
    }
print(json.dumps(data, indent=2))
PYEOF


path: scripts/differential-test.sh
content: #!/usr/bin/env bash
# wreckit — Differential Testing + Metamorphic Testing
# Usage: ./differential-test.sh [project-path]
# Looks for oracles, runs golden tests, checks metamorphic properties.
# Outputs structured JSON report.

set -euo pipefail

PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

log() { echo "  [differential] $*" >&2; }

echo "================================================" >&2
echo "  wreckit Differential Test Scanner" >&2
echo "  Project: $PROJECT" >&2
echo "================================================" >&2

# ─── Detect stack ────────────────────────────────────────────────────────────
STACK_JSON=$("$SCRIPT_DIR/detect-stack.sh" "$PROJECT" 2>/dev/null || echo "{}")
LANG=$(echo "$STACK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('language','unknown'))" 2>/dev/null || echo "unknown")
TEST_CMD=$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commands',{}).get('test',''))" 2>/dev/null || echo "")

log "Language: $LANG"
log "Test command: $TEST_CMD"

# ─── Initialize report state ─────────────────────────────────────────────────
ORACLE_TYPE="none"
ORACLE_DESC="No oracle found"
GOLDEN_PASS=0
GOLDEN_FAIL=0
GOLDEN_FAILURES="[]"
METAMORPHIC_VIOLATIONS="[]"
TEST_CASES_RUN=0
MISMATCHES="[]"
VERDICT="WARN"

# ─── 1. Run existing test suite (golden test verification) ───────────────────
log ""
log "Phase 1: Running existing test suite (golden tests)..."

if [ -n "$TEST_CMD" ]; then
  cd "$PROJECT"
  TEST_OUTPUT=$(bash -c "$TEST_CMD 2>&1" || true)
  
  # Parse pass/fail counts by language
  if echo "$TEST_OUTPUT" | grep -qE "passed|failed|error" 2>/dev/null; then
    # Jest/Vitest style
    PASS_COUNT=$(echo "$TEST_OUTPUT" | grep -oE "[0-9]+ (test)?(s)? passed" | grep -oE "^[0-9]+" | tail -1 || echo "0")
    FAIL_COUNT=$(echo "$TEST_OUTPUT" | grep -oE "[0-9]+ (test)?(s)? failed" | grep -oE "^[0-9]+" | tail -1 || echo "0")
    
    # Pytest style
    if [ "$PASS_COUNT" = "0" ] && [ "$FAIL_COUNT" = "0" ]; then
      PASS_COUNT=$(echo "$TEST_OUTPUT" | grep -oE "[0-9]+ passed" | grep -oE "^[0-9]+" | tail -1 || echo "0")
      FAIL_COUNT=$(echo "$TEST_OUTPUT" | grep -oE "[0-9]+ failed" | grep -oE "^[0-9]+" | tail -1 || echo "0")
    fi
    
    GOLDEN_PASS="${PASS_COUNT:-0}"
    GOLDEN_FAIL="${FAIL_COUNT:-0}"
    
    log "  Golden tests: $GOLDEN_PASS passed, $GOLDEN_FAIL failed"
    
    if [ "${GOLDEN_FAIL:-0}" -gt 0 ]; then
      # Extract failing test names
      FAILING=$(echo "$TEST_OUTPUT" | grep -E "(FAIL|ERROR|✗|×)" | head -5 | python3 -c "
import sys, json
lines = [l.strip() for l in sys.stdin if l.strip()][:5]
print(json.dumps(lines))
" 2>/dev/null || echo "[]")
      GOLDEN_FAILURES="$FAILING"
      ORACLE_TYPE="golden"
      ORACLE_DESC="Existing test suite with expected outputs"
      VERDICT="FAIL"
    else
      ORACLE_TYPE="golden"
      ORACLE_DESC="Existing test suite with expected outputs ($GOLDEN_PASS tests)"
      VERDICT="PASS"
      TEST_CASES_RUN=$GOLDEN_PASS
    fi
  else
    log "  Could not parse test output"
    TEST_CASES_RUN=0
  fi
  cd "$OLDPWD" 2>/dev/null || true
else
  log "  No test command detected — skipping golden test phase"
fi

# ─── 2. Look for snapshot/fixture files ──────────────────────────────────────
log ""
log "Phase 2: Checking for snapshot/fixture files..."

SNAPSHOT_COUNT=$(find "$PROJECT" \
  -not -path "*/.git/*" -not -path "*/node_modules/*" -not -path "*/__pycache__/*" \
  \( -name "*.snap" -o -name "*.snapshot" -o -name "*fixture*.json" -o -name "*expected*.json" \) \
  2>/dev/null | wc -l | tr -d ' ')

if [ "$SNAPSHOT_COUNT" -gt 0 ]; then
  log "  Found $SNAPSHOT_COUNT snapshot/fixture files"
  if [ "$ORACLE_TYPE" = "none" ]; then
    ORACLE_TYPE="golden"
    ORACLE_DESC="Snapshot/fixture files ($SNAPSHOT_COUNT files)"
  fi
else
  log "  No snapshot files found"
fi

# ─── 3. Look for reference implementations ───────────────────────────────────
log ""
log "Phase 3: Looking for reference/oracle implementations..."

REF_DIRS=$(find "$PROJECT" \
  -not -path "*/.git/*" -not -path "*/node_modules/*" \
  -type d \( -name "reference" -o -name "oracle" -o -name "reference_impl" -o -name "baseline" \) \
  2>/dev/null | head -5)

if [ -n "$REF_DIRS" ]; then
  log "  Found reference implementation directories: $REF_DIRS"
  if [ "$ORACLE_TYPE" = "none" ]; then
    ORACLE_TYPE="reference"
    ORACLE_DESC="Reference implementation directory found: $(echo "$REF_DIRS" | head -1)"
  fi
else
  log "  No reference implementation directories found"
fi

# ─── 4. Metamorphic property detection ───────────────────────────────────────
log ""
log "Phase 4: Checking metamorphic properties..."

# Look for common patterns that should have metamorphic properties
METAMORPHIC_CHECKS=()

# Sort functions
if grep -rn --include="*.py" --include="*.js" --include="*.ts" \
  -E "\bsort\b|\bsorted\b" "$PROJECT" 2>/dev/null | head -3 | grep -q .; then
  METAMORPHIC_CHECKS+=("sort")
  log "  Detected: sort function (should satisfy: sort(reverse(x)) = reverse(sort(x)))"
fi

# Encode/decode pairs  
if grep -rn --include="*.py" --include="*.js" --include="*.ts" \
  -E "\bencode\b|\bdecode\b|\bserialize\b|\bdeserialize\b|\bparse\b|\bstringify\b" \
  "$PROJECT" 2>/dev/null | head -3 | grep -q .; then
  METAMORPHIC_CHECKS+=("encode_decode")
  log "  Detected: encode/decode pair (should satisfy: decode(encode(x)) = x)"
fi

# Hash functions
if grep -rn --include="*.py" --include="*.js" --include="*.ts" \
  -E "\bhash\b|\bdigest\b|\bchecksum\b" \
  "$PROJECT" 2>/dev/null | head -3 | grep -q .; then
  METAMORPHIC_CHECKS+=("hash")
  log "  Detected: hash function (should satisfy: hash(x) = hash(x) always)"
fi

# Filter functions
if grep -rn --include="*.py" --include="*.js" --include="*.ts" \
  -E "\bfilter\b|\bwhere\b|\bselect\b" \
  "$PROJECT" 2>/dev/null | head -3 | grep -q .; then
  METAMORPHIC_CHECKS+=("filter")
  log "  Detected: filter function (should satisfy: len(filter(x)) <= len(x))"
fi

if [ ${#METAMORPHIC_CHECKS[@]} -gt 0 ]; then
  DETECTED_TYPES=$(printf '"%s",' "${METAMORPHIC_CHECKS[@]}" | sed 's/,$//')
  log "  Metamorphic checks applicable: ${METAMORPHIC_CHECKS[*]}"
  log "  NOTE: Run these manually or add property tests (see property-based gate)"
  
  if [ "$ORACLE_TYPE" = "none" ]; then
    ORACLE_TYPE="metamorphic"
    ORACLE_DESC="Metamorphic properties detected for: ${METAMORPHIC_CHECKS[*]}"
    if [ "$VERDICT" = "WARN" ]; then
      VERDICT="WARN"
    fi
  fi
else
  log "  No obvious metamorphic properties detected"
fi

# ─── 5. Check for duplicate implementations ─────────────────────────────────
log ""
log "Phase 5: Looking for duplicate/alternative implementations..."

# Find files with similar names that might be alternatives
DUP_PATTERN=""
if [ "$LANG" = "python" ]; then
  # Look for _v2, _old, _new, _backup, _ref suffixes
  DUPS=$(find "$PROJECT" -name "*.py" \
    -not -path "*/.git/*" -not -path "*/__pycache__/*" \
    | grep -E "(_v[0-9]+|_old|_new|_backup|_ref|_orig)\." | head -5 || true)
elif [ "$LANG" = "typescript" ] || [ "$LANG" = "javascript" ]; then
  DUPS=$(find "$PROJECT" \( -name "*.ts" -o -name "*.js" \) \
    -not -path "*/.git/*" -not -path "*/node_modules/*" \
    | grep -E "(_v[0-9]+|_old|_new|_backup|_ref|_orig)\." | head -5 || true)
fi

if [ -n "${DUPS:-}" ]; then
  log "  Found potential alternative implementations: $DUPS"
  if [ "$ORACLE_TYPE" = "none" ]; then
    ORACLE_TYPE="reference"
    ORACLE_DESC="Alternative implementation files found: $(echo "$DUPS" | head -1)"
  fi
fi

# ─── 6. Git diff check (regression against last commit) ──────────────────────
log ""
log "Phase 6: Checking for regressions vs last commit..."

if git -C "$PROJECT" log --oneline -1 2>/dev/null > /dev/null; then
  # Check if any test files changed in the last commit
  CHANGED_TESTS=$(git -C "$PROJECT" diff HEAD~1 --name-only 2>/dev/null | grep -E "test_|_test\.|spec\." | head -5 || true)
  if [ -n "$CHANGED_TESTS" ]; then
    log "  Test files changed in last commit: $CHANGED_TESTS"
    log "  These should be run to verify no regressions"
  else
    log "  No test file changes in last commit"
  fi
fi

# ─── Build summary ────────────────────────────────────────────────────────────
log ""
log "Building report..."

# Final verdict logic
if [ "$GOLDEN_FAIL" -gt 0 ]; then
  VERDICT="FAIL"
elif [ "$ORACLE_TYPE" = "none" ] && [ ${#METAMORPHIC_CHECKS[@]} -eq 0 ]; then
  VERDICT="WARN"
  ORACLE_DESC="No oracle, no metamorphic properties, no reference found. Differential testing skipped."
elif [ "$VERDICT" = "WARN" ] && [ "$GOLDEN_PASS" -gt 0 ]; then
  VERDICT="PASS"
fi

METAMORPHIC_ARRAY="[]"
if [ ${#METAMORPHIC_CHECKS[@]} -gt 0 ]; then
  METAMORPHIC_ARRAY=$(printf '"%s",' "${METAMORPHIC_CHECKS[@]}" | sed 's/,$//')
  METAMORPHIC_ARRAY="[$METAMORPHIC_ARRAY]"
fi

python3 << PYEOF
import json

report = {
    "project": "$PROJECT",
    "scanner": "wreckit-differential",
    "oracle_type": "$ORACLE_TYPE",
    "oracle_description": "$ORACLE_DESC",
    "golden_tests": {
        "passed": $GOLDEN_PASS,
        "failed": $GOLDEN_FAIL,
        "failures": $GOLDEN_FAILURES
    },
    "metamorphic_properties_detected": $METAMORPHIC_ARRAY,
    "metamorphic_violations": $METAMORPHIC_VIOLATIONS,
    "snapshot_files_found": $SNAPSHOT_COUNT,
    "mismatches": $MISMATCHES,
    "verdict": "$VERDICT",
    "notes": "For full differential testing, create a reference implementation or golden fixture files. See references/gates/differential.md for the manual process."
}

print(json.dumps(report, indent=2))
PYEOF

echo "" >&2
echo "Results:" >&2
echo "  Oracle type: $ORACLE_TYPE" >&2
echo "  Golden tests: $GOLDEN_PASS passed, $GOLDEN_FAIL failed" >&2
echo "  Metamorphic checks: ${#METAMORPHIC_CHECKS[@]}" >&2
echo "  Verdict: $VERDICT" >&2


path: scripts/dynamic-analysis.sh
content: #!/usr/bin/env bash
# wreckit — Dynamic Analysis Scanner
# Usage: ./dynamic-analysis.sh [project-path]
# Runs memory leak checks, race detectors, and sanitizers per language.
# Outputs structured JSON report.

set -euo pipefail

PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

log() { echo "  [dynamic-analysis] $*" >&2; }

echo "================================================" >&2
echo "  wreckit Dynamic Analysis Scanner" >&2
echo "  Project: $PROJECT" >&2
echo "================================================" >&2

# ─── Detect stack ─────────────────────────────────────────────────────────────
STACK_JSON=$("$SCRIPT_DIR/detect-stack.sh" "$PROJECT" 2>/dev/null || echo "{}")
LANG=$(echo "$STACK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('language','unknown'))" 2>/dev/null || echo "unknown")
TEST_CMD=$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commands',{}).get('test',''))" 2>/dev/null || echo "")

log "Language: $LANG"
log "Test command: $TEST_CMD"

# ─── State ─────────────────────────────────────────────────────────────────────
TOOLS_AVAILABLE="[]"
FINDINGS="[]"
RACE_DETECTED=false
LEAK_DETECTED=false
BLOCKER_COUNT=0
WARNING_COUNT=0
VERDICT="WARN"
NOTES=""

add_finding() {
  local severity="$1" category="$2" description="$3" evidence="$4"
  case "$severity" in
    blocker) BLOCKER_COUNT=$((BLOCKER_COUNT+1)) ;;
    warning) WARNING_COUNT=$((WARNING_COUNT+1)) ;;
  esac
  NEW_FINDING=$(python3 -c "
import json
print(json.dumps({
    'severity': '$severity',
    'category': '$category',
    'description': '$3',
    'evidence': '$4'[:200]
}))
" 2>/dev/null || echo "{}")
  FINDINGS=$(python3 -c "
import json,sys
findings = json.loads('$FINDINGS') if '$FINDINGS' != '[]' else []
findings.append(json.loads('''$NEW_FINDING'''))
print(json.dumps(findings))
" 2>/dev/null || echo "$FINDINGS")
}

# ─── Phase 1: Language-specific dynamic analysis ──────────────────────────────
log ""
log "Phase 1: Running language-specific dynamic analysis..."

case "$LANG" in

  go)
    log "  Running: go test -race"
    cd "$PROJECT"
    
    # Check if go is available
    if command -v go >/dev/null 2>&1; then
      TOOLS_AVAILABLE='["go-race", "go-pprof"]'
      
      # Race detector
      RACE_OUTPUT=$(go test -race ./... 2>&1 || true)
      if echo "$RACE_OUTPUT" | grep -q "DATA RACE\|WARNING: DATA RACE"; then
        RACE_DETECTED=true
        BLOCKER_COUNT=$((BLOCKER_COUNT+1))
        RACE_EVIDENCE=$(echo "$RACE_OUTPUT" | grep -A3 "DATA RACE" | head -5 | tr '\n' '|')
        log "  ❌ RACE CONDITION DETECTED: $RACE_EVIDENCE"
        FINDINGS=$(python3 -c "
import json
print(json.dumps([{
    'severity': 'blocker',
    'category': 'race_condition',
    'description': 'Data race detected by go -race',
    'evidence': '''${RACE_EVIDENCE:0:200}'''
}]))
")
      else
        log "  ✅ No race conditions detected"
      fi
      
      # Memory profile (if tests pass)
      MEM_OUTPUT=$(go test -memprofile=/tmp/wreckit-mem.out ./... 2>&1 | tail -3 || true)
      log "  Memory profile: $MEM_OUTPUT"
      rm -f /tmp/wreckit-mem.out
    else
      log "  go not found — skipping"
      NOTES="go not installed"
    fi
    cd "$OLDPWD" 2>/dev/null || true
    ;;

  python)
    log "  Running: Python memory + async leak checks"
    cd "$PROJECT"
    
    TOOLS_FOUND="[]"
    
    # Check what's available
    if python3 -c "import tracemalloc" 2>/dev/null; then
      TOOLS_FOUND='["tracemalloc"]'
      log "  tracemalloc: available (built-in)"
    fi
    
    if python3 -c "import memory_profiler" 2>/dev/null; then
      TOOLS_FOUND='["tracemalloc","memory_profiler"]'
      log "  memory_profiler: available"
    fi
    
    if python3 -c "import objgraph" 2>/dev/null; then
      log "  objgraph: available"
    fi
    
    TOOLS_AVAILABLE="$TOOLS_FOUND"
    
    # Run tests with tracemalloc
    if [ -n "$TEST_CMD" ]; then
      log "  Running test suite with memory tracking..."
      
      PYTHON_MEM_SCRIPT=$(cat << 'PYEOF'
import tracemalloc
import subprocess
import sys

tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()

# Run tests
result = subprocess.run(sys.argv[1:], capture_output=True, text=True)
print(result.stdout[-2000:] if result.stdout else "")
if result.stderr:
    print(result.stderr[-1000:], file=sys.stderr)

snapshot2 = tracemalloc.take_snapshot()
stats = snapshot2.compare_to(snapshot1, 'lineno')

total_added = sum(s.size_diff for s in stats if s.size_diff > 0)
total_mb = total_added / 1024 / 1024

print(f"\n[wreckit-tracemalloc] Net memory added: {total_mb:.2f} MB", file=sys.stderr)

if total_mb > 10:
    print(f"[wreckit-tracemalloc] WARNING: >10MB memory growth", file=sys.stderr)
    top5 = "\n".join(str(s) for s in stats[:5])
    print(f"[wreckit-tracemalloc] Top allocations:\n{top5}", file=sys.stderr)
    sys.exit(2)  # Signal high memory growth
PYEOF
)
      MEM_OUTPUT=$(python3 -c "$PYTHON_MEM_SCRIPT" $TEST_CMD 2>&1 | tail -10 || true)
      
      if echo "$MEM_OUTPUT" | grep -q "WARNING: >10MB"; then
        WARNING_COUNT=$((WARNING_COUNT+1))
        log "  ⚠️ Memory growth >10MB detected"
        FINDINGS=$(python3 -c "
import json
print(json.dumps([{
    'severity': 'warning',
    'category': 'memory_leak',
    'description': 'Test suite causes >10MB memory growth',
    'evidence': '${MEM_OUTPUT//\'/}'[:200]
}]))
")
      else
        MEM_MB=$(echo "$MEM_OUTPUT" | grep "Net memory" | grep -oE "[0-9]+\.[0-9]+" || echo "unknown")
        log "  ✅ Memory growth: ${MEM_MB}MB (acceptable)"
      fi
    fi
    
    # Check for common async leak patterns
    log "  Scanning for async leak patterns..."
    ASYNC_LEAKS=$(grep -rn --include="*.py" \
      -E "asyncio\.create_task\|loop\.call_soon\|threading\.Thread" \
      "$PROJECT" 2>/dev/null | grep -v "test_\|#" | head -5 || true)
    
    if [ -n "$ASYNC_LEAKS" ]; then
      log "  Note: Async task creation found — verify cleanup on exit"
    fi
    
    cd "$OLDPWD" 2>/dev/null || true
    ;;

  typescript|javascript)
    log "  Running: Node.js heap + async leak checks"
    cd "$PROJECT"
    
    # Check available tools
    TOOLS_FOUND="[]"
    
    if [ -f "node_modules/.bin/clinic" ] || command -v clinic >/dev/null 2>&1; then
      TOOLS_FOUND='["clinic"]'
      log "  clinic.js: available"
    fi
    
    if [ -f "node_modules/leakage/package.json" ]; then
      TOOLS_FOUND='["leakage"]'
      log "  leakage: available"
    fi
    
    TOOLS_AVAILABLE="$TOOLS_FOUND"
    
    # Check for common Node.js leak patterns in source
    log "  Scanning for common memory leak patterns..."
    
    # Event listener leaks
    LISTENER_LEAKS=$(grep -rn --include="*.ts" --include="*.js" \
      -E "\.on\([\"']|addEventListener\(" \
      "$PROJECT" 2>/dev/null \
      --exclude-dir=node_modules --exclude-dir=dist | head -10 || true)
    
    LISTENER_COUNT=$(echo "$LISTENER_LEAKS" | grep -c "." || echo "0")
    LISTENER_REMOVE=$(grep -rn --include="*.ts" --include="*.js" \
      -E "\.off\([\"']|removeEventListener\(|removeAllListeners\(" \
      "$PROJECT" 2>/dev/null \
      --exclude-dir=node_modules --exclude-dir=dist | wc -l | tr -d ' ' || echo "0")
    
    if [ "$LISTENER_COUNT" -gt 0 ] && [ "$LISTENER_REMOVE" -eq 0 ]; then
      WARNING_COUNT=$((WARNING_COUNT+1))
      log "  ⚠️ $LISTENER_COUNT event listeners added, 0 removed — possible leak"
      FINDINGS=$(python3 -c "
import json
print(json.dumps([{
    'severity': 'warning',
    'category': 'async_leak',
    'description': '$LISTENER_COUNT event listeners added but none removed',
    'evidence': 'No .off() or removeEventListener() calls found'
}]))
")
    else
      log "  ✅ Event listener cleanup looks ok ($LISTENER_COUNT adds, $LISTENER_REMOVE removes)"
    fi
    
    # Check for setInterval without clearInterval
    INTERVALS=$(grep -rn --include="*.ts" --include="*.js" \
      -E "setInterval\(" \
      "$PROJECT" 2>/dev/null --exclude-dir=node_modules | wc -l | tr -d ' ' || echo "0")
    CLEAR_INTERVALS=$(grep -rn --include="*.ts" --include="*.js" \
      -E "clearInterval\(" \
      "$PROJECT" 2>/dev/null --exclude-dir=node_modules | wc -l | tr -d ' ' || echo "0")
    
    if [ "$INTERVALS" -gt 0 ] && [ "$CLEAR_INTERVALS" -eq 0 ]; then
      WARNING_COUNT=$((WARNING_COUNT+1))
      log "  ⚠️ $INTERVALS setInterval calls with 0 clearInterval — possible timer leak"
    fi
    
    cd "$OLDPWD" 2>/dev/null || true
    ;;

  rust)
    log "  Running: cargo test + Miri check"
    cd "$PROJECT"
    TOOLS_AVAILABLE='["cargo-test"]'
    
    if command -v cargo >/dev/null 2>&1; then
      # Regular tests first
      TEST_OUT=$(cargo test 2>&1 | tail -5 || true)
      log "  Test output: $TEST_OUT"
      
      # Check if Miri is available
      if cargo miri --version >/dev/null 2>&1; then
        TOOLS_AVAILABLE='["cargo-test", "miri"]'
        log "  Running Miri (undefined behavior detector)..."
        MIRI_OUT=$(cargo miri test 2>&1 | tail -10 || true)
        
        if echo "$MIRI_OUT" | grep -q "Undefined Behavior\|error\[E"; then
          BLOCKER_COUNT=$((BLOCKER_COUNT+1))
          log "  ❌ Miri found undefined behavior"
          FINDINGS=$(python3 -c "
import json
print(json.dumps([{
    'severity': 'blocker',
    'category': 'undefined_behavior',
    'description': 'Miri detected undefined behavior',
    'evidence': '''${MIRI_OUT:0:300}'''
}]))
")
        else
          log "  ✅ Miri: no undefined behavior detected"
        fi
      else
        log "  Miri not installed (optional: rustup component add miri)"
      fi
    fi
    cd "$OLDPWD" 2>/dev/null || true
    ;;

  *)
    log "  No specific dynamic analysis for language: $LANG"
    NOTES="No dynamic analysis tooling for $LANG. See references/gates/dynamic-analysis.md for manual steps."
    ;;
esac

# ─── Phase 2: Static file descriptor leak scan ─────────────────────────────────
log ""
log "Phase 2: Static file handle leak scan..."

# Look for opened files/streams without close() in the same scope
cd "$PROJECT"

# Python: open() without close() or context manager
PY_OPEN_NO_CLOSE=$(grep -rn --include="*.py" \
  -E "^\s*\w+\s*=\s*open\s*\(" \
  . 2>/dev/null | grep -v '.git/' | grep -v 'test_' || true)
PY_WITH_OPEN=$(grep -rn --include="*.py" \
  -E "with open\s*\(" \
  . 2>/dev/null | grep -v '.git/' | wc -l | tr -d ' ' || echo "0")
PY_RAW_OPEN=$(echo "$PY_OPEN_NO_CLOSE" | grep -c '.' 2>/dev/null || true)
PY_RAW_OPEN="${PY_RAW_OPEN:-0}"

if [ "$PY_RAW_OPEN" -gt 0 ] && [ "$PY_RAW_OPEN" -gt "$PY_WITH_OPEN" ]; then
  WARNING_COUNT=$((WARNING_COUNT+1))
  log "  ⚠️ $PY_RAW_OPEN raw open() calls found (prefer 'with open()' context manager)"
else
  log "  ✅ File handle patterns look safe ($PY_RAW_OPEN raw opens, $PY_WITH_OPEN context managers)"
fi

# JS/TS: fs.open/createReadStream without .close() or .destroy()
JS_OPEN=$(grep -rn --include="*.ts" --include="*.js" \
  -E "fs\.(open|createReadStream|createWriteStream)\s*\(" \
  . 2>/dev/null --exclude-dir=node_modules --exclude-dir=dist | wc -l | tr -d ' ' || echo "0")
JS_CLOSE=$(grep -rn --include="*.ts" --include="*.js" \
  -E "\.(close|destroy)\s*\(\s*\)" \
  . 2>/dev/null --exclude-dir=node_modules --exclude-dir=dist | wc -l | tr -d ' ' || echo "0")

if [ "$JS_OPEN" -gt 0 ] && [ "$JS_CLOSE" -eq 0 ]; then
  WARNING_COUNT=$((WARNING_COUNT+1))
  log "  ⚠️ $JS_OPEN fs.open/stream calls with 0 .close()/.destroy() calls"
else
  log "  ✅ Stream/FD cleanup looks ok ($JS_OPEN opens, $JS_CLOSE closes)"
fi

cd "$OLDPWD" 2>/dev/null || true

# ─── Determine verdict ─────────────────────────────────────────────────────────
if [ "$BLOCKER_COUNT" -gt 0 ]; then
  VERDICT="FAIL"
elif [ "$WARNING_COUNT" -gt 0 ]; then
  VERDICT="WARN"
elif [ "$LANG" = "unknown" ]; then
  VERDICT="WARN"
else
  VERDICT="PASS"
fi

# ─── Output report ─────────────────────────────────────────────────────────────
log ""
log "Building report..."

python3 << PYEOF
import json

race_detected = "$RACE_DETECTED" == "true"
leak_detected = "$LEAK_DETECTED" == "true"

tools = $TOOLS_AVAILABLE if '$TOOLS_AVAILABLE' != '' else []
findings = $FINDINGS if '$FINDINGS' != '' else []

report = {
    "project": "$PROJECT",
    "scanner": "wreckit-dynamic-analysis",
    "language": "$LANG",
    "tools_used": tools,
    "findings": findings,
    "summary": {
        "blockers": $BLOCKER_COUNT,
        "warnings": $WARNING_COUNT,
        "race_detected": race_detected,
        "leak_detected": leak_detected
    },
    "verdict": "$VERDICT",
    "notes": "${NOTES:-}"
}

print(json.dumps(report, indent=2))
PYEOF

echo "" >&2
echo "Results:" >&2
echo "  Blockers: $BLOCKER_COUNT, Warnings: $WARNING_COUNT" >&2
echo "  Race detected: $RACE_DETECTED" >&2
echo "  Verdict: $VERDICT" >&2


path: scripts/mutation-test-stryker.sh
content: #!/usr/bin/env bash
# wreckit — Stryker-based mutation testing for JS/TS projects
# Usage: ./mutation-test-stryker.sh [project-path]
# Output: JSON to stdout with kill rate and verdict
# Exit 0 = results produced (check JSON verdict), Exit 1 = cannot run

set -euo pipefail
PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

echo "=== Stryker Mutation Testing ===" >&2
echo "Project: $(pwd)" >&2

# ─── Prerequisites ────────────────────────────────────────────────────────────

if ! command -v npx >/dev/null 2>&1; then
  echo '{"error":"npx required but not found. Install Node.js and npm first.","tool":"stryker"}' >&2
  echo '{"error":"npx required but not found. Install Node.js and npm first.","tool":"stryker"}'
  exit 1
fi

if [ ! -f "package.json" ]; then
  echo '{"error":"No package.json found. Stryker requires a Node.js/JS/TS project.","tool":"stryker"}' >&2
  echo '{"error":"No package.json found. Stryker requires a Node.js/JS/TS project.","tool":"stryker"}'
  exit 1
fi

# ─── Check Stryker installed ──────────────────────────────────────────────────

STRYKER_INSTALLED=false
if [ -d "node_modules/@stryker-mutator/core" ]; then
  STRYKER_INSTALLED=true
elif npx --no-install stryker --version >/dev/null 2>&1; then
  STRYKER_INSTALLED=true
fi

if [ "$STRYKER_INSTALLED" = false ]; then
  echo "Stryker not installed in this project." >&2
  echo "To install Stryker, run one of:" >&2
  echo "  npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner" >&2
  echo "  npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner" >&2
  echo "  npm install --save-dev @stryker-mutator/core @stryker-mutator/mocha-runner" >&2
  echo "" >&2
  echo "Then create stryker.config.json or stryker.config.mjs in your project root." >&2
  echo "Fallback: run scripts/mutation-test.sh for AI-based mutation testing." >&2
  cat <<EOF
{
  "error": "Stryker not installed",
  "tool": "stryker",
  "install_commands": [
    "npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner",
    "npm install --save-dev @stryker-mutator/core @stryker-mutator/vitest-runner"
  ],
  "fallback": "Run scripts/mutation-test.sh for AI-based mutation testing",
  "verdict": "BLOCKED"
}
EOF
  exit 1
fi

# ─── Run Stryker ──────────────────────────────────────────────────────────────

echo "Running Stryker mutation testing..." >&2
STRYKER_EXIT=0
npx stryker run --reporters json 2>&1 | tee /tmp/wreckit-stryker-output-$$ >&2 || STRYKER_EXIT=$?

# ─── Find and parse mutation report ───────────────────────────────────────────

REPORT_PATH=""
for candidate in \
  "reports/mutation/mutation.json" \
  "reports/mutation.json" \
  ".stryker-tmp/reports/mutation.json"; do
  if [ -f "$candidate" ]; then
    REPORT_PATH="$candidate"
    break
  fi
done

if [ -z "$REPORT_PATH" ]; then
  echo "Stryker ran but mutation report not found." >&2
  echo "Expected at: reports/mutation/mutation.json" >&2
  echo "Fallback: run scripts/mutation-test.sh instead." >&2
  cat <<EOF
{
  "error": "Mutation report not found after stryker run",
  "tool": "stryker",
  "stryker_exit_code": $STRYKER_EXIT,
  "fallback": "Run scripts/mutation-test.sh for AI-based mutation testing",
  "verdict": "BLOCKED"
}
EOF
  rm -f "/tmp/wreckit-stryker-output-$$"
  exit 1
fi

echo "Parsing report: $REPORT_PATH" >&2

# ─── Extract metrics from JSON report ─────────────────────────────────────────
# Stryker JSON report format: { "files": {...}, "schemaVersion": "...", ... }
# Counts come from iterating mutant statuses

KILLED=0
SURVIVED=0
TIMEOUT_COUNT=0
TOTAL=0

# Parse using basic shell tools (no jq dependency)
if command -v python3 >/dev/null 2>&1; then
  METRICS=$(python3 - "$REPORT_PATH" <<'PYEOF'
import json, sys

with open(sys.argv[1]) as f:
    report = json.load(f)

killed = survived = timeout = total = 0

# Stryker report has "files" key with file entries containing "mutants"
files = report.get("files", {})
for fname, fdata in files.items():
    for mutant in fdata.get("mutants", []):
        status = mutant.get("status", "").lower()
        total += 1
        if status == "killed":
            killed += 1
        elif status == "survived":
            survived += 1
        elif status in ("timeout", "timedout"):
            timeout += 1
            killed += 1  # timeouts count as killed

print(f"{killed} {survived} {timeout} {total}")
PYEOF
  ) || METRICS="0 0 0 0"

  KILLED=$(echo "$METRICS" | awk '{print $1}')
  SURVIVED=$(echo "$METRICS" | awk '{print $2}')
  TIMEOUT_COUNT=$(echo "$METRICS" | awk '{print $3}')
  TOTAL=$(echo "$METRICS" | awk '{print $4}')

elif command -v node >/dev/null 2>&1; then
  METRICS=$(node -e "
const r = require('$(pwd)/$REPORT_PATH');
let killed=0,survived=0,timeout=0,total=0;
for(const f of Object.values(r.files||{})){
  for(const m of (f.mutants||[])){
    total++;
    const s=(m.status||'').toLowerCase();
    if(s==='killed') killed++;
    else if(s==='survived') survived++;
    else if(s==='timeout'||s==='timedout'){timeout++;killed++;}
  }
}
console.log(killed+' '+survived+' '+timeout+' '+total);
" 2>/dev/null) || METRICS="0 0 0 0"

  KILLED=$(echo "$METRICS" | awk '{print $1}')
  SURVIVED=$(echo "$METRICS" | awk '{print $2}')
  TIMEOUT_COUNT=$(echo "$METRICS" | awk '{print $3}')
  TOTAL=$(echo "$METRICS" | awk '{print $4}')
else
  # Fallback: count occurrences of status strings in raw JSON
  KILLED=$(grep -o '"status":"Killed"' "$REPORT_PATH" 2>/dev/null | wc -l | tr -d ' ' || echo 0)
  SURVIVED=$(grep -o '"status":"Survived"' "$REPORT_PATH" 2>/dev/null | wc -l | tr -d ' ' || echo 0)
  TIMEOUT_COUNT=$(grep -o '"status":"Timeout"' "$REPORT_PATH" 2>/dev/null | wc -l | tr -d ' ' || echo 0)
  KILLED=$((KILLED + TIMEOUT_COUNT))
  TOTAL=$((KILLED + SURVIVED))
fi

# ─── Compute kill rate ────────────────────────────────────────────────────────

KILL_RATE="0.0"
if [ "$TOTAL" -gt 0 ] && command -v bc >/dev/null 2>&1; then
  KILL_RATE=$(echo "scale=1; $KILLED * 100 / $TOTAL" | bc 2>/dev/null || echo "0.0")
elif [ "$TOTAL" -gt 0 ] && command -v python3 >/dev/null 2>&1; then
  KILL_RATE=$(python3 -c "print(round($KILLED*100/$TOTAL,1))" 2>/dev/null || echo "0.0")
fi

# ─── Determine verdict ────────────────────────────────────────────────────────

VERDICT="BLOCKED"
KILL_INT=$(echo "$KILL_RATE" | cut -d. -f1)

if [ "$KILL_INT" -ge 95 ] 2>/dev/null; then
  VERDICT="SHIP"
elif [ "$KILL_INT" -ge 90 ] 2>/dev/null; then
  VERDICT="CAUTION"
else
  VERDICT="BLOCKED"
fi

# ─── Output ───────────────────────────────────────────────────────────────────

echo "" >&2
echo "Total mutations: $TOTAL" >&2
echo "Killed: $KILLED  |  Survived: $SURVIVED  |  Timeout: $TIMEOUT_COUNT" >&2
echo "Kill rate: ${KILL_RATE}%" >&2
echo "Verdict: $VERDICT" >&2

if [ "$VERDICT" = "BLOCKED" ]; then
  echo "" >&2
  echo "Kill rate below 90%. Tests are not catching enough mutations." >&2
  echo "Consider: adding edge-case tests, boundary value tests, error-path tests." >&2
fi

cat <<EOF
{
  "tool": "stryker",
  "kill_rate": $KILL_RATE,
  "killed": $KILLED,
  "survived": $SURVIVED,
  "timeout": $TIMEOUT_COUNT,
  "total": $TOTAL,
  "report_path": "$REPORT_PATH",
  "verdict": "$VERDICT"
}
EOF

rm -f "/tmp/wreckit-stryker-output-$$"


path: scripts/mutation-test.sh
content: #!/usr/bin/env bash
# wreckit — automated mutation testing
# Usage: ./mutation-test.sh [project-path] [test-command]
# Generates mutations, runs tests, reports kill rate
# Exit 0 = results produced, check JSON for pass/fail

set -euo pipefail
# Capture SCRIPT_DIR before any cd (BASH_SOURCE may be relative)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT="${1:-.}"
TEST_CMD="${2:-}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

verdict_from_rate() {
  local rate="$1"
  local rate_int
  rate_int=$(echo "$rate" | cut -d. -f1)
  if [ "$rate_int" -ge 95 ] 2>/dev/null; then
    echo "SHIP"
  elif [ "$rate_int" -ge 90 ] 2>/dev/null; then
    echo "CAUTION"
  else
    echo "BLOCKED"
  fi
}

# Auto-detect test command
if [ -z "$TEST_CMD" ]; then
  if [ -f "package.json" ]; then
    if grep -q '"vitest"' package.json 2>/dev/null; then TEST_CMD="npx vitest run"
    elif grep -q '"jest"' package.json 2>/dev/null; then TEST_CMD="npx jest"
    elif grep -q 'node --test' package.json 2>/dev/null; then TEST_CMD="npm test"
    fi
  elif [ -f "Cargo.toml" ]; then TEST_CMD="cargo test"
  elif [ -f "go.mod" ]; then TEST_CMD="go test ./..."
  elif [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then TEST_CMD="pytest"
  elif [ -f "Package.swift" ]; then TEST_CMD="swift test"
  elif [ -f "tests/run_tests.sh" ]; then TEST_CMD="bash tests/run_tests.sh"
  elif [ -f "test/run_tests.sh" ]; then TEST_CMD="bash test/run_tests.sh"
  elif [ -f "run_tests.sh" ]; then TEST_CMD="bash run_tests.sh"
  elif find . -name "run_tests.sh" -not -path '*/.git/*' -maxdepth 3 2>/dev/null | head -1 | grep -q .; then
    TEST_CMD="bash $(find . -name 'run_tests.sh' -not -path '*/.git/*' -maxdepth 3 2>/dev/null | head -1)"
  fi
fi

if [ -z "$TEST_CMD" ]; then
  echo '{"error":"Could not detect test command. Pass as second argument."}'
  exit 1
fi

echo "Test command: $TEST_CMD" >&2

# ─── Swift AI-estimated mutation testing ───────────────────────────────────
# Swift is handled BEFORE the generic baseline check because:
# 1. Swift has no real mutation tool — we do AI estimation
# 2. The Swift block runs its own test check internally
# 3. We want to produce useful output even when swift test is slow or flaky
# WHY AI-ESTIMATED: As of 2026, there is no production-ready mutation testing
# framework for Swift. Tools like Mull (LLVM-based) exist in research but are
# not reliable enough for CI. We honestly estimate mutation kill rate based on
# code analysis + test results. The verdict is ALWAYS "CAUTION" — never "SHIP"
# — because we can't mechanically verify that tests catch mutations.
if [ -f "Package.swift" ] || find . -name "*.xcodeproj" -maxdepth 2 2>/dev/null | head -1 | grep -q .; then
  echo "Swift project detected — using AI-estimated mutation analysis" >&2
  echo "NOTE: No production mutation testing tool exists for Swift" >&2

  # Sample up to 20 Swift source files (exclude tests, generated code, Pods, build artifacts)
  SWIFT_SRC_FILES=$(find . -name '*.swift' \
    -not -name '*Test*.swift' -not -name '*Tests.swift' -not -name '*Spec.swift' \
    -not -path '*/Tests/*' -not -path '*/XCTests/*' -not -path '*/.build/*' \
    -not -path '*/Pods/*' -not -path '*/Generated/*' -not -path '*/DerivedData/*' \
    -not -path '*/.git/*' -not -path '*/Carthage/*' \
    2>/dev/null | head -20 || true)

  SWIFT_FILE_COUNT=$(echo "$SWIFT_SRC_FILES" | grep -c '.' 2>/dev/null || echo 0)

  if [ "$SWIFT_FILE_COUNT" -gt 0 ]; then
    # Count test files to estimate test coverage
    TEST_FILE_COUNT=$(find . -name '*Test*.swift' -o -name '*Tests.swift' -o -name '*Spec.swift' \
      2>/dev/null | grep -c '.' || echo 0)
    TEST_FILE_COUNT=${TEST_FILE_COUNT//[^0-9]/}; TEST_FILE_COUNT=${TEST_FILE_COUNT:-0}

    # Analyze mutation surface: count mutable code points
    MUTATION_SURFACE=0
    for src_file in $SWIFT_SRC_FILES; do
      # Count conditional branches: if, guard, switch, while, for
      CONDITIONALS=$(grep -cE '\b(if |guard |switch |while |for )\b' "$src_file" 2>/dev/null || echo 0)
      CONDITIONALS=${CONDITIONALS//[^0-9]/}; CONDITIONALS=${CONDITIONALS:-0}
      # Count optionals: ??, optional chaining ?.
      OPTIONALS=$(grep -cE '(\?\?|\?\.)' "$src_file" 2>/dev/null || echo 0)
      OPTIONALS=${OPTIONALS//[^0-9]/}; OPTIONALS=${OPTIONALS:-0}
      # Count arithmetic operators that could be mutated: +, -, *, /
      ARITHMETIC=$(grep -cE '[^/+*-][+*/-][^/+*==-]' "$src_file" 2>/dev/null || echo 0)
      ARITHMETIC=${ARITHMETIC//[^0-9]/}; ARITHMETIC=${ARITHMETIC:-0}
      # Count boolean operators: &&, ||
      BOOLEANS=$(grep -cE '(&&|\|\|)' "$src_file" 2>/dev/null || echo 0)
      BOOLEANS=${BOOLEANS//[^0-9]/}; BOOLEANS=${BOOLEANS:-0}
      # Count comparison operators: ==, !=, >, <, >=, <=
      COMPARISONS=$(grep -cE '(==|!=|>=|<=)' "$src_file" 2>/dev/null || echo 0)
      COMPARISONS=${COMPARISONS//[^0-9]/}; COMPARISONS=${COMPARISONS:-0}

      FILE_POINTS=$((CONDITIONALS + OPTIONALS + ARITHMETIC + BOOLEANS + COMPARISONS))
      MUTATION_SURFACE=$((MUTATION_SURFACE + FILE_POINTS))
    done

    # Ensure minimum surface to avoid division by zero
    [ "$MUTATION_SURFACE" -lt 1 ] && MUTATION_SURFACE=1

    # Run tests and check if they pass
    TESTS_PASS=0
    echo "Running swift tests to validate baseline..." >&2
    if eval "$TEST_CMD" >/dev/null 2>&1; then
      TESTS_PASS=1
      echo "Swift tests pass ✓" >&2
    else
      echo "Swift tests fail ✗" >&2
    fi

    # Conservative kill rate estimate:
    # min(0.72, test_file_count / mutation_surface)
    # 0.72 cap = honest upper bound for AI estimation (we can't prove better)
    python3 - "$SWIFT_FILE_COUNT" "$TEST_FILE_COUNT" "$MUTATION_SURFACE" "$TESTS_PASS" <<'PYEOF'
import json, sys

src_count = int(sys.argv[1])
test_count = int(sys.argv[2])
mutation_surface = int(sys.argv[3])
tests_pass = int(sys.argv[4])

if tests_pass and mutation_surface > 0:
    # Conservative estimate: test coverage correlates loosely with mutation kill rate
    raw_rate = min(0.72, test_count / mutation_surface) if mutation_surface > 0 else 0.0
    kill_rate = round(raw_rate, 4)
else:
    kill_rate = 0.0

print(json.dumps({
    "language": "swift",
    "tool": "ai-estimated",
    "method": "ai-estimated",
    "confidence": "low",
    "sourceFiles": src_count,
    "testFiles": test_count,
    "mutationSurface": mutation_surface,
    "testsPass": bool(tests_pass),
    "killRate": round(kill_rate * 100, 1),
    "kill_rate": kill_rate,
    "verdict": "CAUTION",
    "note": "Swift mutation testing requires manual verification — no automated tool available. Kill rate is a conservative AI estimate based on code analysis, NOT mechanical mutation testing."
}))
PYEOF
    exit 0
  else
    echo '{"language":"swift","tool":"ai-estimated","error":"No Swift source files found","verdict":"SKIP"}'
    exit 0
  fi
fi

# ─── Baseline test check (non-Swift languages) ────────────────────────────
echo "Verifying baseline tests pass..." >&2
if ! eval "$TEST_CMD" >/dev/null 2>&1; then
  echo '{"error":"Baseline tests fail. Fix tests before mutation testing."}'
  exit 1
fi
echo "Baseline OK" >&2

# ─── Real mutation frameworks ──────────────────────────────────────────────

# JS/TS → Stryker
if ([ -f "tsconfig.json" ] || [ -f "package.json" ]) && ! [ -f "Cargo.toml" ]; then
  if npx stryker --version >/dev/null 2>&1 && [ -f "$SCRIPT_DIR/mutation-test-stryker.sh" ]; then
    echo "Using Stryker for mutation testing..." >&2
    OUTPUT=$("$SCRIPT_DIR/mutation-test-stryker.sh" "$PROJECT" 2>/dev/null || true)
    LAST_JSON=$(echo "$OUTPUT" | grep '^{' | tail -1)
    if [ -n "$LAST_JSON" ]; then
      echo "$LAST_JSON" | python3 -c "
import json,sys
d=json.load(sys.stdin)
d['tool']='stryker'
print(json.dumps(d))
" 2>/dev/null || echo "$LAST_JSON"
      exit 0
    fi
    echo "Stryker produced no JSON — falling back to AI mutations" >&2
  fi
fi

# Python → mutmut
if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ] || [ -f "setup.py" ]; then
  if command -v mutmut >/dev/null 2>&1; then
    echo "Using mutmut for Python mutation testing..." >&2
    mutmut run >/dev/null 2>&1 || true
    RESULTS=$(mutmut results 2>/dev/null || true)
    # mutmut outputs "Killed: N" or "Killed N out of M" — use case-insensitive grep
    KILLED=$(echo "$RESULTS" | grep -iE "^[[:space:]]*killed" | grep -oE '[0-9]+' | head -1 || echo 0)
    SURVIVED=$(echo "$RESULTS" | grep -iE "^[[:space:]]*(survived|missed)" | grep -oE '[0-9]+' | head -1 || echo 0)
    # Fallback: count individual mutation status lines
    if [ -z "$KILLED" ] || [ "$KILLED" = "0" ]; then
      KILLED=$(echo "$RESULTS" | grep -icE "\bkilled\b" || echo 0)
      SURVIVED=$(echo "$RESULTS" | grep -icE "\b(survived|missed)\b" || echo 0)
    fi
    KILLED=${KILLED:-0}
    SURVIVED=${SURVIVED:-0}
    TOTAL=$((KILLED + SURVIVED))
    if [ "$TOTAL" -gt 0 ]; then
      KILL_RATE=$(echo "scale=1; $KILLED * 100 / $TOTAL" | bc 2>/dev/null || echo "0")
      VERDICT=$(verdict_from_rate "$KILL_RATE")
      echo "{\"total\":$TOTAL,\"killed\":$KILLED,\"survived\":$SURVIVED,\"killRate\":$KILL_RATE,\"language\":\"py\",\"tool\":\"mutmut\",\"verdict\":\"$VERDICT\"}"
      exit 0
    fi
    echo "mutmut returned no results — falling back to AI mutations" >&2
  else
    echo "mutmut not installed. Install with: pip install mutmut" >&2
    echo "Falling back to AI mutations..." >&2
  fi
fi

# Rust → cargo-mutants
if [ -f "Cargo.toml" ]; then
  if command -v cargo-mutants >/dev/null 2>&1; then
    echo "Using cargo-mutants for Rust mutation testing..." >&2
    MUTANTS_OUT=$(cargo mutants --json 2>/dev/null || true)
    if [ -n "$MUTANTS_OUT" ] && echo "$MUTANTS_OUT" | python3 -c "import json,sys; json.load(sys.stdin)" >/dev/null 2>&1; then
      echo "$MUTANTS_OUT" | python3 -c "
import json,sys
data=json.load(sys.stdin)
killed=sum(1 for m in data if m.get('outcome')=='Caught')
survived=sum(1 for m in data if m.get('outcome')=='Missed')
total=killed+survived
rate=round(killed*100/total,1) if total>0 else 0
verdict='SHIP' if rate>=95 else ('CAUTION' if rate>=90 else 'BLOCKED')
print(json.dumps({'total':total,'killed':killed,'survived':survived,'killRate':rate,'language':'rs','tool':'cargo-mutants','verdict':verdict}))
"
      exit 0
    fi
    echo "cargo-mutants returned no usable JSON — falling back to AI mutations" >&2
  else
    echo "cargo-mutants not installed. Install with: cargo install cargo-mutants" >&2
    echo "Falling back to AI mutations..." >&2
  fi
fi

# Find source files
if [ -f "tsconfig.json" ] || ([ -f "package.json" ] && ! [ -f "Cargo.toml" ]); then
  LANG="ts"
  SRC_FILES=$(find . -name '*.ts' -not -name '*.test.*' -not -name '*.spec.*' \
    -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' \
    -not -name '*.d.ts' -not -path '*/tests/*' -not -path '*/__tests__/*' 2>/dev/null || true)
elif [ -f "Cargo.toml" ]; then
  LANG="rs"; SRC_FILES=$(find . -name '*.rs' -not -path '*/target/*' -not -path '*/.git/*' 2>/dev/null || true)
elif [ -f "go.mod" ]; then
  LANG="go"; SRC_FILES=$(find . -name '*.go' -not -name '*_test.go' -not -path '*/.git/*' 2>/dev/null || true)
elif [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then
  LANG="py"; SRC_FILES=$(find . -name '*.py' -not -name 'test_*' -not -name '*_test.py' -not -path '*/.git/*' -not -path '*/venv/*' 2>/dev/null || true)
else
  LANG="sh"; SRC_FILES=$(find . -name '*.sh' -not -path '*/.git/*' 2>/dev/null || true)
fi

FILE_COUNT=$(echo "$SRC_FILES" | grep -c '.' 2>/dev/null || echo 0)
echo "Found $FILE_COUNT source files ($LANG)" >&2

# Use temp files for counters (avoids subshell issues)
RESULTS_FILE=$(mktemp)
KILLED=0
SURVIVED=0
TOTAL=0
MAX_MUTATIONS=20

mutate_line() {
  local line="$1"
  if echo "$line" | grep -q '==='; then echo "$line" | sed 's/===/!==/'
  elif echo "$line" | grep -q '!=='; then echo "$line" | sed 's/!==/===/'
  elif echo "$line" | grep -q '>='; then echo "$line" | sed 's/>=/</'
  elif echo "$line" | grep -q '<='; then echo "$line" | sed 's/<=/>/g'
  elif echo "$line" | grep -q '&&'; then echo "$line" | sed 's/&&/||/'
  elif echo "$line" | grep -qF '||'; then echo "$line" | sed 's/||/\&\&/'
  elif echo "$line" | grep -q ' true'; then echo "$line" | sed 's/ true/ false/'
  elif echo "$line" | grep -q ' false'; then echo "$line" | sed 's/ false/ true/'
  elif echo "$line" | grep -q 'return '; then echo "$line" | sed 's/return /return undefined; \/\/ /'
  else echo "$line"
  fi
}

for file in $SRC_FILES; do
  [ "$TOTAL" -ge "$MAX_MUTATIONS" ] && break
  LINE_COUNT=$(wc -l < "$file" | tr -d ' ')
  [ "$LINE_COUNT" -lt 5 ] && continue

  CANDIDATES=$(grep -nE '(===|!==|>=|<=|&&|\|\|| true| false|return )' "$file" 2>/dev/null | head -5 || true)
  [ -z "$CANDIDATES" ] && continue

  cp "$file" "/tmp/wreckit-backup-$$"

  while IFS= read -r candidate; do
    [ "$TOTAL" -ge "$MAX_MUTATIONS" ] && break
    LINENUM=$(echo "$candidate" | cut -d: -f1)
    ORIGINAL=$(sed -n "${LINENUM}p" "$file")
    MUTATED=$(mutate_line "$ORIGINAL")
    [ "$ORIGINAL" = "$MUTATED" ] && continue

    # Apply mutation via awk
    awk -v ln="$LINENUM" -v rep="$MUTATED" 'NR==ln{print rep;next}{print}' "$file" > "/tmp/wreckit-mutated-$$"
    cp "/tmp/wreckit-mutated-$$" "$file"
    TOTAL=$((TOTAL + 1))

    if eval "$TEST_CMD" >/dev/null 2>&1; then
      SURVIVED=$((SURVIVED + 1))
      echo "  SURVIVED: ${file}:${LINENUM}" >> "$RESULTS_FILE"
    else
      KILLED=$((KILLED + 1))
      echo "  KILLED:   ${file}:${LINENUM}" >> "$RESULTS_FILE"
    fi

    cp "/tmp/wreckit-backup-$$" "$file"
  done <<< "$CANDIDATES"

  cp "/tmp/wreckit-backup-$$" "$file"
  rm -f "/tmp/wreckit-backup-$$" "/tmp/wreckit-mutated-$$"
done

if [ "$TOTAL" -eq 0 ]; then
  rm -f "$RESULTS_FILE"
  echo '{"error":"No mutatable lines found in source files."}'
  exit 1
fi

KILL_RATE=$(echo "scale=1; $KILLED * 100 / $TOTAL" | bc 2>/dev/null || echo "0")
VERDICT=$(verdict_from_rate "$KILL_RATE")

echo ""
echo "=== MUTATION TEST RESULTS ==="
cat "$RESULTS_FILE"
echo ""
echo "Total: $TOTAL | Killed: $KILLED | Survived: $SURVIVED"
echo "Kill rate: ${KILL_RATE}%"

cat <<EOF

{"total":$TOTAL,"killed":$KILLED,"survived":$SURVIVED,"killRate":$KILL_RATE,"language":"$LANG","tool":"ai-mutations","verdict":"$VERDICT"}
EOF

rm -f "$RESULTS_FILE"


path: scripts/perf-benchmark.sh
content: #!/usr/bin/env bash
# wreckit — Performance Benchmark Runner
# Usage: ./perf-benchmark.sh [project-path]
# Detects and runs benchmarks, compares vs baseline, flags regressions.
# Outputs structured JSON report.

set -euo pipefail

PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WRECKIT_DIR="$PROJECT/.wreckit"
BASELINE_FILE="$WRECKIT_DIR/perf-baseline.json"

log() { echo "  [perf-benchmark] $*" >&2; }

echo "================================================" >&2
echo "  wreckit Performance Benchmark Runner" >&2
echo "  Project: $PROJECT" >&2
echo "================================================" >&2

# ─── Detect stack ─────────────────────────────────────────────────────────────
STACK_JSON=$("$SCRIPT_DIR/detect-stack.sh" "$PROJECT" 2>/dev/null || echo "{}")
LANG=$(echo "$STACK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('language','unknown'))" 2>/dev/null || echo "unknown")

log "Language: $LANG"

# ─── State ─────────────────────────────────────────────────────────────────────
FRAMEWORK="none"
BENCHMARKS="[]"
REGRESSIONS="[]"
BASELINE_CREATED=false
VERDICT="WARN"
REGRESSION_COUNT=0
BENCH_COUNT=0

mkdir -p "$WRECKIT_DIR"

# ─── Phase 1: Detect benchmark framework ──────────────────────────────────────
log ""
log "Phase 1: Detecting benchmark framework..."

case "$LANG" in
  typescript|javascript)
    # Vitest bench
    if grep -rn --include="*.ts" --include="*.js" --include="*.bench.ts" \
      -E "^\s*bench\s*\(" "$PROJECT" 2>/dev/null \
      --exclude-dir=node_modules --exclude-dir=dist | grep -q .; then
      FRAMEWORK="vitest-bench"
      log "  Found: vitest bench() calls"
    # Jest bench
    elif grep -rn --include="*.ts" --include="*.js" \
      -E "describe\.bench|it\.bench" "$PROJECT" 2>/dev/null \
      --exclude-dir=node_modules | grep -q .; then
      FRAMEWORK="jest-bench"
      log "  Found: jest bench"
    fi
    ;;
  python)
    # pytest-benchmark
    if grep -rn --include="*.py" \
      -E "def test_.*\(.*benchmark\|benchmark\.pedantic\|benchmark\.calibrate" \
      "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="pytest-benchmark"
      log "  Found: pytest-benchmark"
    # timeit-based
    elif grep -rn --include="*.py" \
      -E "import timeit|from timeit" "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="timeit"
      log "  Found: timeit usage"
    fi
    ;;
  rust)
    if grep -rn --include="*.rs" -E "#\[bench\]|test::Bencher" "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="cargo-bench"
      log "  Found: cargo bench (#[bench])"
    fi
    ;;
  go)
    if grep -rn --include="*.go" -E "func Benchmark" "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="go-bench"
      log "  Found: go test -bench"
    fi
    ;;
  *)
    log "  Unknown language: $LANG"
    ;;
esac

# Check for k6 load test
if [ -f "$PROJECT/k6.js" ] || [ -f "$PROJECT/load-test.js" ] || \
   find "$PROJECT" -name "*.k6.js" 2>/dev/null | head -1 | grep -q .; then
  FRAMEWORK="k6"
  log "  Found: k6 load test scripts"
fi

if [ "$FRAMEWORK" = "none" ]; then
  log "  No benchmark framework detected"
fi

# ─── Phase 2: Run benchmarks ──────────────────────────────────────────────────
log ""
log "Phase 2: Running benchmarks..."

cd "$PROJECT"

BENCH_OUTPUT=""
BENCH_EXIT=0

case "$FRAMEWORK" in
  vitest-bench)
    if command -v npx >/dev/null 2>&1; then
      log "  Running: npx vitest bench --reporter=json"
      BENCH_OUTPUT=$(npx vitest bench --reporter=json 2>&1 || true)
      BENCH_COUNT=$(echo "$BENCH_OUTPUT" | python3 -c "
import json,sys
try:
    d = json.load(sys.stdin)
    print(sum(len(s.get('benchmarks',[])) for s in d.get('testSuites',[])))
except:
    print(0)
" 2>/dev/null || echo "0")
      log "  Ran $BENCH_COUNT benchmarks"
    else
      log "  npx not available"
    fi
    ;;
    
  pytest-benchmark)
    if command -v pytest >/dev/null 2>&1 || command -v python3 >/dev/null 2>&1; then
      log "  Running: pytest --benchmark-only --benchmark-json=/tmp/wreckit-bench.json"
      pytest --benchmark-only --benchmark-json=/tmp/wreckit-bench.json 2>&1 || true
      
      if [ -f /tmp/wreckit-bench.json ]; then
        BENCH_OUTPUT=$(cat /tmp/wreckit-bench.json)
        BENCHMARKS=$(python3 -c "
import json,sys
d=json.load(open('/tmp/wreckit-bench.json'))
results = []
for b in d.get('benchmarks', []):
    results.append({
        'name': b['name'],
        'mean_ms': round(b['stats']['mean'] * 1000, 4),
        'min_ms': round(b['stats']['min'] * 1000, 4),
        'max_ms': round(b['stats']['max'] * 1000, 4),
        'rounds': b['stats']['rounds']
    })
print(json.dumps(results))
" 2>/dev/null || echo "[]")
        BENCH_COUNT=$(echo "$BENCHMARKS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))")
        log "  Ran $BENCH_COUNT benchmarks"
        rm -f /tmp/wreckit-bench.json
      fi
    fi
    ;;
    
  cargo-bench)
    if command -v cargo >/dev/null 2>&1; then
      log "  Running: cargo bench"
      BENCH_OUTPUT=$(cargo bench 2>&1 || true)
      
      # Parse cargo bench output: "test bench_name ... bench: NNN ns/iter (+/- N)"
      BENCHMARKS=$(echo "$BENCH_OUTPUT" | python3 -c "
import sys, re, json
output = sys.stdin.read()
pattern = r'test (\S+)\s+\.\.\. bench:\s+([\d,]+) ns/iter'
results = []
for m in re.finditer(pattern, output):
    ns = int(m.group(2).replace(',',''))
    results.append({
        'name': m.group(1),
        'mean_ms': round(ns / 1000000, 4),
        'unit': 'ns/iter'
    })
print(json.dumps(results))
" 2>/dev/null || echo "[]")
      BENCH_COUNT=$(echo "$BENCHMARKS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" || echo "0")
      log "  Ran $BENCH_COUNT benchmarks"
    fi
    ;;
    
  go-bench)
    if command -v go >/dev/null 2>&1; then
      log "  Running: go test -bench=. -benchmem -count=3"
      BENCH_OUTPUT=$(go test -bench=. -benchmem -count=3 ./... 2>&1 || true)
      
      # Parse go bench output: "BenchmarkName-N   NNN   NNN ns/op   NNN B/op   N allocs/op"
      BENCHMARKS=$(echo "$BENCH_OUTPUT" | python3 -c "
import sys, re, json
output = sys.stdin.read()
# Go bench: BenchmarkFoo-8   100   12345 ns/op   1234 B/op   12 allocs/op
pattern = r'(Benchmark\S+)\s+\d+\s+([\d]+)\s+ns/op(?:\s+([\d]+)\s+B/op)?'
results = {}
for m in re.finditer(pattern, output):
    name = m.group(1)
    ns = int(m.group(2))
    bop = int(m.group(3)) if m.group(3) else 0
    if name not in results:
        results[name] = []
    results[name].append(ns)

output_list = []
for name, times in results.items():
    mean_ns = sum(times) / len(times)
    output_list.append({
        'name': name,
        'mean_ms': round(mean_ns / 1000000, 4),
        'runs': len(times)
    })
print(json.dumps(output_list))
" 2>/dev/null || echo "[]")
      BENCH_COUNT=$(echo "$BENCHMARKS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" || echo "0")
      log "  Ran $BENCH_COUNT benchmarks"
    fi
    ;;
    
  none)
    log "  No benchmark framework — running basic timing test"
    
    # Time the test suite itself as a proxy
    if [ -n "$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commands',{}).get('test',''))" 2>/dev/null)" ]; then
      TEST_CMD=$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commands',{}).get('test',''))" 2>/dev/null)
      
      log "  Timing test suite: $TEST_CMD"
      START=$(python3 -c "import time; print(time.time())")
      bash -c "$TEST_CMD" > /dev/null 2>&1 || true
      END=$(python3 -c "import time; print(time.time())")
      DURATION=$(python3 -c "print(round($END - $START, 2))")
      
      log "  Test suite duration: ${DURATION}s"
      BENCHMARKS=$(python3 -c "
import json
print(json.dumps([{
    'name': 'full_test_suite',
    'mean_ms': float('$DURATION') * 1000,
    'note': 'Total test suite duration (no granular benchmarks available)'
}]))
")
      BENCH_COUNT=1
    fi
    ;;
esac

cd "$OLDPWD" 2>/dev/null || true

# ─── Phase 3: Compare vs baseline ─────────────────────────────────────────────
log ""
log "Phase 3: Comparing vs baseline..."

if [ -f "$BASELINE_FILE" ] && [ "$BENCH_COUNT" -gt 0 ]; then
  log "  Baseline found: $BASELINE_FILE"
  
  COMPARISON=$(python3 << PYEOF
import json

current = json.loads('$BENCHMARKS') if '$BENCHMARKS' != '[]' else []
with open('$BASELINE_FILE') as f:
    baseline_data = json.load(f)
baseline_list = baseline_data.get('benchmarks', [])
baseline_map = {b['name']: b['mean_ms'] for b in baseline_list}

regressions = []
updated = []

for bench in current:
    name = bench['name']
    current_ms = bench['mean_ms']
    
    if name in baseline_map:
        baseline_ms = baseline_map[name]
        if baseline_ms > 0:
            delta_pct = ((current_ms - baseline_ms) / baseline_ms) * 100
        else:
            delta_pct = 0
        
        regression = delta_pct > 20
        severe = delta_pct > 50
        
        bench_with_comparison = {
            **bench,
            'baseline_ms': baseline_ms,
            'delta_pct': round(delta_pct, 1),
            'regression': regression,
            'severe_regression': severe
        }
        updated.append(bench_with_comparison)
        
        if regression:
            regressions.append({
                'name': name,
                'delta_pct': round(delta_pct, 1),
                'current_ms': current_ms,
                'baseline_ms': baseline_ms,
                'severe': severe
            })
    else:
        bench['baseline_ms'] = None
        bench['delta_pct'] = None
        bench['regression'] = False
        updated.append(bench)

print(json.dumps({'benchmarks': updated, 'regressions': regressions}))
PYEOF
)
  
  BENCHMARKS=$(echo "$COMPARISON" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['benchmarks']))" || echo "$BENCHMARKS")
  REGRESSIONS=$(echo "$COMPARISON" | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['regressions']))" || echo "[]")
  REGRESSION_COUNT=$(echo "$REGRESSIONS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" || echo "0")
  
  if [ "$REGRESSION_COUNT" -gt 0 ]; then
    SEVERE=$(echo "$REGRESSIONS" | python3 -c "import json,sys; d=json.load(sys.stdin); print(any(r['severe'] for r in d))" || echo "False")
    if [ "$SEVERE" = "True" ]; then
      VERDICT="FAIL"
    else
      VERDICT="WARN"
    fi
    log "  ⚠️ $REGRESSION_COUNT regressions detected"
  else
    VERDICT="PASS"
    log "  ✅ No regressions vs baseline"
  fi
else
  log "  No baseline found — creating new baseline"
  BASELINE_CREATED=true
  
  if [ "$BENCH_COUNT" -gt 0 ]; then
    python3 << PYEOF
import json
data = {
    "benchmarks": json.loads('$BENCHMARKS') if '$BENCHMARKS' != '[]' else []
}
with open('$BASELINE_FILE', 'w') as f:
    json.dump(data, f, indent=2)
print(f"Baseline written: $BASELINE_FILE")
PYEOF
    VERDICT="PASS"
    log "  Baseline saved to $BASELINE_FILE"
  else
    VERDICT="WARN"
    log "  No benchmarks to save as baseline"
  fi
fi

# ─── Output report ─────────────────────────────────────────────────────────────
log ""
log "Building report..."

python3 << PYEOF
import json

baseline_created = "$BASELINE_CREATED" == "true"
benchmarks = json.loads('$BENCHMARKS') if '$BENCHMARKS' not in ('', '[]') else []
regressions = json.loads('$REGRESSIONS') if '$REGRESSIONS' not in ('', '[]') else []

report = {
    "project": "$PROJECT",
    "scanner": "wreckit-perf-benchmark",
    "language": "$LANG",
    "framework": "$FRAMEWORK",
    "benchmarks_run": $BENCH_COUNT,
    "benchmarks": benchmarks,
    "regressions": regressions,
    "regression_count": $REGRESSION_COUNT,
    "baseline_created": baseline_created,
    "baseline_path": "$BASELINE_FILE" if $BENCH_COUNT > 0 else None,
    "verdict": "$VERDICT"
}

print(json.dumps(report, indent=2))
PYEOF

echo "" >&2
echo "Results:" >&2
echo "  Framework: $FRAMEWORK" >&2
echo "  Benchmarks: $BENCH_COUNT" >&2
echo "  Regressions: $REGRESSION_COUNT" >&2
echo "  Verdict: $VERDICT" >&2


path: scripts/project-type.sh
content: #!/usr/bin/env bash
# wreckit — Project type classifier
# Usage: ./project-type.sh [project-path]
# Output: JSON with type, confidence, signals, and calibration profile

set -euo pipefail
PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
cd "$PROJECT"

# Helpers
has_file() {
  local pattern="$1"
  compgen -G "$pattern" >/dev/null 2>&1
}

json_get_pkg() {
  local expr="$1"
  if [ -f package.json ]; then
    python3 - <<PYEOF
import json
with open('package.json','r',encoding='utf-8') as f:
    d=json.load(f)
print($expr)
PYEOF
  fi
}

safe_int() {
  local v="$1"
  if [[ "$v" =~ ^[0-9]+$ ]]; then
    echo "$v"
  else
    echo "0"
  fi
}

# ─── Detect signals ─────────────────────────────────────────────────────────

# package.json signals
PKG_HAS_EXPORTS="false"
PKG_MAIN=""
PKG_MAIN_DIST="false"
PKG_MAIN_APP="false"
PKG_EXPORTS_DIST="false"
if [ -f package.json ]; then
  PKG_HAS_EXPORTS=$(json_get_pkg "'true' if 'exports' in d else 'false'")
  PKG_MAIN=$(json_get_pkg "d.get('main','')")
  if echo "$PKG_MAIN" | grep -q "dist/"; then
    PKG_MAIN_DIST="true"
  fi
  if echo "$PKG_MAIN" | grep -Eq "(^|/)(src/)?(app|pages)(/|$)"; then
    PKG_MAIN_APP="true"
  fi
  if python3 - <<'PYEOF' >/dev/null 2>&1
import json, sys
with open('package.json','r',encoding='utf-8') as f:
    d=json.load(f)
exp=d.get('exports',{})
vals=[]
if isinstance(exp,str):
    vals=[exp]
elif isinstance(exp,dict):
    for v in exp.values():
        if isinstance(v,str):
            vals.append(v)
        elif isinstance(v,dict):
            vals.extend([x for x in v.values() if isinstance(x,str)])
sys.exit(0 if any('dist/' in v for v in vals) else 1)
PYEOF
  then
    PKG_EXPORTS_DIST="true"
  fi
fi

HAS_DIST_DIR="false"
HAS_LIB_DIR="false"
[ -d "dist" ] && HAS_DIST_DIR="true"
[ -d "lib" ] && HAS_LIB_DIR="true"

HAS_APP_DIR="false"
[ -d "src/app" ] && HAS_APP_DIR="true"
[ -d "src/pages" ] && HAS_APP_DIR="true"

HAS_APP_CONFIG="false"
if has_file "next.config.*" || has_file "vite.config.*"; then
  HAS_APP_CONFIG="true"
fi

HAS_ENV_FILE="false"
if find . -maxdepth 1 -name ".env*" 2>/dev/null | head -1 | grep -q .; then
  HAS_ENV_FILE="true"
fi

# AI-generated signals
GIT_IS_SHALLOW="false"
[ -f ".git/shallow" ] && GIT_IS_SHALLOW="true"
git rev-parse --is-shallow-repository 2>/dev/null | grep -q "true" && GIT_IS_SHALLOW="true"

GIT_COMMIT_COUNT=0
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  GIT_COMMIT_COUNT=$(git log --oneline 2>/dev/null | wc -l | tr -d ' ')
fi
GIT_COMMIT_COUNT=$(safe_int "$GIT_COMMIT_COUNT")

PROJECT_AGE_DAYS=9999
if [ "$GIT_IS_SHALLOW" = "false" ] && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
  FIRST_COMMIT_TS=$(git log --reverse --format=%ct 2>/dev/null | head -1 || true)
  if [[ "$FIRST_COMMIT_TS" =~ ^[0-9]+$ ]]; then
    NOW_TS=$(date +%s)
    PROJECT_AGE_DAYS=$(( (NOW_TS - FIRST_COMMIT_TS) / 86400 ))
  fi
else
  # Fallback: oldest file mtime
  OLDEST_TS=$(python3 - <<'PYEOF'
import os, time
old=None
for root, dirs, files in os.walk('.'):
    if '/.git' in root or '/node_modules' in root or '/dist' in root or '/build' in root or '/.wreckit' in root:
        continue
    for f in files:
        p=os.path.join(root,f)
        try:
            m=os.path.getmtime(p)
            if old is None or m < old:
                old=m
        except Exception:
            pass
print(int(old) if old else 0)
PYEOF
  )
  if [[ "$OLDEST_TS" =~ ^[0-9]+$ ]] && [ "$OLDEST_TS" -gt 0 ]; then
    NOW_TS=$(date +%s)
    PROJECT_AGE_DAYS=$(( (NOW_TS - OLDEST_TS) / 86400 ))
  fi
fi

TEST_FILE_COUNT=$(find . \( -name '*test*' -o -name '*spec*' -o -path '*/__tests__/*' -o -path '*/tests/*' \) \
  -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/dist/*' -not -path '*/build/*' 2>/dev/null | wc -l | tr -d ' ')
TEST_FILE_COUNT=$(safe_int "$TEST_FILE_COUNT")

# Monorepo signals
HAS_PNPM_WORKSPACE="false"
HAS_NX="false"
HAS_TURBO="false"
[ -f "pnpm-workspace.yaml" ] && HAS_PNPM_WORKSPACE="true"
[ -f "nx.json" ] && HAS_NX="true"
[ -f "turbo.json" ] && HAS_TURBO="true"

PACKAGES_PKG_COUNT=0
if [ -d "packages" ]; then
  PACKAGES_PKG_COUNT=$(find packages -mindepth 1 -maxdepth 3 -name package.json 2>/dev/null | wc -l | tr -d ' ')
fi
PACKAGES_PKG_COUNT=$(safe_int "$PACKAGES_PKG_COUNT")
HAS_MULTI_PACKAGES="false"
if [ "$PACKAGES_PKG_COUNT" -ge 2 ] 2>/dev/null; then
  HAS_MULTI_PACKAGES="true"
fi

# ─── Score each type ─────────────────────────────────────────────────────────

LIB_SCORE=0
LIB_SIGNALS=()
if [ "$PKG_HAS_EXPORTS" = "true" ]; then
  LIB_SCORE=$((LIB_SCORE + 30))
  LIB_SIGNALS+=("package.json exports present")
fi
if [ "$PKG_MAIN_DIST" = "true" ] || [ "$PKG_EXPORTS_DIST" = "true" ]; then
  LIB_SCORE=$((LIB_SCORE + 25))
  LIB_SIGNALS+=("package.json main/exports point to dist/")
fi
if [ "$HAS_DIST_DIR" = "true" ] || [ "$HAS_LIB_DIR" = "true" ]; then
  LIB_SCORE=$((LIB_SCORE + 15))
  LIB_SIGNALS+=("dist/ or lib/ directory present")
fi
if [ "$HAS_APP_DIR" = "false" ] && [ "$HAS_APP_CONFIG" = "false" ] && [ "$PKG_MAIN_APP" = "false" ]; then
  LIB_SCORE=$((LIB_SCORE + 20))
  LIB_SIGNALS+=("no app entrypoints detected")
fi

APP_SCORE=0
APP_SIGNALS=()
if [ "$HAS_APP_CONFIG" = "true" ]; then
  APP_SCORE=$((APP_SCORE + 30))
  APP_SIGNALS+=("next/vite config detected")
fi
if [ "$HAS_APP_DIR" = "true" ]; then
  APP_SCORE=$((APP_SCORE + 25))
  APP_SIGNALS+=("src/app or src/pages present")
fi
if [ "$HAS_ENV_FILE" = "true" ]; then
  APP_SCORE=$((APP_SCORE + 15))
  APP_SIGNALS+=(".env file present")
fi
if [ "$PKG_MAIN_APP" = "true" ]; then
  APP_SCORE=$((APP_SCORE + 20))
  APP_SIGNALS+=("package.json main points to app")
fi

AI_SCORE=0
AI_SIGNALS=()
if [ "$GIT_IS_SHALLOW" = "false" ] && [ "$GIT_COMMIT_COUNT" -gt 0 ] && [ "$GIT_COMMIT_COUNT" -lt 10 ]; then
  AI_SCORE=$((AI_SCORE + 40))
  AI_SIGNALS+=("git history under 10 commits")
fi
if [ "$PROJECT_AGE_DAYS" -lt 30 ] 2>/dev/null; then
  AI_SCORE=$((AI_SCORE + 30))
  AI_SIGNALS+=("project age under 30 days")
fi
if [ "$TEST_FILE_COUNT" -eq 0 ]; then
  AI_SCORE=$((AI_SCORE + 30))
  AI_SIGNALS+=("no test files detected")
fi

MONO_SCORE=0
MONO_SIGNALS=()
if [ "$HAS_PNPM_WORKSPACE" = "true" ]; then
  MONO_SCORE=$((MONO_SCORE + 40))
  MONO_SIGNALS+=("pnpm-workspace.yaml present")
fi
if [ "$HAS_NX" = "true" ]; then
  MONO_SCORE=$((MONO_SCORE + 30))
  MONO_SIGNALS+=("nx.json present")
fi
if [ "$HAS_TURBO" = "true" ]; then
  MONO_SCORE=$((MONO_SCORE + 30))
  MONO_SIGNALS+=("turbo.json present")
fi
if [ "$HAS_MULTI_PACKAGES" = "true" ]; then
  MONO_SCORE=$((MONO_SCORE + 40))
  MONO_SIGNALS+=("multiple packages/ package.json files")
fi

# ─── Select best type ────────────────────────────────────────────────────────

TYPE="unknown"
CONF=0.0
SIGNALS=()

BEST_SCORE=$LIB_SCORE
TYPE="library"
SIGNALS=("${LIB_SIGNALS[@]}")
MAX_SCORE=90

if [ "$APP_SCORE" -gt "$BEST_SCORE" ]; then
  BEST_SCORE=$APP_SCORE
  TYPE="app"
  SIGNALS=("${APP_SIGNALS[@]}")
  MAX_SCORE=90
fi
if [ "$AI_SCORE" -gt "$BEST_SCORE" ]; then
  BEST_SCORE=$AI_SCORE
  TYPE="ai-generated"
  SIGNALS=("${AI_SIGNALS[@]}")
  MAX_SCORE=100
fi
if [ "$MONO_SCORE" -gt "$BEST_SCORE" ]; then
  BEST_SCORE=$MONO_SCORE
  TYPE="monorepo"
  SIGNALS=("${MONO_SIGNALS[@]}")
  MAX_SCORE=100
fi

if [ "$BEST_SCORE" -eq 0 ]; then
  TYPE="unknown"
  SIGNALS=("no strong signals detected")
  MAX_SCORE=100
fi

CONF=$(python3 - <<PYEOF
score=$BEST_SCORE
max_score=$MAX_SCORE
conf=0.0 if max_score<=0 else round(min(1.0, score/max_score), 2)
print(conf)
PYEOF
)

# ─── Calibration profiles ───────────────────────────────────────────────────

SLOP_PER_KLOC=5
GOD_MODULE_FANIN=10
CI_REQUIRED="false"
COVERAGE_MIN=70
SKIP_GATES=( )
TOLERATED_WARNS=( )

case "$TYPE" in
  library)
    SLOP_PER_KLOC=8
    GOD_MODULE_FANIN=20
    CI_REQUIRED="false"
    COVERAGE_MIN=70
    SKIP_GATES=("ralph_loop")
    TOLERATED_WARNS=("ci_integration")
    ;;
  app)
    SLOP_PER_KLOC=3
    GOD_MODULE_FANIN=10
    CI_REQUIRED="true"
    COVERAGE_MIN=60
    ;;
  ai-generated)
    SLOP_PER_KLOC=1
    GOD_MODULE_FANIN=5
    CI_REQUIRED="true"
    COVERAGE_MIN=80
    ;;
  monorepo)
    SLOP_PER_KLOC=8
    GOD_MODULE_FANIN=25
    CI_REQUIRED="false"
    COVERAGE_MIN=60
    ;;
  unknown)
    SLOP_PER_KLOC=5
    GOD_MODULE_FANIN=10
    CI_REQUIRED="false"
    COVERAGE_MIN=70
    ;;
  *)
    ;;
esac

# ─── Output JSON ─────────────────────────────────────────────────────────────

# Join arrays into pipe-delimited strings for safe passing to Python
SIGNALS_STR=$(printf '%s|' "${SIGNALS[@]+"${SIGNALS[@]}"}" | sed 's/|$//')
SKIP_STR=$(printf '%s|' "${SKIP_GATES[@]+"${SKIP_GATES[@]}"}" | sed 's/|$//')
TOLERATED_STR=$(printf '%s|' "${TOLERATED_WARNS[@]+"${TOLERATED_WARNS[@]}"}" | sed 's/|$//')

python3 - "$TYPE" "$CONF" "$SIGNALS_STR" "$SLOP_PER_KLOC" "$GOD_MODULE_FANIN" "$CI_REQUIRED" "$COVERAGE_MIN" "$SKIP_STR" "$TOLERATED_STR" <<'PYEOF'
import json, sys

_type, conf, signals_str, slop, fanin, ci_req, cov, skip_str, tolerated_str = sys.argv[1:]

signals = [s for s in signals_str.split("|") if s] if signals_str else []
skip = [s for s in skip_str.split("|") if s] if skip_str else []
tolerated = [s for s in tolerated_str.split("|") if s] if tolerated_str else []

print(json.dumps({
    "type": _type,
    "confidence": round(float(conf), 2),
    "signals": signals,
    "calibration": {
        "slop_per_kloc": int(slop),
        "god_module_fanin": int(fanin),
        "ci_required": ci_req == "true",
        "coverage_min": int(cov),
        "skip_gates": skip,
        "tolerated_warns": tolerated
    }
}))
PYEOF


path: scripts/proof-bundle.sh
content: #!/usr/bin/env bash
# wreckit — Proof Bundle writer
# Reads gate results from stdin (JSON array) and writes .wreckit/ folder
# Usage: cat gate-results.json | ./proof-bundle.sh [project-path] [mode] [verdict]
# Output: .wreckit/proof.json, .wreckit/dashboard.json, .wreckit/decision.md

set -euo pipefail
PROJECT="${1:-.}"
MODE="${2:-AUDIT}"
VERDICT="${3:-}"

mkdir -p "$PROJECT/.wreckit"

GATES_INPUT_FILE="$(mktemp)"
cat > "$GATES_INPUT_FILE"

python3 - "$PROJECT" "$MODE" "$VERDICT" "$GATES_INPUT_FILE" <<'PYEOF'
import datetime
import json
import os
import subprocess
import sys
import uuid
from typing import Any, Dict, List, Optional, Tuple

project, mode, verdict_arg, gates_input_file = sys.argv[1:]
try:
    with open(gates_input_file, "r", encoding="utf-8") as fh:
        raw = fh.read().strip()
except Exception:
    raw = ""

if not raw:
    gates_input = []
else:
    try:
        gates_input = json.loads(raw)
    except json.JSONDecodeError:
        print(json.dumps({"error": "Invalid JSON on stdin"}))
        sys.exit(1)

if not isinstance(gates_input, list):
    print(json.dumps({"error": "Gate results must be a JSON array"}))
    sys.exit(1)

now = datetime.datetime.now(datetime.UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
run_id = str(uuid.uuid4())

try:
    git_sha = subprocess.check_output(["git", "-C", project, "rev-parse", "HEAD"], stderr=subprocess.DEVNULL).decode().strip()
except Exception:
    git_sha = None


def _to_float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value)
    except Exception:
        return default


def _to_int(value: Any, default: int = 0) -> int:
    try:
        return int(value)
    except Exception:
        return default


def _normalize_status(value: Any) -> str:
    status_up = str(value or "UNKNOWN").upper()
    if status_up == "SKIPPED":
        return "SKIP"
    if status_up == "BLOCKED":
        return "FAIL"
    if status_up == "SHIP":
        return "PASS"
    return status_up


def _load_project_profile(project_path: str) -> Dict[str, Any]:
    fallback = {
        "type": "unknown",
        "confidence": 0.0,
        "signals": ["profile_missing"],
        "calibration": {
            "slop_per_kloc": 5,
            "god_module_fanin": 10,
            "ci_required": False,
            "coverage_min": 70,
            "skip_gates": [],
            "tolerated_warns": [],
        },
    }

    candidates = []
    env_path = os.environ.get("WRECKIT_PROJECT_PROFILE_FILE")
    if env_path:
        candidates.append(env_path)
    candidates.append(os.path.join(project_path, ".wreckit", "project-type.json"))

    for c in candidates:
        if not c:
            continue
        try:
            with open(c, "r", encoding="utf-8") as f:
                d = json.load(f)
            if isinstance(d, dict) and "type" in d:
                return d
        except Exception:
            continue
    return fallback


project_profile = _load_project_profile(project)
project_type = str(project_profile.get("type", "unknown"))
calibration = project_profile.get("calibration", {}) if isinstance(project_profile.get("calibration", {}), dict) else {}

# Normalize gate results
gates: List[Dict[str, Any]] = []
normalized: Dict[str, Dict[str, Any]] = {}
mutation_rate: Optional[float] = None

for item in gates_input:
    if not isinstance(item, dict):
        continue

    name = item.get("gate") or item.get("name") or item.get("id") or "unknown"
    status = _normalize_status(item.get("status") or item.get("verdict") or item.get("result"))
    confidence = _to_float(item.get("confidence", 0.0), 0.0)

    row = {
        "gate": name,
        "status": status,
        "confidence": confidence,
        "raw": item,
    }
    gates.append(row)
    normalized[name] = {
        "verdict": status,
        "score": item.get("score"),
        "confidence": confidence,
        "raw": item,
    }

    if name.lower() in {"mutation", "mutation_kill", "mutation-kill", "mutation_test", "mutationtest"}:
        rate = item.get("killRate") or item.get("kill_rate") or item.get("mutation_kill") or item.get("mutation_kill_rate")
        if rate is not None:
            parsed = _to_float(rate, -1.0)
            if parsed >= 0.0:
                mutation_rate = parsed

mutation_rate_fraction = None
if mutation_rate is not None:
    mutation_rate_fraction = mutation_rate / 100.0 if mutation_rate > 1.5 else mutation_rate


def gate_by_name(name: str) -> Optional[Dict[str, Any]]:
    for g in gates:
        if g.get("gate") == name:
            return g
    return None


BASE_WEIGHTS = {
    "check_deps": 1.2,
    "slop_scan": 1.0,
    "type_check": 1.1,
    "coverage_stats": 0.8,
    "red_team": 1.0,
    "regex_complexity": 0.7,
    "design_review": 0.9,
    "ci_integration": 0.8,
    "ralph_loop": 0.8,
    "mutation_test": 1.1,
}

TYPE_DELTAS = {
    "library": {"type_check": 0.35, "design_review": 0.2, "ci_integration": -0.2},
    "app": {"ci_integration": 0.35, "red_team": 0.25, "coverage_stats": 0.2},
    "ai-generated": {"check_deps": 0.35, "slop_scan": 0.4, "type_check": 0.25},
    "monorepo": {"design_review": 0.35, "check_deps": 0.2, "ci_integration": 0.2},
}

GATE_DOMAINS = {
    "check_deps": "dependencies",
    "slop_scan": "code_quality",
    "type_check": "correctness",
    "coverage_stats": "testing",
    "mutation_test": "testing",
    "red_team": "security",
    "regex_complexity": "security",
    "design_review": "architecture",
    "ci_integration": "delivery",
    "ralph_loop": "planning",
}


def weight_for_gate(gate_name: str) -> float:
    base = BASE_WEIGHTS.get(gate_name, 0.9)
    delta = TYPE_DELTAS.get(project_type, {}).get(gate_name, 0.0)
    weight = base + delta
    return max(0.3, min(2.0, round(weight, 3)))


def domain_for_gate(gate_name: str) -> str:
    return GATE_DOMAINS.get(gate_name, "misc")


hard_blocks: List[str] = []
hard_block_reasons: List[str] = []
hard_block_confs: List[float] = []


def add_hard_block(gate: str, reason: str, confidence: float) -> None:
    if gate not in hard_blocks:
        hard_blocks.append(gate)
    hard_block_reasons.append(reason)
    hard_block_confs.append(max(0.0, min(1.0, confidence)))


check_deps_gate = gate_by_name("check_deps")
if check_deps_gate and check_deps_gate.get("status") == "FAIL":
    raw_deps = check_deps_gate.get("raw", {})
    findings = _to_int(raw_deps.get("findings", 0), 0)
    hallucinated = raw_deps.get("hallucinated", [])
    confirmed = findings > 0 or (isinstance(hallucinated, list) and len(hallucinated) > 0)
    if confirmed and check_deps_gate.get("confidence", 0.0) >= 0.9:
        add_hard_block("check_deps", "Critical: hallucinated dependency confirmed", check_deps_gate.get("confidence", 1.0))

type_check_gate = gate_by_name("type_check")
if type_check_gate and type_check_gate.get("status") == "FAIL":
    errors = _to_int(type_check_gate.get("raw", {}).get("errors", 0), 0)
    if errors > 0:
        add_hard_block("type_check", f"Critical: type_check reports {errors} error(s)", max(type_check_gate.get("confidence", 1.0), 0.95))

slop_gate = gate_by_name("slop_scan")
if slop_gate and slop_gate.get("status") == "FAIL":
    raw_slop = slop_gate.get("raw", {})
    density = raw_slop.get("density_per_kloc")
    threshold = raw_slop.get("threshold_per_kloc")
    density = _to_float(density, -1.0)
    threshold = _to_float(threshold, -1.0)
    if density >= 0 and threshold > 0 and density > (3.0 * threshold):
        add_hard_block("slop_scan", f"Critical: slop density {density:.2f}/KLOC > 3x threshold {threshold:.2f}", max(slop_gate.get("confidence", 1.0), 0.95))

# Evidence aggregation for non-hard failures
soft_fails: List[Dict[str, Any]] = []
warn_high: List[Dict[str, Any]] = []

for g in gates:
    name = g.get("gate", "unknown")
    status = g.get("status")
    conf = max(0.0, min(1.0, _to_float(g.get("confidence", 0.0), 0.0)))

    if status == "FAIL" and name in hard_blocks:
        continue

    if status in {"FAIL", "ERROR"}:
        confidence_used = conf if conf > 0 else 0.6
        weight = weight_for_gate(name)
        soft_fails.append({
            "gate": name,
            "status": status,
            "domain": domain_for_gate(name),
            "confidence": confidence_used,
            "weight": weight,
            "evidence": round(confidence_used * weight, 4),
        })
    elif status == "WARN" and conf >= 0.5:
        warn_high.append(g)

mutation_warnings: List[str] = []
if mutation_rate is not None:
    if mutation_rate < 90:
        confidence_used = 0.95
        weight = weight_for_gate("mutation_test")
        soft_fails.append({
            "gate": "mutation_test",
            "status": "FAIL",
            "domain": domain_for_gate("mutation_test"),
            "confidence": confidence_used,
            "weight": weight,
            "evidence": round(confidence_used * weight, 4),
            "reason": f"mutation kill rate {mutation_rate:.1f}% (<90)",
        })
    elif mutation_rate < 95:
        mutation_warnings.append(f"mutation kill rate {mutation_rate:.1f}% (90-94 caution)")

corroborating_signals = len(soft_fails)
weighted_evidence = round(sum(_to_float(x.get("evidence", 0.0), 0.0) for x in soft_fails), 4)
corroborating_domains = sorted({str(x.get("domain", "misc")) for x in soft_fails})
corroborating_domain_count = len(corroborating_domains)
avg_soft_confidence = round((sum(_to_float(x.get("confidence", 0.0), 0.0) for x in soft_fails) / corroborating_signals), 4) if corroborating_signals > 0 else 0.0

# Count status buckets
passed = warned = failed = skipped = 0
for g in gates:
    verdict = g.get("status")
    if verdict == "PASS":
        passed += 1
    elif verdict == "WARN":
        warned += 1
    elif verdict in {"FAIL", "ERROR"}:
        failed += 1
    elif verdict == "SKIP":
        skipped += 1

executed_gates = passed + warned + failed
dissenting_signals = max(0, executed_gates - corroborating_signals - len(hard_blocks))

blockers: List[str] = []
warnings: List[str] = []
verdict_reasoning = ""
verdict_confidence = 1.0

mode_up = str(mode or "AUDIT").upper()
mutation_expected = mode_up in {"BUILD", "REBUILD", "FIX"}

if verdict_arg:
    verdict = verdict_arg.upper()
    verdict_reasoning = f"Override: {verdict_arg}"
    verdict_confidence = 1.0
else:
    if hard_blocks:
        verdict = "BLOCKED"
        verdict_reasoning = hard_block_reasons[0]
        verdict_confidence = max(hard_block_confs) if hard_block_confs else 1.0
    elif (
        corroborating_signals >= 2
        and weighted_evidence >= 1.4
        and corroborating_domain_count >= 2
        and avg_soft_confidence >= 0.65
    ):
        verdict = "BLOCKED"
        verdict_reasoning = (
            f"{corroborating_signals} corroborating failures across {corroborating_domain_count} domains "
            f"(weighted evidence {weighted_evidence:.2f}, avg confidence {avg_soft_confidence:.2f})"
        )
        verdict_confidence = min(0.95, 0.55 + (weighted_evidence / 3.0))
    elif corroborating_signals >= 1:
        verdict = "CAUTION"
        verdict_reasoning = f"{corroborating_signals} gate(s) failed; corroboration below block threshold"
        verdict_confidence = min(0.85, 0.45 + (weighted_evidence / 4.0))
    elif warn_high or mutation_warnings:
        verdict = "CAUTION"
        verdict_reasoning = f"{len(warn_high) + len(mutation_warnings)} cautionary warning signal(s)"
        max_warn_conf = max([_to_float(w.get("confidence", 0.5), 0.5) for w in warn_high], default=0.5)
        verdict_confidence = min(0.8, max_warn_conf)
    elif mutation_rate is None and mutation_expected:
        verdict = "CAUTION"
        verdict_reasoning = "mutation kill rate missing in mutation-required mode"
        verdict_confidence = 0.55
        warnings.append("mutation kill rate missing in mutation-required mode")
    else:
        verdict = "SHIP"
        verdict_reasoning = "No corroborated failures; no hard blocks"
        verdict_confidence = 0.9

if hard_block_reasons:
    blockers.extend(hard_block_reasons)
if soft_fails:
    warnings.append(
        f"{corroborating_signals} non-hard fail signal(s) across {corroborating_domain_count} domain(s); "
        f"weighted evidence={weighted_evidence:.2f}; avg confidence={avg_soft_confidence:.2f}"
    )
if warn_high:
    warnings.append(f"{len(warn_high)} high-confidence WARN gate(s)")
if mutation_warnings:
    warnings.extend(mutation_warnings)

# Score formula
eligible = max(0, (passed + warned + failed) - skipped)
if eligible == 0:
    score = 0
else:
    score = int(((passed + (0.5 * warned)) / eligible) * 100)
score = max(0, min(100, score))
if blockers:
    score = 0

proof = {
    "wreckit_version": "2.1.0",
    "run_id": run_id,
    "timestamp": now,
    "mode": mode,
    "project": os.path.abspath(project),
    "git_sha": git_sha,
    "project_type": {
        "type": project_type,
        "confidence": _to_float(project_profile.get("confidence", 0.0), 0.0),
        "signals": project_profile.get("signals", []),
    },
    "calibration": calibration,
    "gates": normalized,
    "verdict": verdict.title(),
    "verdict_reasoning": verdict_reasoning,
    "corroborating_signals": corroborating_signals,
    "corroborating_domains": corroborating_domains,
    "dissenting_signals": dissenting_signals,
    "hard_blocks": hard_blocks,
    "soft_fails": [g.get("gate") for g in soft_fails],
    "aggregation": {
        "weighted_evidence": weighted_evidence,
        "avg_soft_confidence": avg_soft_confidence,
        "corroborating_domains": corroborating_domains,
        "required_domains": 2,
        "block_threshold": 1.4,
        "required_corroborating_fails": 2,
        "min_avg_soft_confidence": 0.65,
        "gate_weights": {g.get("gate"): g.get("weight") for g in soft_fails},
    },
    "confidence": round(float(verdict_confidence), 4),
    "blockers": blockers,
    "warnings": warnings,
    "score": score,
}

verdict_emoji = "✅" if verdict == "SHIP" else ("⚠️" if verdict == "CAUTION" else "🚫")

dashboard = {
    "verdict": verdict.title(),
    "verdict_emoji": verdict_emoji,
    "score": score,
    "gates_passed": passed,
    "gates_failed": failed,
    "gates_warned": warned,
    "gates_skipped": skipped,
    "mutation_kill_rate": mutation_rate_fraction,
    "run_id": run_id,
    "timestamp": now,
    "project_type": project_type,
}

project_wreckit = os.path.join(project, ".wreckit")
with open(os.path.join(project_wreckit, "proof.json"), "w", encoding="utf-8") as f:
    json.dump(proof, f, indent=2)

with open(os.path.join(project_wreckit, "dashboard.json"), "w", encoding="utf-8") as f:
    json.dump(dashboard, f, indent=2)

# decision.md
lines = [
    f"# Verdict: {verdict.title()} {verdict_emoji}",
    "",
    "## Decision Date",
    now,
    "",
]

if blockers:
    lines += ["## Blocking Gates"]
    for b in blockers:
        lines.append(f"- {b}")
    lines.append("")

if warnings:
    lines += ["## Warning Gates"]
    for w in warnings:
        lines.append(f"- {w}")
    lines.append("")

lines += [
    "## Gate Summary",
    f"- Passed: {passed}",
    f"- Warned: {warned}",
    f"- Failed: {failed}",
    f"- Skipped: {skipped}",
    f"- Corroborating fail signals: {corroborating_signals}",
    f"- Corroborating domains: {', '.join(corroborating_domains) if corroborating_domains else 'none'}",
    f"- Dissenting signals: {dissenting_signals}",
    f"- Weighted evidence: {weighted_evidence}",
    f"- Avg soft-fail confidence: {avg_soft_confidence}",
    "",
]
lines += [
    "## Verdict Criteria Applied",
    "Ship: no hard blocks and no corroborated fail evidence above block threshold.",
    "Caution: single non-hard fail, warning-only signals, or evidence below corroboration threshold.",
    "Blocked: any hard block OR at least 2 corroborating non-hard failures across >=2 domains with weighted evidence >= 1.4 and avg confidence >= 0.65.",
    "Hard blocks are deterministic critical failures (e.g., confirmed hallucinated deps, real type errors, extreme slop density).",
    "",
]

with open(os.path.join(project_wreckit, "decision.md"), "w", encoding="utf-8") as f:
    f.write("\n".join(lines))

print(verdict)
PYEOF

rm -f "$GATES_INPUT_FILE"


path: scripts/property-test.sh
content: #!/usr/bin/env bash
# wreckit — Property-Based Testing Scanner
# Usage: ./property-test.sh [project-path]
# Detects and runs property-based tests, generates stubs if none found.
# Outputs structured JSON report.

set -euo pipefail

PROJECT="${1:-.}"
PROJECT="$(cd "$PROJECT" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

log() { echo "  [property-test] $*" >&2; }

echo "================================================" >&2
echo "  wreckit Property-Based Test Scanner" >&2
echo "  Project: $PROJECT" >&2
echo "================================================" >&2

# ─── Detect stack ─────────────────────────────────────────────────────────────
STACK_JSON=$("$SCRIPT_DIR/detect-stack.sh" "$PROJECT" 2>/dev/null || echo "{}")
LANG=$(echo "$STACK_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('language','unknown'))" 2>/dev/null || echo "unknown")
TEST_CMD=$(echo "$STACK_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commands',{}).get('test',''))" 2>/dev/null || echo "")

log "Language: $LANG"

# ─── State ────────────────────────────────────────────────────────────────────
FRAMEWORK="none"
PROP_TESTS_FOUND=0
PROP_TESTS_RUN=0
FAILURES="[]"
CRASH_INPUTS="[]"
STUBS_PATH=""
VERDICT="WARN"
NOTES=""

# ─── Phase 1: Detect property-based testing framework ─────────────────────────
log ""
log "Phase 1: Detecting property-based testing framework..."

case "$LANG" in
  python)
    # Check for hypothesis
    if grep -rn --include="*.py" \
      -E "from hypothesis|import hypothesis|@given|@settings" \
      "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="hypothesis"
      log "  Found: hypothesis (@given decorators)"
    elif python3 -c "import hypothesis" 2>/dev/null; then
      FRAMEWORK="hypothesis_available"
      log "  Hypothesis installed but no @given tests found"
    else
      log "  No property testing framework found (hypothesis not installed)"
    fi
    ;;
  typescript|javascript)
    # Check for fast-check
    if grep -rn --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx" \
      -E "from 'fast-check'|require\('fast-check'\)|fc\.property|fc\.assert" \
      "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="fast-check"
      log "  Found: fast-check (fc.property)"
    elif [ -f "$PROJECT/node_modules/fast-check/package.json" ]; then
      FRAMEWORK="fast-check_available"
      log "  fast-check installed but no fc.property tests found"
    else
      log "  No property testing framework found (fast-check not installed)"
    fi
    ;;
  rust)
    # Check for proptest or quickcheck
    if grep -rn --include="*.rs" \
      -E "proptest!|#\[proptest\]|quickcheck!|#\[quickcheck\]" \
      "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="proptest"
      log "  Found: proptest/quickcheck macros"
    elif grep -r "proptest\|quickcheck" "$PROJECT/Cargo.toml" 2>/dev/null | grep -q .; then
      FRAMEWORK="proptest_available"
      log "  proptest/quickcheck in Cargo.toml but no macros found"
    else
      log "  No property testing framework detected"
    fi
    ;;
  go)
    # Check for native go fuzz (1.18+) or gopter
    if grep -rn --include="*.go" -E "func Fuzz" "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="go-fuzz-native"
      log "  Found: native Go fuzz functions"
    elif grep -rn --include="*.go" -E "gopter|gocheck" "$PROJECT" 2>/dev/null | grep -q .; then
      FRAMEWORK="gopter"
      log "  Found: gopter/gocheck"
    else
      log "  No property testing framework detected"
    fi
    ;;
  *)
    log "  Language not supported for automated property test detection: $LANG"
    ;;
esac

# ─── Phase 2: Count existing property tests ───────────────────────────────────
log ""
log "Phase 2: Counting property tests..."

case "$FRAMEWORK" in
  hypothesis)
    PROP_TESTS_FOUND=$(grep -rn --include="*.py" -E "@given" "$PROJECT" 2>/dev/null | wc -l | tr -d ' ')
    log "  Found $PROP_TESTS_FOUND @given test cases"
    ;;
  fast-check)
    PROP_TESTS_FOUND=$(grep -rn --include="*.ts" --include="*.js" -E "fc\.property|fc\.assert" "$PROJECT" 2>/dev/null | wc -l | tr -d ' ')
    log "  Found $PROP_TESTS_FOUND fc.property() calls"
    ;;
  proptest)
    PROP_TESTS_FOUND=$(grep -rn --include="*.rs" -E "proptest!\s*\{|#\[proptest\]" "$PROJECT" 2>/dev/null | wc -l | tr -d ' ')
    log "  Found $PROP_TESTS_FOUND proptest cases"
    ;;
  go-fuzz-native)
    PROP_TESTS_FOUND=$(grep -rn --include="*.go" -E "func Fuzz" "$PROJECT" 2>/dev/null | wc -l | tr -d ' ')
    log "  Found $PROP_TESTS_FOUND fuzz functions"
    ;;
  *)
    PROP_TESTS_FOUND=0
    log "  0 property tests found"
    ;;
esac

# ─── Phase 3: Run property tests if found ────────────────────────────────────
log ""
log "Phase 3: Running property tests..."

if [ "$PROP_TESTS_FOUND" -gt 0 ]; then
  cd "$PROJECT"
  PROP_TEST_OUTPUT=""
  PROP_EXIT=0
  
  case "$FRAMEWORK" in
    hypothesis)
      log "  Running: $TEST_CMD (hypothesis tests)"
      PROP_TEST_OUTPUT=$(bash -c "$TEST_CMD 2>&1" || true)
      ;;
    fast-check)
      log "  Running: $TEST_CMD (fast-check tests)"
      PROP_TEST_OUTPUT=$(bash -c "$TEST_CMD 2>&1" || true)
      ;;
    proptest)
      log "  Running: cargo test (proptest)"
      PROP_TEST_OUTPUT=$(cargo test 2>&1 || true)
      ;;
    go-fuzz-native)
      log "  Running: go test (fuzz functions — short mode, 10s)"
      PROP_TEST_OUTPUT=$(go test -fuzz=. -fuzztime=10s ./... 2>&1 || true)
      ;;
  esac
  
  # Parse failures
  FAIL_LINES=$(echo "$PROP_TEST_OUTPUT" | grep -E "(FAILED|Falsifying|counterexample|panic|Error)" | head -5 || true)
  
  if [ -n "$FAIL_LINES" ]; then
    VERDICT="FAIL"
    FAILURES=$(echo "$FAIL_LINES" | python3 -c "
import sys, json
lines = [l.strip() for l in sys.stdin if l.strip()][:5]
print(json.dumps(lines))
" 2>/dev/null || echo '["Property test failed"]')
    log "  FAILURES detected: $FAIL_LINES"
  else
    VERDICT="PASS"
    PROP_TESTS_RUN=$PROP_TESTS_FOUND
    log "  All $PROP_TESTS_FOUND property tests passed"
  fi
  
  cd "$OLDPWD" 2>/dev/null || true
else
  log "  No property tests to run"
fi

# ─── Phase 4: Generate stubs if no property tests ────────────────────────────
log ""
log "Phase 4: Generating property test stubs..."

if [ "$PROP_TESTS_FOUND" -eq 0 ] && [ "$FRAMEWORK" != "none" ]; then
  STUBS_DIR="$PROJECT/.wreckit/property-stubs"
  mkdir -p "$STUBS_DIR"
  
  case "$LANG" in
    python)
      # Find public functions
      PUBLIC_FUNCS=$(grep -rn --include="*.py" \
        -E "^def [a-z][a-zA-Z0-9_]+\s*\(" \
        "$PROJECT" 2>/dev/null | grep -v "test_\|_test\|__" | head -5 || true)
      
      STUB_FILE="$STUBS_DIR/property_stubs.py"
      cat > "$STUB_FILE" << 'PYEOF'
"""
wreckit-generated property test stubs.
Move these into your actual test suite and expand them.
"""
from hypothesis import given, settings
import hypothesis.strategies as st

# TODO: Import the functions you want to test
# from mymodule import my_function


@given(st.lists(st.integers()))
def test_sort_is_ordered(lst):
    """Sort output should always be ordered."""
    # result = sorted(lst)  # Replace with your sort function
    # assert all(result[i] <= result[i+1] for i in range(len(result)-1))
    pass


@given(st.text())
def test_encode_decode_roundtrip(s):
    """Encode then decode should return original."""
    # encoded = encode(s)
    # assert decode(encoded) == s
    pass


@given(st.lists(st.integers()), st.lists(st.integers()))
def test_merge_length(a, b):
    """Merged result length should equal sum of inputs."""
    # result = merge(a, b)
    # assert len(result) == len(a) + len(b)
    pass
PYEOF
      log "  Generated: $STUB_FILE"
      STUBS_PATH="$STUBS_DIR"
      VERDICT="WARN"
      NOTES="No property tests found. Stubs generated at $STUBS_DIR — move to test suite."
      ;;
      
    typescript|javascript)
      STUB_FILE="$STUBS_DIR/property.spec.ts"
      cat > "$STUB_FILE" << 'TSEOF'
/**
 * wreckit-generated property test stubs.
 * Move these into your actual test suite and expand them.
 * Requires: npm install fast-check
 */
import * as fc from 'fast-check';

// TODO: Import your functions
// import { myFunction } from '../src/myModule';

describe('Property tests', () => {
  it('sort output is always ordered', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        // const result = arr.slice().sort((a, b) => a - b);
        // for (let i = 0; i < result.length - 1; i++) {
        //   if (result[i] > result[i + 1]) return false;
        // }
        return true;  // Replace with your assertion
      })
    );
  });

  it('encode/decode roundtrip', () => {
    fc.assert(
      fc.property(fc.string(), (s) => {
        // const encoded = encode(s);
        // return decode(encoded) === s;
        return true;  // Replace with your assertion
      })
    );
  });

  it('filter output is a subset of input', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), fc.func(fc.boolean()), (arr, predicate) => {
        // const result = arr.filter(predicate);
        // return result.every(item => arr.includes(item));
        return true;  // Replace with your assertion
      })
    );
  });
});
TSEOF
      log "  Generated: $STUB_FILE"
      STUBS_PATH="$STUBS_DIR"
      VERDICT="WARN"
      NOTES="No property tests found. Stubs generated at $STUBS_DIR"
      ;;
      
    rust)
      STUB_FILE="$STUBS_DIR/property_tests.rs"
      cat > "$STUB_FILE" << 'RSEOF'
// wreckit-generated property test stubs.
// Add to your test module and expand.
// Requires: cargo add proptest

#[cfg(test)]
mod property_tests {
    use proptest::prelude::*;
    // use crate::my_module::my_function;

    proptest! {
        #[test]
        fn sort_is_ordered(mut v: Vec<i32>) {
            // let sorted = my_sort(v.clone());
            // prop_assert!(sorted.windows(2).all(|w| w[0] <= w[1]));
        }

        #[test]
        fn encode_decode_roundtrip(s: String) {
            // let encoded = encode(&s);
            // prop_assert_eq!(decode(&encoded), s);
        }
    }
}
RSEOF
      log "  Generated: $STUB_FILE"
      STUBS_PATH="$STUBS_DIR"
      VERDICT="WARN"
      NOTES="No property tests found. Stubs generated at $STUBS_DIR"
      ;;
  esac
elif [ "$FRAMEWORK" = "none" ]; then
  log "  No framework available — skipping stub generation"
  VERDICT="WARN"
  NOTES="No property testing framework installed. Consider: hypothesis (Python), fast-check (JS/TS), proptest (Rust), go test -fuzz (Go)"
fi

# ─── Output report ────────────────────────────────────────────────────────────
log ""
log "Building report..."

python3 << PYEOF
import json

report = {
    "project": "$PROJECT",
    "scanner": "wreckit-property-test",
    "language": "$LANG",
    "framework_detected": "$FRAMEWORK",
    "property_tests_found": $PROP_TESTS_FOUND,
    "property_tests_run": $PROP_TESTS_RUN,
    "failures": $FAILURES,
    "crash_inputs": $CRASH_INPUTS,
    "stubs_generated": "${STUBS_PATH:-null}" if "${STUBS_PATH:-}" else None,
    "verdict": "$VERDICT",
    "notes": "${NOTES:-}"
}

print(json.dumps(report, indent=2))
PYEOF

echo "" >&2
echo "Results:" >&2
echo "  Framework: $FRAMEWORK" >&2
echo "  Property tests found: $PROP_TESTS_FOUND" >&2
echo "  Tests run: $PROP_TESTS_RUN" >&2
echo "  Verdict: $VERDICT" >&2


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)