#!/usr/bin/env node
'use strict';
// SigMap — standalone bundle (bundled modules generated by scripts/build-bundle.mjs)
// Copy this single file to any project: node gen-context.js --setup
// No src/ directory required. Node.js 18+ only. Zero npm dependencies.

const __factories = {};
const __cache = {};
function __require(key) {
  if (Object.prototype.hasOwnProperty.call(__cache, key)) {
    return __cache[key].exports;
  }
  if (!Object.prototype.hasOwnProperty.call(__factories, key)) {
    throw new Error('[sigmap] bundled module not found: ' + key);
  }
  const module = { exports: {} };
  __cache[key] = module;
  __factories[key](module, module.exports);
  return module.exports;
}


// ═══ SIGMAP BUNDLED MODULES — generated by scripts/build-bundle.mjs; do not edit by hand ═══

// ── ./packages/adapters/claude ──
__factories["./packages/adapters/claude"] = function(module, exports) {
  
  /**
   * Claude adapter — appends to CLAUDE.md under a marker line.
   * Never overwrites human-written content above the marker.
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   *   write(context, cwd, opts?) → void   (handles append logic)
   */

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

  const name = 'claude';

  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
  const ALLOWLIST_MARKER = '<!-- sigmap-bash-allowlist -->';

  const ALLOWLIST_BLOCK = [
    '## Bash allowlist',
    '',
    ALLOWLIST_MARKER,
    '',
    'The following sigmap commands are pre-approved — Claude Code will not prompt for confirmation:',
    '',
    '```json',
    JSON.stringify({
      permissions: {
        allow: [
          'Bash(sigmap ask*)',
          'Bash(sigmap validate*)',
          'Bash(sigmap judge*)',
          'Bash(sigmap weights*)',
          'Bash(sigmap history*)',
          'Bash(sigmap --query*)',
          'Bash(sigmap --diff*)',
          'Bash(sigmap --health*)',
          'Bash(sigmap --coverage*)',
          'Bash(node gen-context.js*)',
        ],
      },
    }, null, 2),
    '```',
    '',
    'Add the `permissions.allow` array above to `.claude/settings.json` to activate.',
    '',
  ].join('\n');

  /**
   * Format context suited for CLAUDE.md.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @param {string} [opts.version] - SigMap version string
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const version   = opts.version || 'unknown';
    const timestamp = new Date().toISOString();
    const meta      = _confidenceMeta(opts);
    return [
      `<!-- Generated by SigMap v${version} — ${timestamp} -->`,
      meta,
      '',
      context,
    ].join('\n');
  }

  function _confidenceMeta(opts) {
    const parts = [`version=${opts.version || 'unknown'}`];
    if (opts.confidence)    parts.push(`confidence=${opts.confidence}`);
    if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
    if (opts.dropped  != null) parts.push(`dropped=${opts.dropped}`);
    if (opts.commit)        parts.push(`commit=${opts.commit}`);
    return `<!-- sigmap: ${parts.join(' ')} -->`;
  }

  /**
   * Return the output file path for this adapter.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, 'CLAUDE.md');
  }

  /**
   * Write signatures into CLAUDE.md using the append-under-marker strategy.
   * Human content above the marker is never touched.
   * @param {string} context - Raw signature context string
   * @param {string} cwd - Project root
   * @param {object} [opts]
   */
  function write(context, cwd, opts = {}) {
    const filePath = outputPath(cwd);
    let existing = '';
    if (fs.existsSync(filePath)) {
      existing = fs.readFileSync(filePath, 'utf8');
    }
    const formatted = format(context, opts);
    const markerIdx = existing.indexOf('## Auto-generated signatures');
    let newContent;
    if (markerIdx !== -1) {
      newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
    } else {
      newContent = existing + MARKER + formatted;
    }

    // Inject ## Bash allowlist above the sig marker if not already present
    if (!newContent.includes(ALLOWLIST_MARKER)) {
      const sigMarkerPos = newContent.indexOf('## Auto-generated signatures');
      if (sigMarkerPos !== -1) {
        newContent = newContent.slice(0, sigMarkerPos) + ALLOWLIST_BLOCK + '\n' + newContent.slice(sigMarkerPos);
      } else {
        newContent = ALLOWLIST_BLOCK + '\n' + newContent;
      }
    }

    fs.writeFileSync(filePath, newContent, 'utf8');
  }

  module.exports = { name, format, outputPath, write };
  
};

// ── ./packages/adapters/codex ──
__factories["./packages/adapters/codex"] = function(module, exports) {
  
  /**
   * Codex adapter — writes OpenAI-style context to AGENTS.md.
   *
   * This adapter reuses the same prompt format as the OpenAI adapter,
   * but targets AGENTS.md so Codex-style agents can read repository guidance.
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   *   write(context, cwd, opts?) → void
   */

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

  const name = 'codex';
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';

  /**
   * Format context for AGENTS.md — clean markdown, no LLM preamble.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string' || !context.trim()) return '';
    return `# Code signatures\n\n${context}`;
  }

  /**
   * Return the output file path for this adapter.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, 'AGENTS.md');
  }

  /**
   * Write signatures into AGENTS.md using append-under-marker.
   * If marker exists, content above marker is preserved.
   * If legacy generated content exists without marker, replace it cleanly.
   * @param {string} context - Raw signature context string
   * @param {string} cwd - Project root
   * @param {object} [opts]
   */
  function write(context, cwd, opts = {}) {
    const filePath = outputPath(cwd);
    let existing = '';
    if (fs.existsSync(filePath)) {
      existing = fs.readFileSync(filePath, 'utf8');
    }

    const formatted = format(context, opts);
    const markerIdx = existing.indexOf('## Auto-generated signatures');

    let newContent;
    if (markerIdx !== -1) {
      newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
    } else {
      const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
        || existing.includes('## Code Signatures')
        || existing.includes('# Code signatures');
      newContent = isLegacyGenerated
        ? MARKER.trimStart() + formatted
        : existing + MARKER + formatted;
    }

    fs.writeFileSync(filePath, newContent, 'utf8');
  }

  module.exports = { name, format, outputPath, write };
  
};

// ── ./packages/adapters/copilot ──
__factories["./packages/adapters/copilot"] = function(module, exports) {
  
  /**
   * Copilot adapter — writes to .github/copilot-instructions.md
   * GitHub Copilot reads this file automatically in every workspace.
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   */

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

  const name = 'copilot';
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';

  /**
   * Format context for GitHub Copilot instructions.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @param {string} [opts.version] - SigMap version string
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const version   = opts.version   || 'unknown';
    const timestamp = new Date().toISOString();
    const meta = _confidenceMeta(opts);
    const header = [
      `<!-- Generated by SigMap gen-context.js v${version} -->`,
      `<!-- Updated: ${timestamp} -->`,
      meta,
      `<!-- Do not edit below — regenerate with: node gen-context.js -->`,
      '',
      '# Code signatures',
      '',
    ].join('\n');
    return header + context;
  }

  function _confidenceMeta(opts) {
    const parts = [`version=${opts.version || 'unknown'}`];
    if (opts.confidence)    parts.push(`confidence=${opts.confidence}`);
    if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
    if (opts.dropped  != null) parts.push(`dropped=${opts.dropped}`);
    if (opts.commit)        parts.push(`commit=${opts.commit}`);
    return `<!-- sigmap: ${parts.join(' ')} -->`;
  }

  /**
   * Return the output file path for this adapter.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, '.github', 'copilot-instructions.md');
  }

  /**
   * Write signatures into copilot-instructions.md using append-under-marker.
   * If marker exists, content above marker is preserved.
   * If legacy generated content exists without marker, replace it cleanly.
   * @param {string} context - Raw signature context string
   * @param {string} cwd - Project root
   * @param {object} [opts]
   */
  function write(context, cwd, opts = {}) {
    const filePath = outputPath(cwd);
    let existing = '';
    if (fs.existsSync(filePath)) {
      existing = fs.readFileSync(filePath, 'utf8');
    }

    const formatted = format(context, opts);
    const markerIdx = existing.indexOf('## Auto-generated signatures');

    let newContent;
    if (markerIdx !== -1) {
      newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
    } else {
      const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
        || existing.includes('# Code signatures');
      newContent = isLegacyGenerated
        ? MARKER.trimStart() + formatted
        : existing + MARKER + formatted;
    }

    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, newContent, 'utf8');
  }

  module.exports = { name, format, outputPath, write };
  
};

// ── ./packages/adapters/cursor ──
__factories["./packages/adapters/cursor"] = function(module, exports) {
  
  /**
   * Cursor adapter — writes to .cursorrules
   * Cursor reads .cursorrules automatically in every workspace.
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   */

  const path = require('path');

  const name = 'cursor';

  /**
   * Format context for Cursor rules file.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @param {string} [opts.version] - SigMap version string
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const version   = opts.version || 'unknown';
    const timestamp = new Date().toISOString();
    const meta      = _confidenceMeta(opts);
    const header = [
      `# Code signatures — generated by SigMap v${version}`,
      `# Updated: ${timestamp}`,
      `# ${meta}`,
      `# Regenerate: node gen-context.js`,
      '',
    ].join('\n');
    return header + context;
  }

  function _confidenceMeta(opts) {
    const parts = [`version=${opts.version || 'unknown'}`];
    if (opts.confidence)    parts.push(`confidence=${opts.confidence}`);
    if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
    if (opts.dropped  != null) parts.push(`dropped=${opts.dropped}`);
    if (opts.commit)        parts.push(`commit=${opts.commit}`);
    return `sigmap: ${parts.join(' ')}`;
  }

  /**
   * Return the output file path for this adapter.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, '.cursorrules');
  }

  module.exports = { name, format, outputPath };
  
};

// ── ./packages/adapters/gemini ──
__factories["./packages/adapters/gemini"] = function(module, exports) {
  
  /**
   * Gemini adapter — formats context as a Gemini system instruction.
   * Use the output as the `system_instruction` field in a Gemini API request.
   *
   * Example usage:
   *   const { format } = require('sigmap/adapters/gemini');
   *   const instruction = format(context);
   *   // Pass to: genAI.getGenerativeModel({ model: 'gemini-pro', systemInstruction: instruction })
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   */

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

  const name = 'gemini';
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';

  /**
   * Format context as a Gemini system instruction.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @param {string} [opts.version] - SigMap version string
   * @param {string} [opts.projectName] - Optional project name
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const version = opts.version || 'unknown';
    const timestamp = new Date().toISOString();
    const projectLine = opts.projectName
      ? `Project: ${opts.projectName}\n`
      : '';

    const meta = _confidenceMeta(opts);
    return [
      `You are a coding assistant with complete knowledge of this codebase.`,
      `The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
      `<!-- ${meta} -->`,
      projectLine,
      `These signatures represent every public function, class, and type in the project.`,
      `Refer to them when answering questions about code structure, APIs, and implementation.`,
      `## Code Signatures`,
      ``,
      context,
    ].join('\n');
  }

  /**
   * Return the output file path for this adapter.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, '.github', 'gemini-context.md');
  }

  /**
   * Write signatures into gemini-context.md using append-under-marker.
   * If marker exists, content above marker is preserved.
   * If legacy generated content exists without marker, replace it cleanly.
   * @param {string} context - Raw signature context string
   * @param {string} cwd - Project root
   * @param {object} [opts]
   */
  function write(context, cwd, opts = {}) {
    const filePath = outputPath(cwd);
    let existing = '';
    if (fs.existsSync(filePath)) {
      existing = fs.readFileSync(filePath, 'utf8');
    }

    const formatted = format(context, opts);
    const markerIdx = existing.indexOf('## Auto-generated signatures');

    let newContent;
    if (markerIdx !== -1) {
      newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
    } else {
      const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
        || existing.includes('## Code Signatures');
      newContent = isLegacyGenerated
        ? MARKER.trimStart() + formatted
        : existing + MARKER + formatted;
    }

    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, newContent, 'utf8');
  }

  function _confidenceMeta(opts) {
    const parts = [`version=${opts.version || 'unknown'}`];
    if (opts.confidence)    parts.push(`confidence=${opts.confidence}`);
    if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
    if (opts.dropped  != null) parts.push(`dropped=${opts.dropped}`);
    if (opts.commit)        parts.push(`commit=${opts.commit}`);
    return `sigmap: ${parts.join(' ')}`;
  }

  module.exports = { name, format, outputPath, write };
  
};

// ── ./packages/adapters/index ──
__factories["./packages/adapters/index"] = function(module, exports) {
  
  /**
   * packages/adapters/index.js
   * Central registry for all SigMap output adapters.
   *
   * Usage:
   *   const { getAdapter, listAdapters, adapt } = require('sigmap/adapters');
   *   const output = adapt(context, 'copilot', { version: '3.0.0' });
   */

  const path = require('path');

  // Third-party adapters: 'willow' writes atoms to a Willow MCP knowledge store
  // instead of a flat file (see willow.js for configuration).
  const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex', 'willow'];

  // Lazy-load adapters so unused ones don't pay any require() cost
  const _cache = {};

  /**
   * Load and return an adapter module by name.
   * @param {string} name - Adapter name (copilot|claude|cursor|windsurf|openai|gemini|codex|willow)
   * @returns {{ name: string, format: Function, outputPath: Function }|null}
   */
  function getAdapter(name) {
    if (!name || typeof name !== 'string') return null;
    const key = name.toLowerCase();
    if (!ADAPTER_NAMES.includes(key)) return null;
    if (!_cache[key]) {
      try {
        _cache[key] = require(path.join(__dirname, key + '.js'));
      } catch (_) {
        return null;
      }
    }
    return _cache[key];
  }

  /**
   * List all available adapter names.
   * @returns {string[]}
   */
  function listAdapters() {
    return ADAPTER_NAMES.slice();
  }

  /**
   * Format context using the named adapter.
   * @param {string} context - Raw signature context string
   * @param {string} adapterName - Adapter name
   * @param {object} [opts] - Options passed to adapter.format()
   * @returns {string} Formatted output string (empty string if adapter not found or context empty)
   */
  function adapt(context, adapterName, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const adapter = getAdapter(adapterName);
    if (!adapter || typeof adapter.format !== 'function') return '';
    try {
      return adapter.format(context, opts);
    } catch (_) {
      return '';
    }
  }

  /**
   * Map old `outputs` config values to new `adapters` names.
   * Provides backward compatibility for existing configurations.
   * @param {string[]} outputs - Legacy outputs array
   * @returns {string[]} Equivalent adapters array
   */
  function outputsToAdapters(outputs) {
    if (!Array.isArray(outputs)) return ['copilot'];
    return outputs.map((o) => {
      // All current output names already match adapter names
      if (ADAPTER_NAMES.includes(o)) return o;
      return o; // pass through unknowns — getAdapter() will handle gracefully
    });
  }

  module.exports = { getAdapter, listAdapters, adapt, outputsToAdapters };
  
};

// ── ./packages/adapters/llm-full ──
__factories["./packages/adapters/llm-full"] = function(module, exports) {
  
  const path = require('path');
  const fs   = require('fs');
  module.exports = { name: 'llm-full', format, outputPath, write };

  function outputPath(cwd) { return path.join(cwd, 'llm-full.txt'); }

  function format(context, opts) {
    opts = opts || {};
    const lines = [
      `# ${context.projectName || 'Project'} — SigMap Context`,
      `Generated: ${new Date().toISOString()} | SigMap v${opts.version || ''}`,
      '',
    ];
    for (const entry of (context.fileEntries || [])) {
      const rel = path.relative(opts.cwd || '', entry.filePath);
      lines.push(`## ${rel}`, '```', ...(entry.sigs || []), '```', '');
    }
    return lines.join('\n');
  }

  function write(context, cwd, opts) {
    opts = opts || {};
    fs.writeFileSync(outputPath(cwd), format(context, { ...opts, cwd }));
  }
  
};

// ── ./packages/adapters/openai ──
__factories["./packages/adapters/openai"] = function(module, exports) {
  
  /**
   * OpenAI adapter — formats context as an OpenAI system message.
   * Use the output as the `content` field of a system role message.
   *
   * Example usage in code:
   *   const { format } = require('sigmap/adapters/openai');
   *   const systemPrompt = format(context);
   *   // Pass to: openai.chat.completions.create({ messages: [{ role: 'system', content: systemPrompt }] })
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   */

  const path = require('path');

  const name = 'openai';

  /**
   * Format context as an OpenAI system prompt.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @param {string} [opts.version] - SigMap version string
   * @param {string} [opts.projectName] - Optional project name
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const version = opts.version || 'unknown';
    const timestamp = new Date().toISOString();
    const projectLine = opts.projectName
      ? `Project: ${opts.projectName}\n`
      : '';

    const meta = _confidenceMeta(opts);
    return [
      `You are a coding assistant with full knowledge of this codebase.`,
      `Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
      `<!-- ${meta} -->`,
      projectLine,
      `Use these signatures to answer questions about the code accurately.`,
      `When the user asks about a specific file or function, refer to the signatures below.`,
      `## Code Signatures`,
      ``,
      context,
    ].join('\n');
  }

  /**
   * Return the output file path for this adapter.
   * Writes a .openai-context.md file that can be loaded at runtime.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, '.github', 'openai-context.md');
  }

  function _confidenceMeta(opts) {
    const parts = [`version=${opts.version || 'unknown'}`];
    if (opts.confidence)    parts.push(`confidence=${opts.confidence}`);
    if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
    if (opts.dropped  != null) parts.push(`dropped=${opts.dropped}`);
    if (opts.commit)        parts.push(`commit=${opts.commit}`);
    return `sigmap: ${parts.join(' ')}`;
  }

  module.exports = { name, format, outputPath };
  
};

// ── ./packages/adapters/willow ──
__factories["./packages/adapters/willow"] = function(module, exports) {
  
  /**
   * Willow adapter — writes SigMap context to Willow MCP knowledge store.
   *
   * Instead of writing a flat .willow-context.md file, this adapter sends
   * signature atoms to a Willow MCP server (https://github.com/rudi193-cmd/willow-1.9)
   * via HTTP POST. Each indexed file becomes a searchable knowledge atom.
   *
   * Contract:
   *   format(context, opts?) → string   (markdown for display/debug)
   *   outputPath(cwd)        → string   (placeholder — no file written)
   *   write(context, cwd, opts?) → Promise<void> (POSTs to Willow MCP, must await)
   *
   * Configuration (env vars or opts):
   *   WILLOW_MCP_URL   — MCP server base URL (default: http://localhost:8000)
   *   WILLOW_AGENT     — agent namespace (default: sigmap)
   *   WILLOW_TIMEOUT   — fetch timeout in ms (default: 30000)
   *   WILLOW_MAX_ATOM_SIZE — max atom size in bytes (default: 100000)
   *   WILLOW_RETRIES   — max retry attempts for transient failures (default: 3)
   */

  const crypto = require('crypto');
  const name = 'willow';
  const DEFAULT_MCP_URL = 'http://localhost:8000';
  const DEFAULT_TIMEOUT_MS = 30000;
  const DEFAULT_MAX_ATOM_SIZE = 100000;
  const DEFAULT_RETRIES = 3;

  /**
   * Format SigMap context as markdown for display or debug.
   * @param {string} context - Raw SigMap context string
   * @param {object} [opts] - Unused; reserved for future options
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const ts = new Date().toISOString();
    return `<!-- SigMap Willow context — ${ts} -->\n\n${context}`;
  }

  /**
   * Return the placeholder output path (no file is written by this adapter).
   * @param {string} cwd - Working directory
   * @returns {string}
   */
  function outputPath(cwd) {
    return '.willow-context.md';
  }

  /**
   * Generate a cryptographically strong ID for an atom.
   * Uses SHA256 hash of the filepath to ensure uniqueness and prevent collisions.
   * @param {string} filepath - File path to hash
   * @returns {string} - sigmap-{32-char-hex}
   */
  function generateAtomId(filepath) {
    const hash = crypto
      .createHash('sha256')
      .update(filepath)
      .digest('hex');
    return `sigmap-${hash}`;
  }

  /**
   * Fetch with timeout support.
   * @param {string} url - URL to fetch
   * @param {object} opts - Fetch options
   * @param {number} timeoutMs - Timeout in milliseconds
   * @returns {Promise<Response>}
   */
  async function fetchWithTimeout(url, opts, timeoutMs) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
    try {
      return await fetch(url, { ...opts, signal: controller.signal });
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /**
   * POST an atom to Willow with exponential backoff retry.
   * @param {object} atom - Atom to ingest
   * @param {string} mcpUrl - MCP server URL
   * @param {number} timeoutMs - Fetch timeout
   * @param {number} maxRetries - Max retry attempts
   * @returns {Promise<boolean>} - True if succeeded, false if all retries exhausted
   */
  async function postAtomWithRetry(atom, mcpUrl, timeoutMs, maxRetries) {
    let lastErr;
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      try {
        const resp = await fetchWithTimeout(
          `${mcpUrl}/tools/call`,
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              name: 'willow_knowledge_ingest',
              arguments: {
                app_id: atom.agent,
                title: atom.title,
                summary: atom.summary,
                domain: atom.domain,
                source_type: atom.source_type,
                category: 'code',
                record_id: atom.id,
              },
            }),
          },
          timeoutMs,
        );

        if (resp.ok) {
          return true;
        }

        if (resp.status >= 500) {
          lastErr = new Error(`HTTP ${resp.status}`);
          if (attempt < maxRetries - 1) {
            await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt)));
            continue;
          }
        } else {
          process.stderr.write(`[willow-adapter] ${atom.id}: HTTP ${resp.status} (not retryable)\n`);
          return false;
        }
      } catch (err) {
        lastErr = err;
        if (attempt < maxRetries - 1) {
          await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt)));
          continue;
        }
      }
    }

    process.stderr.write(
      `[willow-adapter] ${atom.id}: failed after ${maxRetries} attempts: ${lastErr?.message || 'unknown'}\n`,
    );
    return false;
  }

  /**
   * POST each file section from SigMap context to the Willow MCP knowledge store.
   * Each `## filepath` section becomes one searchable knowledge atom.
   * Failures are per-atom and logged to stderr; function never throws.
   * IMPORTANT: This is async — caller MUST await write() before process exit.
   *
   * @param {string} context - Raw SigMap context string
   * @param {string} cwd - Working directory (used as project label)
   * @param {object} [opts] - Optional overrides: { mcpUrl, agent, timeoutMs, maxAtomSize, maxRetries }
   * @returns {Promise<void>}
   */
  async function write(context, cwd, opts = {}) {
    if (!context) return;

    const mcpUrl = opts.mcpUrl || process.env.WILLOW_MCP_URL || DEFAULT_MCP_URL;
    const agent = opts.agent || process.env.WILLOW_AGENT || 'sigmap';
    const timeoutMs = opts.timeoutMs || parseInt(process.env.WILLOW_TIMEOUT, 10) || DEFAULT_TIMEOUT_MS;
    const maxAtomSize = opts.maxAtomSize || parseInt(process.env.WILLOW_MAX_ATOM_SIZE, 10) || DEFAULT_MAX_ATOM_SIZE;
    const maxRetries = opts.maxRetries || parseInt(process.env.WILLOW_RETRIES, 10) || DEFAULT_RETRIES;

    const sections = context.split(/\n(?=##\s)/);
    const atoms = sections
      .map((section) => {
        const titleMatch = section.match(/^##\s+(.+)/);
        if (!titleMatch) return null;

        const title = titleMatch[1].trim();
        const contentSize = section.length;

        if (contentSize > maxAtomSize) {
          process.stderr.write(`[willow-adapter] ${title}: oversized (${contentSize} > ${maxAtomSize} bytes)\n`);
          return null;
        }

        return {
          id: generateAtomId(title),
          title,
          summary: `${title} (${contentSize} bytes)`,
          content: section.trim(),
          domain: 'code',
          source_type: 'sigmap',
          agent,
          project: cwd ? require('path').basename(cwd) : 'unknown',
        };
      })
      .filter(Boolean);

    if (!atoms.length) return;

    await Promise.all(
      atoms.map((atom) => postAtomWithRetry(atom, mcpUrl, timeoutMs, maxRetries).catch((err) => {
        process.stderr.write(`[willow-adapter] ${atom.id}: unexpected error: ${err.message}\n`);
      })),
    );
  }

  module.exports = { name, format, outputPath, write };
  
};

// ── ./packages/adapters/windsurf ──
__factories["./packages/adapters/windsurf"] = function(module, exports) {
  
  /**
   * Windsurf adapter — writes to .windsurfrules
   * Windsurf reads .windsurfrules automatically in every workspace.
   *
   * Contract:
   *   format(context, opts?) → string
   *   outputPath(cwd) → string
   */

  const path = require('path');

  const name = 'windsurf';

  /**
   * Format context for Windsurf rules file.
   * @param {string} context - Raw signature context string
   * @param {object} [opts]
   * @param {string} [opts.version] - SigMap version string
   * @returns {string}
   */
  function format(context, opts = {}) {
    if (!context || typeof context !== 'string') return '';
    const version   = opts.version || 'unknown';
    const timestamp = new Date().toISOString();
    const meta      = _confidenceMeta(opts);
    const header = [
      `# Code signatures — generated by SigMap v${version}`,
      `# Updated: ${timestamp}`,
      `# ${meta}`,
      `# Regenerate: node gen-context.js`,
      '',
    ].join('\n');
    return header + context;
  }

  function _confidenceMeta(opts) {
    const parts = [`version=${opts.version || 'unknown'}`];
    if (opts.confidence)    parts.push(`confidence=${opts.confidence}`);
    if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
    if (opts.dropped  != null) parts.push(`dropped=${opts.dropped}`);
    if (opts.commit)        parts.push(`commit=${opts.commit}`);
    return `sigmap: ${parts.join(' ')}`;
  }

  /**
   * Return the output file path for this adapter.
   * @param {string} cwd - Project root
   * @returns {string}
   */
  function outputPath(cwd) {
    return path.join(cwd, '.windsurfrules');
  }

  module.exports = { name, format, outputPath };
  
};

// ── ./src/analysis/coverage-score ──
__factories["./src/analysis/coverage-score"] = function(module, exports) {
  
  /**
   * SigMap coverage scorer — v5.5.0
   *
   * Measures what fraction of *code* files made it into the context output
   * after token-budget application. Non-code files (json, md, config) are
   * counted separately as `nonCodeSkipped` so the grade reflects real coverage.
   *
   *   - Health score = context freshness / reduction quality / budget compliance
   *   - Coverage score = how much of the codebase is represented in context
   *
   * Grade scale:  A ≥ 90%  |  B ≥ 75%  |  C ≥ 50%  |  D < 50%
   *
   * @param {string} cwd
   * @param {Array<{filePath:string}>} fileEntries - files that made it into output
   * @param {{srcDirs:string[], exclude:string[]}} config
   * @returns {{
   *   score: number,
   *   grade: 'A'|'B'|'C'|'D',
   *   total: number,
   *   included: number,
   *   dropped: number,
   *   nonCodeSkipped: number,
   *   confidence: 'HIGH'|'MEDIUM'|'LOW',
   *   perModule: Map<string, {total:number, included:number, pct:number}>,
   * }}
   */

  const CODE_EXTS = new Set([
    '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx',
    '.py', '.rb', '.go', '.rs', '.java', '.kt',
    '.cs', '.cpp', '.c', '.h', '.hpp',
    '.swift', '.dart', '.scala', '.php',
    '.vue', '.svelte', '.css', '.scss',
    '.sql', '.graphql', '.proto', '.tf',
    '.lua', '.r', '.jl', '.ex', '.exs',
    '.sh', '.bash', '.zsh', '.ps1',
  ]);

  function coverageScore(cwd, fileEntries, config) {
    const fs   = require('fs');
    const path = require('path');

    const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length > 0)
      ? config.srcDirs
      : ['src', 'app', 'lib'];

    const excludeSet = new Set([
      'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
      '.next', 'coverage', 'target', 'vendor', '.context',
    ]);
    if (config && Array.isArray(config.exclude)) {
      for (const x of config.exclude) excludeSet.add(String(x));
    }

    const includedSet = new Set((fileEntries || []).map(f => f.filePath));

    // Walk srcDirs: separate code files from non-code files
    const allFiles  = [];
    const allSource = [];
    for (const relDir of srcDirs) {
      const absDir = path.resolve(cwd, relDir);
      if (fs.existsSync(absDir)) _walk(absDir, excludeSet, allFiles);
    }
    for (const f of allFiles) {
      if (CODE_EXTS.has(path.extname(f).toLowerCase())) allSource.push(f);
    }
    const nonCodeSkipped = allFiles.length - allSource.length;

    const total    = allSource.length;
    const included = allSource.filter(f => includedSet.has(f)).length;
    const dropped  = total - included;
    const pct      = total > 0 ? Math.round((included / total) * 100) : 100;

    const grade = pct >= 90 ? 'A' : pct >= 75 ? 'B' : pct >= 50 ? 'C' : 'D';
    const confidence = pct >= 90 ? 'HIGH' : pct >= 70 ? 'MEDIUM' : 'LOW';

    // Per-module breakdown (one entry per srcDir)
    const perModule = new Map();
    for (const relDir of srcDirs) {
      const absDir   = path.resolve(cwd, relDir);
      const modFiles = allSource.filter(f => f.startsWith(absDir + path.sep) || f === absDir);
      const modIncl  = modFiles.filter(f => includedSet.has(f)).length;
      const modPct   = modFiles.length > 0 ? Math.round((modIncl / modFiles.length) * 100) : 100;
      perModule.set(relDir, { total: modFiles.length, included: modIncl, pct: modPct });
    }

    return { score: pct, grade, total, included, dropped, nonCodeSkipped, confidence, perModule };
  }

  function _walk(dir, excludeSet, out) {
    const fs   = require('fs');
    const path = require('path');
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
    for (const e of entries) {
      if (excludeSet.has(e.name)) continue;
      const full = path.join(dir, e.name);
      if (e.isDirectory()) { _walk(full, excludeSet, out); }
      else if (e.isFile())  { out.push(full); }
    }
  }

  module.exports = { coverageScore, CODE_EXTS };
  
};

// ── ./src/analysis/diagnostics ──
__factories["./src/analysis/diagnostics"] = function(module, exports) {
  
  /**
   * Diagnostic utilities for understanding file inclusion/exclusion decisions.
   *
   * Provides detailed per-file diagnostics showing:
   * - Why files are included or excluded
   * - Ranking scores and signals
   * - Budget constraints and priorities
   */

  const path = require('path');

  function estimateTokens(text) {
    return Math.ceil(text.length / 4);
  }

  function formatFileDecision(entry, decision, reason, score = null) {
    const rel = path.relative(process.cwd(), entry.filePath);
    const tokens = estimateTokens(entry.sigs.join('\n'));
    let line = `${decision === 'included' ? '✓' : '✗'} ${rel}`;
    line += ` [${tokens} tokens]`;
    if (score !== null) line += ` [score: ${score.toFixed(2)}]`;
    if (reason) line += ` — ${reason}`;
    return line;
  }

  function computeFileMetrics(entry) {
    const loc = entry.content ? entry.content.split('\n').length : 1;
    const sigCount = entry.sigs ? entry.sigs.length : 0;
    const signalQuality = loc > 0 ? sigCount / loc : 0;
    const tokens = estimateTokens(entry.sigs.join('\n'));

    return {
      lineOfCode: loc,
      sigCount: sigCount,
      signalQuality: signalQuality.toFixed(3),
      tokens: tokens,
      relevance: (sigCount / Math.max(loc, 1)).toFixed(3),
    };
  }

  function explainInclusion(fileEntries, budgetLimit) {
    const lines = [];

    lines.push('## File Inclusion Diagnostics\n');
    lines.push(`Budget: ${budgetLimit} tokens`);
    lines.push(`Files: ${fileEntries.length} scanned\n`);

    let totalTokens = 0;
    const withMetrics = fileEntries.map((e) => {
      const metrics = computeFileMetrics(e);
      totalTokens += metrics.tokens;
      return { entry: e, metrics };
    });

    lines.push(`Total token requirement: ${totalTokens} tokens`);
    lines.push(`Budget headroom: ${budgetLimit * 0.9} tokens (90% of ${budgetLimit})\n`);

    if (totalTokens > budgetLimit * 0.9) {
      lines.push('⚠ Over budget — files will be dropped\n');
      lines.push('### Per-file metrics:');
      for (const { entry, metrics } of withMetrics) {
        const rel = path.relative(process.cwd(), entry.filePath);
        lines.push(`- ${rel}`);
        lines.push(`  - Size: ${metrics.tokens} tokens, ${metrics.lineOfCode} lines`);
        lines.push(`  - Sigs: ${metrics.sigCount}, quality: ${metrics.signalQuality}`);
      }
    } else {
      lines.push('✓ All files fit within budget\n');
    }

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

  function explainExclusion(dropped, reason) {
    return `Excluded ${dropped.length} files: ${reason}`;
  }

  module.exports = {
    formatFileDecision,
    computeFileMetrics,
    explainInclusion,
    explainExclusion,
    estimateTokens,
  };
  
};

// ── ./src/cache/freshen ──
__factories["./src/cache/freshen"] = function(module, exports) {
  
  /**
   * Read-time self-heal (IMPL.md Layer 1, "safety net" tier).
   *
   * Keeps the sig-cache in line with the current source tree so the index reflects
   * on-disk reality even when no write hook was called. Re-extracts files modified
   * since the context file was generated (bounded to actual session edits, not the
   * whole tree), drops cache entries for deleted files, and persists. buildSigIndex
   * already merges the cache, so the next read is fresh.
   *
   * Throttled per cwd. Skips entirely when there is no generated index to heal
   * (a cold repo should run `generate` or use the notify hooks).
   *
   * Zero-dependency, bundle-safe (fs + dispatch + sig-cache).
   */

  const fs = require('fs');
  const path = require('path');
  const { loadCache, saveCache, getChangedFiles } = __require('./src/cache/sig-cache');
  const { extractFile, langFor } = __require('./src/extractors/dispatch');

  const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'packages', 'services', 'api'];
  const DEFAULT_EXCLUDE = [
    'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
    '.next', 'coverage', 'target', 'vendor', '.context',
  ];
  const CONTEXT_PATHS = [
    ['.github', 'copilot-instructions.md'],
    ['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
  ];
  const THROTTLE_MS = 1500;
  const _lastRun = new Map();

  function _readConfig(cwd) {
    try {
      const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
      return cfg && typeof cfg === 'object' ? cfg : {};
    } catch (_) { return {}; }
  }

  function _pkgVersion(cwd) {
    try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
    catch (_) { return '0.0.0'; }
  }

  /** Newest mtime among existing generated context files, or 0 if none. */
  function _contextMtime(cwd) {
    let newest = 0;
    for (const parts of CONTEXT_PATHS) {
      try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
    }
    return newest;
  }

  function _walk(dir, exclude, out, depth, maxDepth) {
    if (depth > maxDepth) return;
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
    for (const e of entries) {
      if (exclude.has(e.name)) continue;
      const full = path.join(dir, e.name);
      if (e.isDirectory()) _walk(full, exclude, out, depth + 1, maxDepth);
      else if (e.isFile() && langFor(e.name)) out.push(full);
    }
  }

  /**
   * Re-extract source files changed since the last generate; drop deleted files.
   * @param {string} cwd
   * @param {{force?:boolean, now?:number}} [opts]
   * @returns {number} cache entries touched
   */
  function freshen(cwd, opts = {}) {
    const now = opts.now != null ? opts.now : Date.now();
    if (!opts.force) {
      if (now - (_lastRun.get(cwd) || 0) < THROTTLE_MS) return 0;
    }
    _lastRun.set(cwd, now);

    try {
      const version = _pkgVersion(cwd);
      const cache = loadCache(cwd, version);
      const ctxMtime = _contextMtime(cwd);
      // Nothing to heal: no generated context AND no live cache overlay.
      if (ctxMtime === 0 && cache.size === 0) return 0;

      const cfg = _readConfig(cwd);
      const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length ? cfg.srcDirs : DEFAULT_SRC_DIRS;
      const exclude = new Set([...DEFAULT_EXCLUDE, ...(Array.isArray(cfg.exclude) ? cfg.exclude : [])]);
      const maxDepth = Number.isFinite(cfg.maxDepth) ? cfg.maxDepth : 8;

      const files = [];
      for (const d of srcDirs) {
        const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
        if (fs.existsSync(abs)) _walk(abs, exclude, files, 0, maxDepth);
      }

      // Candidates = files modified since the context was generated, or not yet cached.
      const candidates = files.filter((f) => {
        try { return fs.statSync(f).mtimeMs > ctxMtime || !cache.has(f); } catch (_) { return false; }
      });
      const { changed } = getChangedFiles(candidates, cache);

      let touched = 0;
      for (const f of changed) {
        try {
          const sigs = extractFile(f, fs.readFileSync(f, 'utf8'));
          cache.set(f, { mtime: fs.statSync(f).mtimeMs, sigs });
          touched++;
        } catch (_) {}
      }
      // Note: deletions are NOT swept here — a cache entry may be a `notify`
      // overlay for a file not yet on disk. Explicit removal is `notify_file_deleted`.

      if (touched > 0) saveCache(cwd, version, cache);
      return touched;
    } catch (_) {
      return 0;
    }
  }

  module.exports = { freshen };
  
};

// ── ./src/cache/sig-cache ──
__factories["./src/cache/sig-cache"] = function(module, exports) {
  
  /**
   * Incremental extraction cache (v6.1).
   *
   * Stores extracted signatures keyed by file path + mtime so only
   * modified files are re-extracted on subsequent runs.
   *
   * Cache file: .sigmap-cache.json in cwd (gitignored).
   * Format:
   *   { sigmapVersion: string, entries: { [absPath]: { mtime: number, sigs: string[] } } }
   */

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

  const CACHE_FILE = '.sigmap-cache.json';

  function cachePath(cwd) {
    return path.join(cwd, CACHE_FILE);
  }

  /**
   * Load the cache from disk.
   * Returns a Map<absPath, { mtime: number, sigs: string[] }>.
   * Returns an empty Map if cache is absent, corrupt, or from a different version.
   *
   * @param {string} cwd
   * @param {string} currentVersion - sigmap VERSION constant
   * @returns {Map<string, { mtime: number, sigs: string[] }>}
   */
  function loadCache(cwd, currentVersion) {
    try {
      const raw = fs.readFileSync(cachePath(cwd), 'utf8');
      const data = JSON.parse(raw);
      // Bust cache on version change to avoid stale sig formats
      if (data.sigmapVersion !== currentVersion) return new Map();
      return new Map(Object.entries(data.entries || {}));
    } catch (_) {
      return new Map();
    }
  }

  /**
   * Persist the cache to disk.
   *
   * @param {string} cwd
   * @param {string} currentVersion
   * @param {Map<string, { mtime: number, sigs: string[] }>} cache
   */
  function saveCache(cwd, currentVersion, cache) {
    try {
      const data = {
        sigmapVersion: currentVersion,
        entries: Object.fromEntries(cache),
      };
      fs.writeFileSync(cachePath(cwd), JSON.stringify(data), 'utf8');
    } catch (_) {
      // Non-fatal: cache save failure just means a full re-extract next run
    }
  }

  /**
   * Given a list of absolute file paths, return only those whose mtime
   * differs from the cached value (or that are not cached at all).
   *
   * @param {string[]} files - absolute paths
   * @param {Map<string, { mtime: number, sigs: string[] }>} cache
   * @returns {{ changed: string[], unchanged: string[] }}
   */
  function getChangedFiles(files, cache) {
    const changed = [];
    const unchanged = [];
    for (const f of files) {
      try {
        const mtime = fs.statSync(f).mtimeMs;
        const cached = cache.get(f);
        if (!cached || cached.mtime !== mtime) {
          changed.push(f);
        } else {
          unchanged.push(f);
        }
      } catch (_) {
        changed.push(f); // file unreadable → treat as changed
      }
    }
    return { changed, unchanged };
  }

  /**
   * Update cache entries for a batch of files after fresh extraction.
   *
   * @param {Map<string, { mtime: number, sigs: string[] }>} cache
   * @param {{ file: string, sigs: string[] }[]} extracted - freshly extracted results
   */
  function updateCacheEntries(cache, extracted) {
    for (const { file, sigs } of extracted) {
      try {
        const mtime = fs.statSync(file).mtimeMs;
        cache.set(file, { mtime, sigs });
      } catch (_) {}
    }
  }

  module.exports = { loadCache, saveCache, getChangedFiles, updateCacheEntries };
  
};

// ── ./src/config/defaults ──
__factories["./src/config/defaults"] = function(module, exports) {
  
  /**
   * Default configuration values for SigMap.
   * All keys documented here. Override via gen-context.config.json.
   */
  const DEFAULTS = {
    // Primary output file (used when outputs includes 'copilot')
    output: '.github/copilot-instructions.md',

    // Output targets: 'copilot' | 'claude' | 'cursor' | 'windsurf'
    outputs: ['copilot'],

    // Adapter targets (v3.0+): replaces 'outputs'. Same names, adds 'openai' | 'gemini'.
    // Old 'outputs' config key is still accepted and silently maps to 'adapters'.
    adapters: null,

    // Directories to scan (relative to project root)
    srcDirs: [
      'src', 'app', 'lib', 'packages', 'services', 'api',
      // common monorepo / multi-project top-level names
      'server', 'client', 'web', 'frontend', 'backend',
      'desktop', 'mobile', 'shared', 'common', 'core',
      'workers', 'functions', 'lambda', 'cmd',
      // framework convention folders
      'pages', 'components', 'hooks', 'routes', 'controllers',
      'models', 'views', 'resources', 'config', 'db',
      'projects', 'apps', 'libs', 'instance', 'blueprints',
      // JVM project structures (Java, Kotlin, Scala)
      'src/main/java', 'src/main/kotlin', 'src/main/scala',
      'app/src/main/java', 'app/src/main/kotlin',
      'src/test/java', 'src/test/kotlin',
    ],

    // Directory/file names to exclude entirely
    exclude: [
      'node_modules', '.git', 'dist', 'build', 'out',
      '__pycache__', '.next', 'coverage', 'target', 'vendor',
      '.context',
      // CI/test artifacts
      'playwright-tmp', 'playwright-report', 'test-results',
      // build/monorepo caches
      '.turbo',
      // documentation build output
      'storybook-static', '.docusaurus',
    ],

    // Maximum directory depth to recurse
    maxDepth: 6,

    // Maximum signatures extracted per file
    maxSigsPerFile: 25,

    // Maximum tokens in final output before budget enforcement kicks in.
    // Used only when autoMaxTokens is false, or as a floor for auto-scaling.
    maxTokens: 6000,

    // Automatically scale the token budget based on repo size.
    // When true, SigMap targets `coverageTarget` fraction of source files and
    // raises the budget up to `modelContextLimit * maxTokensHeadroom`.
    // Set to false (or set maxTokens explicitly) to pin the budget.
    autoMaxTokens: true,

    // Fraction of source files to target for inclusion (0.0–1.0).
    // 0.80 = include at least 80% of source files in the context output.
    coverageTarget: 0.80,

    // Model context window size (tokens). Used to compute the hard cap:
    //   hardCap = modelContextLimit × maxTokensHeadroom
    // Default: GPT-4o / Claude Sonnet (128K). Set higher for Gemini 1M etc.
    modelContextLimit: 128000,

    // Fraction of the model context window reserved for SigMap output.
    // Leaves the remaining fraction for the conversation, system prompt, etc.
    // Default 0.20 = 20% of 128K = 25,600 token hard cap.
    maxTokensHeadroom: 0.20,

    // Scan signatures for secrets and redact matches
    secretScan: true,

    // Auto-detect monorepo packages and write per-package output files
    monorepo: false,

    // Sort recently git-committed files higher in output
    diffPriority: true,

    // Context strategy controls how the output is split and injected.
    // 'full'       -> single context file (default)
    // 'per-module' -> one context-<module>.md per top-level srcDir + thin overview
    // 'hot-cold'   -> recent files in primary output, older files in context-cold.md
    strategy: 'full',

    // For hot-cold strategy: how many recent git commits count as "hot"
    hotCommits: 10,

    // Debounce delay (ms) between file-system events and regeneration in watch mode
    watchDebounce: 300,

    // Append model routing hints section to the context output
    // Routes files to fast/balanced/powerful model tiers based on complexity
    routing: false,

    // Output format: 'default' (markdown only) | 'cache' (also write Anthropic prompt-cache JSON)
    format: 'default',

    // Append run metrics to .context/usage.ndjson after each generate
    tracking: false,

    // MCP server configuration
    mcp: {
      autoRegister: true,
    },

    // Include a compact import dependency map at top of output
    depMap: true,

    // Include a compact `name@version` list of installed direct deps (D8)
    versionPins: true,

    // Include TODO/FIXME/HACK/XXX comments as compact section
    todos: true,

    // Include compact recent git changes section
    changes: true,

    // Number of commits used for changes section
    changesCommits: 10,

    // Add test coverage markers to extracted function signatures (opt-in)
    testCoverage: false,

    // Directories scanned for tests when testCoverage is enabled
    testDirs: ['tests', 'test', '__tests__', 'spec'],

    // Enable incremental signature cache (v6.7) - only re-extract changed files
    sigCache: false,

    // Add reverse dependency usage hints on file headings (opt-in)
    impactRadius: false,

    // Query-aware retrieval settings (v2.3)
    retrieval: {
      // Maximum number of files to return for --query
      topK: 10,
      // Multiplier applied to recently-changed files (>1 boosts them up)
      recencyBoost: 1.5,
    },

    // Impact layer settings (v2.5)
    impact: {
      // BFS traversal depth limit for --impact (0 = unlimited)
      depth: 3,
      // Include signatures of impacted files in --impact output
      includeSigs: true,
    },
  };

  module.exports = { DEFAULTS };
  
};

// ── ./src/config/loader ──
__factories["./src/config/loader"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');
  const { DEFAULTS } = __require('./src/config/defaults');

  const BASE_CONFIG_TTL_MS = 60 * 60 * 1000; // 1 hour

  function loadBaseConfig(extendsVal, cwd) {
    if (!extendsVal || typeof extendsVal !== 'string') return {};

    if (extendsVal.startsWith('https://') || extendsVal.startsWith('http://')) {
      const cacheDir  = path.join(cwd, '.context', 'config-cache');
      const cacheKey  = Buffer.from(extendsVal).toString('base64url').replace(/[^a-zA-Z0-9_-]/g, '_');
      const cachePath = path.join(cacheDir, `${cacheKey}.json`);

      if (fs.existsSync(cachePath)) {
        const age = Date.now() - fs.statSync(cachePath).mtimeMs;
        if (age < BASE_CONFIG_TTL_MS) {
          try { return JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch (_) {}
        }
      }

      try {
        const https = require('https');
        const http  = require('http');
        const mod   = extendsVal.startsWith('https://') ? https : http;
        const raw   = (() => {
          let data = '';
          return new Promise((resolve, reject) => {
            mod.get(extendsVal, (res) => {
              res.on('data', (c) => { data += c; });
              res.on('end', () => resolve(data));
            }).on('error', reject);
          });
        })();
        // Sync fallback: run a tiny fetch in a child node process. Shell-free —
        // execFileSync runs the node binary directly (no /bin/sh) and the URL is
        // passed as an argv value, never interpolated into a command string.
        const { execFileSync } = require('child_process');
        const SYNC_FETCH = "const u=process.argv[1];const h=require(u.startsWith('https')?'https':'http');let d='';h.get(u,r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))";
        const out = execFileSync(process.execPath, ['-e', SYNC_FETCH, extendsVal], { timeout: 10000, encoding: 'utf8' });
        const parsed = JSON.parse(out);
        if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
        fs.writeFileSync(cachePath, JSON.stringify(parsed), 'utf8');
        return parsed;
      } catch (err) {
        process.stderr.write(`[sigmap] config extends: could not fetch ${extendsVal}: ${err.message}\n`);
        if (fs.existsSync(cachePath)) {
          try { return JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch (_) {}
        }
        return {};
      }
    }

    // Local file path
    const absPath = path.resolve(cwd, extendsVal);
    try {
      return JSON.parse(fs.readFileSync(absPath, 'utf8'));
    } catch (err) {
      process.stderr.write(`[sigmap] config extends: could not load ${absPath}: ${err.message}\n`);
      return {};
    }
  }

  // Keys that are valid in gen-context.config.json
  const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));

  // Common top-level folder names that reliably hold source code
  const COMMON_CODE_DIRS = new Set([
    'src', 'app', 'lib', 'packages', 'services', 'api', 'core', 'cmd',
    'internal', 'pkg', 'handlers', 'controllers', 'models', 'views',
    'components', 'pages', 'routes', 'middleware', 'utils', 'helpers',
    'modules', 'plugins', 'extensions', 'adapters', 'drivers',
    'examples', 'sample', 'demo', 'tests', 'test', 'spec', '__tests__',
    'hooks', 'composables', 'stores', 'features', 'domain', 'infra',
    'infrastructure', 'application', 'data', 'Sources', 'Tests',
  ]);

  const SUPPORTED_CODE_EXTS = new Set([
    '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
    '.py', '.pyw', '.java', '.kt', '.kts', '.go', '.rs', '.cs',
    '.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.rake', '.php',
    '.swift', '.dart', '.scala', '.sc', '.vue', '.svelte',
    '.html', '.htm', '.css', '.scss', '.sass', '.less',
    '.yml', '.yaml', '.sh', '.bash', '.zsh', '.fish',
    '.sql', '.graphql', '.gql', '.tf', '.tfvars', '.proto',
    '.toml', '.properties', '.xml', '.md',
  ]);

  /**
   * Detect source directories for the given project root.
   * Uses smart resolver (v6.5+) with fallback to legacy heuristics.
   *
   * @param {string} cwd - Project root
   * @param {string[]} excludeList - Folders to skip
   * @returns {string[]}
   */
  function detectAutoSrcDirs(cwd, excludeList) {
    try {
      const { resolveSourceRoots } = __require('./src/discovery/source-root-resolver');
      const result = resolveSourceRoots(cwd, { exclude: excludeList || [] });
      if (result.roots.length > 0) {
        if (result.confidence === 'low') {
          process.stderr.write(
            '[sigmap] low confidence root detection — run "sigmap roots --explain" to verify\n'
          );
        }
        return result.roots;
      }
    } catch (_) {}

    return _legacyDetectAutoSrcDirs(cwd, excludeList);
  }

  /**
   * Legacy source directory detection (fallback).
   *
   * @param {string} cwd - Project root
   * @param {string[]} excludeList - Folders to skip
   * @returns {string[]}
   */
  function _legacyDetectAutoSrcDirs(cwd, excludeList) {
    const excludeSet = new Set(excludeList || []);
    const candidates = new Set(DEFAULTS.srcDirs);

    // ── Manifest-based detection ──────────────────────────────────────────────
    const pkgPath = path.join(cwd, 'package.json');
    if (fs.existsSync(pkgPath)) {
      try {
        const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
        const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
        if (allDeps.react || allDeps.next) {
          for (const d of ['src', 'app', 'pages', 'components', 'hooks', 'lib', 'utils']) candidates.add(d);
        }
        if (allDeps['@angular/core']) {
          for (const d of ['src', 'projects', 'apps', 'libs']) candidates.add(d);
        }
        if (allDeps['@nestjs/core']) {
          for (const d of ['src', 'libs', 'apps']) candidates.add(d);
        }
        if (allDeps.vue) {
          for (const d of ['src', 'components', 'views', 'stores', 'composables', 'plugins']) candidates.add(d);
        }
        if (allDeps.svelte || allDeps['@sveltejs/kit']) {
          for (const d of ['src', 'lib', 'routes']) candidates.add(d);
        }
        if (allDeps.nx || allDeps.turbo || allDeps.lerna || pkg.workspaces) {
          for (const d of ['packages', 'apps', 'libs', 'services']) candidates.add(d);
        }
      } catch (_) {}
    }

    const hasPyproject = fs.existsSync(path.join(cwd, 'pyproject.toml'));
    const hasRequirements = fs.existsSync(path.join(cwd, 'requirements.txt'));
    const hasSetupPy = fs.existsSync(path.join(cwd, 'setup.py'));
    if (hasPyproject || hasRequirements || hasSetupPy) {
      for (const d of ['src', 'app', 'apps', 'tests', 'examples', 'instance', 'blueprints']) candidates.add(d);
    }

    if (fs.existsSync(path.join(cwd, 'Gemfile'))) {
      for (const d of ['app', 'lib', 'config', 'db', 'spec', 'test']) candidates.add(d);
    }

    if (fs.existsSync(path.join(cwd, 'composer.json'))) {
      for (const d of ['app', 'resources', 'routes', 'database', 'tests']) candidates.add(d);
    }

    if (fs.existsSync(path.join(cwd, 'go.mod'))) {
      for (const d of ['cmd', 'internal', 'pkg', 'api', 'handler', 'handlers', 'middleware', 'service']) candidates.add(d);
    }

    if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
      for (const d of ['src', 'crates', 'examples', 'tests', 'benches']) candidates.add(d);
    }

    const hasGradle = fs.existsSync(path.join(cwd, 'build.gradle')) ||
                      fs.existsSync(path.join(cwd, 'build.gradle.kts'));
    const hasMaven = fs.existsSync(path.join(cwd, 'pom.xml'));
    if (hasGradle || hasMaven) {
      for (const d of [
        'src/main/java', 'src/main/kotlin', 'src/main/scala',
        'src/main/resources', 'src/test/java', 'src/test/kotlin',
      ]) candidates.add(d);
    }

    if (fs.existsSync(path.join(cwd, 'pubspec.yaml'))) {
      for (const d of ['lib', 'test', 'integration_test', 'example', 'bin']) candidates.add(d);
    }

    if (fs.existsSync(path.join(cwd, 'Package.swift'))) {
      for (const d of ['Sources', 'Tests']) candidates.add(d);
    }

    // ── Top-level directory scan ──────────────────────────────────────────────
    try {
      const entries = fs.readdirSync(cwd, { withFileTypes: true });
      for (const entry of entries) {
        if (!entry.isDirectory()) continue;
        if (entry.name.startsWith('.')) continue;
        if (excludeSet.has(entry.name)) continue;

        const lname = entry.name.toLowerCase();
        if (COMMON_CODE_DIRS.has(entry.name) || COMMON_CODE_DIRS.has(lname)) {
          candidates.add(entry.name);
          continue;
        }
        // Unknown dir: add if it directly contains source files
        const dirPath = path.join(cwd, entry.name);
        try {
          const subs = fs.readdirSync(dirPath, { withFileTypes: true });
          const hasSrc = subs.some((s) => {
            if (!s.isFile()) return false;
            return SUPPORTED_CODE_EXTS.has(path.extname(s.name).toLowerCase()) || s.name === 'Dockerfile';
          });
          if (hasSrc) { candidates.add(entry.name); continue; }
          const hasSrcSub = subs.some((s) =>
            s.isDirectory() && ['src', 'lib', 'main', 'java', 'kotlin', 'scala', 'python'].includes(s.name));
          if (hasSrcSub) candidates.add(entry.name);
        } catch (_) {}
      }
    } catch (_) {}

    // Only return those that exist
    return Array.from(candidates).filter((d) => {
      try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
    });
  }

  /**
   * Load and merge configuration for a given working directory.
   *
   * @param {string} cwd - Project root directory
   * @returns {object} Merged config (DEFAULTS + user overrides)
   */
  function loadConfig(cwd) {
    const configPath = path.join(cwd, 'gen-context.config.json');
    if (!fs.existsSync(configPath)) {
      const cfg = deepClone(DEFAULTS);
      const detected = detectAutoSrcDirs(cwd, cfg.exclude);
      if (detected.length > 0) cfg.srcDirs = detected;
      return cfg;
    }

    let userConfig;
    try {
      const raw = fs.readFileSync(configPath, 'utf8');
      userConfig = JSON.parse(raw);
    } catch (err) {
      console.warn(`[sigmap] config parse error in ${configPath}: ${err.message}`);
      const cfg = deepClone(DEFAULTS);
      const detected = detectAutoSrcDirs(cwd, cfg.exclude);
      if (detected.length > 0) cfg.srcDirs = detected;
      return cfg;
    }

    // Warn on unknown keys (helps catch typos)
    for (const key of Object.keys(userConfig)) {
      if (key.startsWith('_') || key === 'extends') continue;
      if (!KNOWN_KEYS.has(key)) {
        console.warn(`[sigmap] unknown config key: "${key}" (ignored)`);
      }
    }

    // Deep merge: DEFAULTS → base (extends) → user config
    const baseConfig = loadBaseConfig(userConfig.extends, cwd);
    const merged = deepClone(DEFAULTS);

    for (const key of Object.keys(baseConfig)) {
      if (key.startsWith('_') || key === 'extends') continue;
      if (!KNOWN_KEYS.has(key)) continue;
      const val = baseConfig[key];
      if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
          typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
        merged[key] = Object.assign({}, merged[key], val);
      } else {
        merged[key] = val;
      }
    }

    for (const key of Object.keys(userConfig)) {
      if (key.startsWith('_') || key === 'extends') continue;
      if (!KNOWN_KEYS.has(key)) continue; // skip unknown keys
      const val = userConfig[key];
      if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
          typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
        merged[key] = Object.assign({}, merged[key], val);
      } else {
        merged[key] = val;
      }
    }

    // If user didn't specify srcDirs, auto-detect; fall back to DEFAULTS if nothing found
    if (!Array.isArray(userConfig.srcDirs)) {
      const detected = detectAutoSrcDirs(cwd, merged.exclude);
      merged.srcDirs = detected.length > 0 ? detected : deepClone(DEFAULTS.srcDirs);
    }

    // Backward compat (v3.0+): mirror outputs ↔ adapters
    if (merged.adapters && !Array.isArray(merged.adapters)) merged.adapters = null;
    if (!merged.adapters && Array.isArray(merged.outputs)) {
      merged.adapters = merged.outputs.slice();
    } else if (Array.isArray(merged.adapters) && !userConfig.outputs) {
      merged.outputs = merged.adapters.filter((a) => ['copilot','claude','cursor','windsurf'].includes(a));
    }
    return merged;
  }

  function deepClone(obj) {
    return JSON.parse(JSON.stringify(obj));
  }

  module.exports = { loadConfig, loadBaseConfig };
  
};

// ── ./src/conventions/ci ──
__factories["./src/conventions/ci"] = function(module, exports) {
  
  /**
   * Convention CI gate (IMPL.md §4 — `conventions --ci`).
   *
   * Fails CI when a repo's overall convention consistency is below a threshold,
   * and optionally when it regresses vs the last recorded run. Builds on the
   * `--report` score. Pure, zero-dependency, bundle-safe.
   */

  const { overallScore } = __require('./src/conventions/report');

  const DEFAULT_MIN = 0.7;
  const EPS = 1e-9;

  /**
   * Evaluate the consistency gate.
   * @param {object} result an `extractConventions` result
   * @param {object} [opts]
   * @param {number} [opts.min=0.7] minimum overall consistency (0–1)
   * @param {boolean} [opts.noRegress=false] also fail if the score dropped vs prior
   * @param {object|null} [prior] the previous snapshot (from `report.snapshot`)
   * @returns {{ score:number, min:number, ok:boolean, regressed:boolean, reasons:string[] }}
   */
  function ciGate(result, opts = {}, prior = null) {
    const min = opts.min != null ? opts.min : DEFAULT_MIN;
    const score = overallScore(result);
    const reasons = [];
    let ok = true;

    if (score < min) {
      ok = false;
      reasons.push(`consistency ${(score * 100).toFixed(0)}% below min ${(min * 100).toFixed(0)}%`);
    }

    let regressed = false;
    if (opts.noRegress && prior && typeof prior.score === 'number') {
      if (score < prior.score - EPS) {
        regressed = true;
        ok = false;
        reasons.push(`consistency dropped ${(prior.score * 100).toFixed(0)}% → ${(score * 100).toFixed(0)}%`);
      }
    }

    return { score, min, ok, regressed, reasons };
  }

  module.exports = { ciGate, DEFAULT_MIN };
  
};

// ── ./src/conventions/conflicts ──
__factories["./src/conventions/conflicts"] = function(module, exports) {
  
  /**
   * Convention conflict analysis (IMPL.md §4 / §5.3 — grounded codegen, Layer 3).
   *
   * Given an `extractConventions` result, surface *why* a convention is mixed:
   * every variant pattern with its file count, share, and example files, plus
   * rename suggestions that move minority file-naming files toward the dominant
   * style. Pure, zero-dependency, bundle-safe.
   */

  /** Split a file name into its stem (before the first dot) and the rest. */
  function _splitName(filename) {
    const s = String(filename || '');
    const dot = s.indexOf('.');
    if (dot <= 0) return { stem: s, ext: '' };
    return { stem: s.slice(0, dot), ext: s.slice(dot) };
  }

  /** Break a stem into lowercase word parts regardless of its current style. */
  function _words(stem) {
    return String(stem || '')
      .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camel/Pascal boundaries
      .replace(/[-_]+/g, ' ')                 // kebab / snake separators
      .trim()
      .split(/\s+/)
      .filter(Boolean)
      .map((w) => w.toLowerCase());
  }

  const _cap = (w) => w.charAt(0).toUpperCase() + w.slice(1);

  /**
   * Convert a file stem to a target naming style.
   * @param {string} stem name without extension
   * @param {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'} style
   * @returns {string}
   */
  function toNamingStyle(stem, style) {
    const w = _words(stem);
    if (w.length === 0) return String(stem || '');
    switch (style) {
      case 'PascalCase': return w.map(_cap).join('');
      case 'camelCase': return w[0] + w.slice(1).map(_cap).join('');
      case 'kebab-case': return w.join('-');
      case 'snake_case': return w.join('_');
      default: return String(stem || '');
    }
  }

  /** Rename suggestion to bring a file to the dominant naming style. */
  function renameSuggestion(filename, dominantStyle) {
    const { stem, ext } = _splitName(filename);
    const to = toNamingStyle(stem, dominantStyle) + ext;
    return { from: filename, to };
  }

  const LABELS = {
    fileNaming: 'file naming',
    exportStyle: 'export style',
  };

  /**
   * Analyze an `extractConventions` result for conflicts.
   * @param {object} result the object returned by `extractConventions`
   * @returns {{ hasConflicts: boolean, conventions: Array<{
   *   key:string, name:string, dominant:string|null, dominantPct:number,
   *   tier:string, total:number,
   *   variants:Array<{pattern:string,count:number,pct:number,examples:string[]}>,
   *   renames:Array<{from:string,to:string}> }> }}
   */
  function analyzeConflicts(result) {
    const out = [];
    for (const key of ['fileNaming', 'exportStyle']) {
      const conv = result && result[key];
      // A conflict is any convention with more than one observed pattern.
      if (!conv || conv.total === 0 || conv.variants.length < 2) continue;

      const variants = conv.variants.map((v) => ({
        pattern: v.label,
        count: v.count,
        pct: v.pct,
        examples: v.examples || [],
      }));

      // Rename suggestions only for file naming (export style is a code change, not a rename).
      const renames = [];
      if (key === 'fileNaming' && conv.dominant) {
        for (const v of conv.variants) {
          if (v.label === conv.dominant) continue;
          for (const ex of (v.examples || [])) {
            renames.push(renameSuggestion(ex, conv.dominant));
          }
        }
      }

      out.push({
        key,
        name: LABELS[key] || key,
        dominant: conv.dominant,
        dominantPct: conv.dominantPct,
        tier: conv.tier,
        total: conv.total,
        variants,
        renames,
      });
    }
    return { hasConflicts: out.length > 0, conventions: out };
  }

  module.exports = { analyzeConflicts, toNamingStyle, renameSuggestion };
  
};

// ── ./src/conventions/extract ──
__factories["./src/conventions/extract"] = function(module, exports) {
  
  /**
   * Convention extraction (IMPL.md Layer 3 — grounded code generation).
   *
   * Detects a repo's dominant coding conventions so generated code matches the
   * house style instead of drifting (Cause 4: naming/convention drift). This
   * first slice covers TS/JS/Python and three conventions: file naming, export
   * style, and test framework. `scoreConvention` is the reusable consistency
   * primitive that Gap 1 (scaffold confidence) will also build on.
   *
   * Zero dependencies, bundle-safe (fs + path only).
   */

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

  const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
  const PY_EXTS = new Set(['.py']);
  const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);

  // Consistency tiers (IMPL.md §5.1): a convention is only safe to enforce when
  // it is actually consistent.
  const TIER_CONSISTENT = 0.9;
  const TIER_MOSTLY = 0.7;

  /**
   * Classify a file's base name (without extension) into a naming style.
   * @param {string} basename a file basename, e.g. "user-service.ts"
   * @returns {'PascalCase'|'camelCase'|'kebab-case'|'snake_case'|'other'}
   */
  function classifyNaming(basename) {
    let stem = String(basename || '');
    const dot = stem.indexOf('.');
    if (dot > 0) stem = stem.slice(0, dot); // strip ext + compound suffix (.test, .d)
    if (!stem) return 'other';
    if (/[-]/.test(stem) && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(stem)) return 'kebab-case';
    if (/[_]/.test(stem) && /^[a-z0-9]+(?:_[a-z0-9]+)+$/.test(stem)) return 'snake_case';
    if (/^[A-Z][A-Za-z0-9]*$/.test(stem) && /[a-z]/.test(stem)) return 'PascalCase';
    if (/^[a-z][A-Za-z0-9]*$/.test(stem) && /[A-Z]/.test(stem)) return 'camelCase';
    if (/^[a-z][a-z0-9]*$/.test(stem)) return 'camelCase'; // single lowercase word
    return 'other';
  }

  const MAX_EXAMPLES = 3;

  /**
   * Score a set of categorical observations into a dominant convention plus its
   * consistency tier. The reusable primitive (IMPL.md §5.2).
   * @param {string[]} labels observed category for each sample (e.g. naming styles)
   * @param {string[]} [refs] optional identifier (e.g. file name) parallel to
   *   `labels`; when given, each variant carries up to 3 `examples`.
   * @returns {{ dominant: string|null, dominantPct: number, total: number,
   *             variants: Array<{label:string, count:number, pct:number, examples?:string[]}>,
   *             tier: 'consistent'|'mostly'|'inconsistent'|'unknown' }}
   */
  function scoreConvention(labels, refs) {
    const all = labels || [];
    const counts = new Map();
    const examples = new Map();
    let total = 0;
    for (let i = 0; i < all.length; i++) {
      const l = all[i];
      if (l == null || l === 'other') continue;
      total++;
      counts.set(l, (counts.get(l) || 0) + 1);
      if (refs && refs[i] != null) {
        const ex = examples.get(l) || [];
        if (ex.length < MAX_EXAMPLES) { ex.push(refs[i]); examples.set(l, ex); }
      }
    }
    if (total === 0) {
      return { dominant: null, dominantPct: 0, total: 0, variants: [], tier: 'unknown' };
    }
    const variants = [...counts.entries()]
      .map(([label, count]) => {
        const v = { label, count, pct: count / total };
        if (refs) v.examples = examples.get(label) || [];
        return v;
      })
      .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
    const top = variants[0];
    let tier = 'inconsistent';
    if (top.pct >= TIER_CONSISTENT) tier = 'consistent';
    else if (top.pct >= TIER_MOSTLY) tier = 'mostly';
    return {
      dominant: top.label,
      dominantPct: top.pct,
      total,
      variants,
      tier,
    };
  }

  /** Detect JS/TS export style for a single file's source. */
  function _jsExportStyle(src) {
    const s = String(src || '');
    if (/\bexport\s+default\b/.test(s) || /\bmodule\.exports\s*=\s*(?:function|class|\{?\s*[A-Za-z_$])/.test(s)) {
      // module.exports = { a, b } reads as named; only treat bare assignment as default.
      if (/\bexport\s+default\b/.test(s)) return 'default';
      if (/\bmodule\.exports\s*=\s*\{/.test(s)) return 'named';
      return 'default';
    }
    if (/\bexport\s+(?:const|let|var|function|class|async\s+function|\{|type|interface|enum)\b/.test(s)
      || /\bmodule\.exports\s*=\s*\{/.test(s) || /\bexports\.[A-Za-z_$]/.test(s)) {
      return 'named';
    }
    return 'other';
  }

  /** Detect the test framework in use from manifests + source heuristics. */
  function _detectTestFramework(cwd, files) {
    const deps = {};
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      Object.assign(deps, pkg.dependencies, pkg.devDependencies);
    } catch (_) {}
    for (const fw of ['vitest', 'jest', 'mocha', 'ava', 'jasmine']) {
      if (deps[fw]) return fw;
    }
    // Python: pytest in requirements / pyproject.
    for (const manifest of ['requirements.txt', 'requirements-dev.txt', 'pyproject.toml', 'setup.cfg']) {
      try {
        const raw = fs.readFileSync(path.join(cwd, manifest), 'utf8');
        if (/\bpytest\b/.test(raw)) return 'pytest';
        if (/\bunittest\b/.test(raw)) return 'unittest';
      } catch (_) {}
    }
    // Fallback: infer from test file contents.
    for (const f of files) {
      if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) continue;
      let src = '';
      try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
      if (/\bvi\.(fn|mock|spyOn)\b|from ['"]vitest['"]/.test(src)) return 'vitest';
      if (/\bjest\.(fn|mock|spyOn)\b/.test(src)) return 'jest';
      if (/\bimport pytest\b|@pytest\./.test(src)) return 'pytest';
      if (/\bdescribe\(|\bit\(/.test(src)) return 'mocha/jest-style';
    }
    return null;
  }

  /**
   * Extract repo coding conventions for the scoped languages (TS/JS/Python).
   * @param {string} cwd repo root
   * @param {string[]} files absolute paths to source files (e.g. from buildFileList)
   * @returns {{ fileNaming: object, exportStyle: object, testFramework: string|null,
   *             scope: string[], scannedFiles: number }}
   */
  function extractConventions(cwd, files) {
    const scoped = (files || []).filter((f) => SCOPED_EXTS.has(path.extname(f).toLowerCase()));
    const namingLabels = [];
    const namingRefs = [];
    const exportLabels = [];
    const exportRefs = [];
    for (const f of scoped) {
      const base = path.basename(f);
      // Skip test files for the naming convention (they have their own naming).
      if (!/\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/.test(f)) {
        namingLabels.push(classifyNaming(base));
        namingRefs.push(base);
      }
      if (JS_TS_EXTS.has(path.extname(f).toLowerCase())) {
        let src = '';
        try { src = fs.readFileSync(f, 'utf8'); } catch (_) {}
        exportLabels.push(_jsExportStyle(src));
        exportRefs.push(base);
      }
    }
    return {
      fileNaming: scoreConvention(namingLabels, namingRefs),
      exportStyle: scoreConvention(exportLabels, exportRefs),
      testFramework: _detectTestFramework(cwd, scoped),
      scope: ['typescript', 'javascript', 'python'],
      scannedFiles: scoped.length,
    };
  }

  module.exports = { classifyNaming, scoreConvention, extractConventions };
  
};

// ── ./src/conventions/fix ──
__factories["./src/conventions/fix"] = function(module, exports) {
  
  /**
   * Convention fix list (IMPL.md §4 — `conventions --fix`).
   *
   * The complete, actionable rename checklist: every scoped source file whose
   * name doesn't match the dominant file-naming convention, with full from→to
   * paths. Distinct from `--conflicts` (a diagnostic summary with up to 3 example
   * basenames) — `--fix` lists *every* offending file with its real path, ready
   * to paste into a task or PR. Pure, zero-dependency, bundle-safe.
   */

  const path = require('path');
  const { classifyNaming } = __require('./src/conventions/extract');
  const { toNamingStyle } = __require('./src/conventions/conflicts');

  const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
  const PY_EXTS = new Set(['.py']);
  const SCOPED_EXTS = new Set([...JS_TS_EXTS, ...PY_EXTS]);

  const TEST_RE = /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.py$/;

  /** Rename a file path's basename to the target naming style (keep dir + ext). */
  function _renamePath(relPath, style) {
    const dir = relPath.includes('/') ? relPath.slice(0, relPath.lastIndexOf('/') + 1) : '';
    const base = relPath.includes('/') ? relPath.slice(relPath.lastIndexOf('/') + 1) : relPath;
    const dot = base.indexOf('.');
    const stem = dot > 0 ? base.slice(0, dot) : base;
    const ext = dot > 0 ? base.slice(dot) : '';
    return `${dir}${toNamingStyle(stem, style)}${ext}`;
  }

  /**
   * Build the exhaustive rename checklist for the dominant file-naming convention.
   * @param {string} cwd repo root (for relative paths)
   * @param {string[]} files absolute source paths (e.g. from buildFileList)
   * @param {object} conventions an `extractConventions` result (for the dominant style)
   * @returns {{ dominant: string|null, renames: Array<{from:string,to:string,fromStyle:string}>, count: number }}
   */
  function buildFixList(cwd, files, conventions) {
    const dominant = conventions && conventions.fileNaming && conventions.fileNaming.dominant;
    if (!dominant) return { dominant: null, renames: [], count: 0 };

    const renames = [];
    for (const f of files || []) {
      if (!SCOPED_EXTS.has(path.extname(f).toLowerCase())) continue;
      if (TEST_RE.test(f)) continue;
      const base = path.basename(f);
      const style = classifyNaming(base);
      if (style === 'other' || style === dominant) continue;
      const rel = path.relative(cwd, f).replace(/\\/g, '/');
      renames.push({ from: rel, to: _renamePath(rel, dominant), fromStyle: style });
    }
    renames.sort((a, b) => a.from.localeCompare(b.from));
    return { dominant, renames, count: renames.length };
  }

  module.exports = { buildFixList };
  
};

// ── ./src/conventions/inject ──
__factories["./src/conventions/inject"] = function(module, exports) {
  
  /**
   * CLAUDE.md convention injection (IMPL.md §7 Phase 2 / §8 step 5).
   *
   * Renders the conventions detected by `extractConventions` into a
   * marker-delimited markdown block and injects it into CLAUDE.md so an agent
   * reading the file plans grounded in the repo's house style. Idempotent and
   * marker-scoped — it never touches human content or the `## Auto-generated
   * signatures` block. Pure string transforms; zero-dependency, bundle-safe.
   */

  const START = '<!-- sigmap-conventions:start -->';
  const END = '<!-- sigmap-conventions:end -->';

  const TIER_NOTE = {
    consistent: 'consistent — match it',
    mostly: 'dominant, with some drift',
    inconsistent: 'no clear convention — check neighboring files',
  };

  const NAMES = {
    fileNaming: 'File naming',
    exportStyle: 'Export style',
  };

  const _pct = (n) => `${Math.round(n * 100)}%`;

  function _conventionLine(label, conv) {
    if (!conv || conv.total === 0 || !conv.dominant) return null;
    const note = TIER_NOTE[conv.tier] || conv.tier;
    let line = `- **${label}:** ${conv.dominant} (${_pct(conv.dominantPct)} — ${note}).`;
    const others = conv.variants.slice(1).filter((v) => v.pct > 0);
    if (others.length) {
      line += ` Variants: ${others.map((v) => `${v.label} ${_pct(v.pct)}`).join(', ')}.`;
    }
    return line;
  }

  /**
   * Render the conventions block (including its start/end markers).
   * @param {object} result an `extractConventions` result
   * @param {string} [version] SigMap version for the footer
   * @returns {string}
   */
  function renderConventionsBlock(result, version) {
    const lines = [];
    for (const key of ['fileNaming', 'exportStyle']) {
      const l = _conventionLine(NAMES[key], result && result[key]);
      if (l) lines.push(l);
    }
    if (result && result.testFramework) {
      lines.push(`- **Test framework:** ${result.testFramework}.`);
    }

    const body = lines.length
      ? lines
      : ['- No conventions detected yet (run `sigmap conventions` on a TS/JS/Python repo).'];

    const ver = version ? ` v${version}` : '';
    return [
      START,
      '## Conventions (auto-detected by SigMap)',
      '',
      'Match these when writing or editing code (TS/JS/Python):',
      '',
      ...body,
      '',
      `<sub>Generated by SigMap${ver} · run \`sigmap conventions --inject\` to refresh.</sub>`,
      END,
    ].join('\n');
  }

  /**
   * Inject (or replace) the conventions block in existing CLAUDE.md content.
   * Replaces an existing marked block in place; appends one when absent.
   * Idempotent — never touches content outside the markers.
   * @param {string} existing current file content ('' if the file is new)
   * @param {string} block the block returned by `renderConventionsBlock`
   * @returns {string}
   */
  function injectConventions(existing, block) {
    const src = String(existing || '');
    const startIdx = src.indexOf(START);
    if (startIdx !== -1) {
      const endIdx = src.indexOf(END, startIdx);
      if (endIdx !== -1) {
        const before = src.slice(0, startIdx);
        const after = src.slice(endIdx + END.length);
        return before + block + after;
      }
    }
    if (src.trim() === '') return block + '\n';
    const sep = src.endsWith('\n') ? '\n' : '\n\n';
    return src + sep + block + '\n';
  }

  module.exports = { renderConventionsBlock, injectConventions, START, END };
  
};

// ── ./src/conventions/report ──
__factories["./src/conventions/report"] = function(module, exports) {
  
  /**
   * Convention audit + trend (IMPL.md §4 — `conventions --report`).
   *
   * Turns an `extractConventions` result into an audit: a consistency score per
   * convention plus a single file-count-weighted overall score, each with a delta
   * vs the previous run (the trend). Pure, zero-dependency, bundle-safe.
   */

  const NAMES = { fileNaming: 'file naming', exportStyle: 'export style' };

  /** File-count-weighted mean of the scored conventions' dominant shares (0–1). */
  function overallScore(result) {
    let num = 0;
    let den = 0;
    for (const key of ['fileNaming', 'exportStyle']) {
      const c = result && result[key];
      if (c && c.total > 0) { num += c.dominantPct * c.total; den += c.total; }
    }
    return den > 0 ? num / den : 0;
  }

  /**
   * Build a consistency report with trend vs a prior snapshot.
   * @param {object} result an `extractConventions` result
   * @param {object|null} [prior] a previous snapshot (this module's `snapshot` shape)
   * @returns {{ conventions: object[], testFramework: string|null,
   *   score: number, prevScore: number|null, scoreDelta: number|null }}
   */
  function scoreReport(result, prior) {
    const conventions = [];
    for (const key of ['fileNaming', 'exportStyle']) {
      const c = (result && result[key]) || { dominant: null, dominantPct: 0, total: 0, tier: 'unknown' };
      const priorPct = prior && prior[key] && typeof prior[key].dominantPct === 'number' ? prior[key].dominantPct : null;
      conventions.push({
        key,
        name: NAMES[key] || key,
        dominant: c.dominant,
        dominantPct: c.dominantPct,
        tier: c.tier,
        total: c.total,
        delta: priorPct == null ? null : c.dominantPct - priorPct,
      });
    }
    const score = overallScore(result);
    const prevScore = prior && typeof prior.score === 'number' ? prior.score : null;
    return {
      conventions,
      testFramework: (result && result.testFramework) || null,
      score,
      prevScore,
      scoreDelta: prevScore == null ? null : score - prevScore,
    };
  }

  /**
   * A compact, persistable snapshot of a run (one line in the history log).
   * @param {object} result an `extractConventions` result
   * @param {string} [ts] ISO timestamp (caller supplies — keeps this pure)
   */
  function snapshot(result, ts) {
    const pick = (c) => (c && c.total > 0
      ? { dominant: c.dominant, dominantPct: c.dominantPct, tier: c.tier, total: c.total }
      : { dominant: null, dominantPct: 0, tier: 'unknown', total: 0 });
    return {
      ts: ts || null,
      fileNaming: pick(result && result.fileNaming),
      exportStyle: pick(result && result.exportStyle),
      testFramework: (result && result.testFramework) || null,
      score: overallScore(result),
    };
  }

  module.exports = { scoreReport, snapshot, overallScore };
  
};

// ── ./src/conventions/update ──
__factories["./src/conventions/update"] = function(module, exports) {
  
  /**
   * Convention incremental rescan (IMPL.md §4 — `conventions --update`).
   *
   * Avoids recomputing the conventions snapshot when nothing changed: compare the
   * source files' mtimes to the stored `.context/conventions.json` and only flag a
   * rescan when the snapshot is missing or some file is newer. Pure (fs reads
   * only), zero-dependency, bundle-safe.
   */

  const fs = require('fs');

  /**
   * Source files modified after a reference time.
   * @param {string[]} files absolute paths
   * @param {number} sinceMs epoch ms threshold
   * @returns {string[]}
   */
  function changedSince(files, sinceMs) {
    const out = [];
    for (const f of files || []) {
      try { if (fs.statSync(f).mtimeMs > sinceMs) out.push(f); } catch (_) {}
    }
    return out;
  }

  /**
   * Decide whether the conventions snapshot needs a rescan.
   * @param {string} cwd repo root (unused but kept for signature symmetry)
   * @param {string[]} files absolute source paths
   * @param {string} snapshotPath path to the stored conventions.json
   * @returns {{ snapshotExists: boolean, stale: boolean, changed: string[] }}
   */
  function planUpdate(cwd, files, snapshotPath) {
    let snapshotMs = null;
    try { snapshotMs = fs.statSync(snapshotPath).mtimeMs; } catch (_) {}
    const snapshotExists = snapshotMs != null;
    if (!snapshotExists) {
      return { snapshotExists: false, stale: true, changed: [] };
    }
    const changed = changedSince(files, snapshotMs);
    return { snapshotExists: true, stale: changed.length > 0, changed };
  }

  module.exports = { changedSince, planUpdate };
  
};

// ── ./src/create/orchestrate ──
__factories["./src/create/orchestrate"] = function(module, exports) {
  
  /**
   * create orchestrator (IMPL.md §6.2 — the capstone of the grounded-creation loop).
   *
   * Sequences the four guard stages — scaffold → verify-plan → verify-ai-output →
   * review-pr — in one pass with `n/4` numbering and a single pass/fail summary.
   * The agent does the LLM writing between stages; `create` runs the deterministic
   * guards it owns. A stage runs only when its input is present (else it is
   * skipped, which does not fail the run). Zero-dependency, bundle-safe; delegates
   * to the real stage modules.
   */

  const { proposeScaffold } = __require('./src/scaffold/propose');
  const { verifyPlan } = __require('./src/plan/verify-plan');
  const { verify } = __require('./src/verify/hallucination-guard');
  const { reviewPr } = __require('./src/review/review-pr');

  const TOTAL = 4;

  /**
   * Run the create pipeline over whatever inputs are available.
   * @param {object} ctx
   * @param {string} [ctx.task] free-text task label (echoed back)
   * @param {string} [ctx.name] module name → enables the scaffold stage
   * @param {object} [ctx.conventions] an `extractConventions` result (for scaffold)
   * @param {object} [ctx.scaffoldOpts] options forwarded to `proposeScaffold`
   * @param {string} [ctx.plan] plan markdown → enables verify-plan
   * @param {string} [ctx.answer] AI answer markdown → enables verify-ai-output
   * @param {Array<{path:string,status:string}>} [ctx.changedFiles] → enables review-pr
   * @param {string} cwd repo root
   * @returns {{ task: string|null, steps: object[], summary: object }}
   */
  function orchestrate(ctx = {}, cwd) {
    const steps = [];

    // 1/4 — scaffold (needs a name + conventions)
    if (ctx.name && ctx.conventions) {
      const d = proposeScaffold(ctx.name, ctx.conventions, ctx.scaffoldOpts || {});
      steps.push({ n: 1, total: TOTAL, name: 'scaffold', ran: true, ok: !!d.ok, skipped: false, detail: d });
    } else {
      steps.push({ n: 1, total: TOTAL, name: 'scaffold', ran: false, ok: null, skipped: true, reason: 'no --name' });
    }

    // 2/4 — verify-plan (needs a plan)
    if (ctx.plan != null && String(ctx.plan).trim() !== '') {
      const r = verifyPlan(ctx.plan, cwd);
      steps.push({ n: 2, total: TOTAL, name: 'verify-plan', ran: true, ok: !!r.summary.ok, skipped: false, detail: r });
    } else {
      steps.push({ n: 2, total: TOTAL, name: 'verify-plan', ran: false, ok: null, skipped: true, reason: 'no --plan' });
    }

    // 3/4 — verify-ai-output (needs an answer)
    if (ctx.answer != null && String(ctx.answer).trim() !== '') {
      const r = verify(ctx.answer, cwd);
      steps.push({ n: 3, total: TOTAL, name: 'verify-ai-output', ran: true, ok: r.summary.total === 0, skipped: false, detail: r });
    } else {
      steps.push({ n: 3, total: TOTAL, name: 'verify-ai-output', ran: false, ok: null, skipped: true, reason: 'no --answer' });
    }

    // 4/4 — review-pr (needs changed files)
    if (Array.isArray(ctx.changedFiles) && ctx.changedFiles.length) {
      const r = reviewPr(ctx.changedFiles, cwd);
      steps.push({ n: 4, total: TOTAL, name: 'review-pr', ran: true, ok: !!r.summary.ok, skipped: false, detail: r });
    } else {
      steps.push({ n: 4, total: TOTAL, name: 'review-pr', ran: false, ok: null, skipped: true, reason: 'no changes' });
    }

    const ran = steps.filter((s) => s.ran);
    const passed = ran.filter((s) => s.ok).length;
    const failed = ran.length - passed;
    return {
      task: ctx.task || null,
      steps,
      summary: {
        total: TOTAL,
        ran: ran.length,
        skipped: steps.length - ran.length,
        passed,
        failed,
        ok: failed === 0,
      },
    };
  }

  module.exports = { orchestrate, TOTAL };
  
};

// ── ./src/discovery/framework-detector ──
__factories["./src/discovery/framework-detector"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');
  const { REGISTRY } = __require('./src/discovery/source-root-registry');

  module.exports = { detectFrameworks };

  function detectFrameworks(cwd) {
    const detected = [];

    for (const [lang, reg] of Object.entries(REGISTRY)) {
      if (!reg.frameworks) continue;
      for (const [name, fw] of Object.entries(reg.frameworks)) {
        let confidence = 0;

        // Detection files: +0.95 / 0.93 / 0.90 depending on specificity
        for (const f of (fw.detectionFiles || [])) {
          if (_existsAnywhere(cwd, f, 3)) { confidence = Math.max(confidence, 0.93); }
        }

        // Detection deps in package.json
        if (fw.detectionDeps?.length) {
          const deps = _readDeps(cwd);
          for (const dep of fw.detectionDeps) {
            if (deps.has(dep)) { confidence = Math.max(confidence, 0.90); }
          }
        }

        // go.mod and Cargo.toml deps
        if (lang === 'go' && fw.detectionDeps?.length) {
          const goMod = _readFile(path.join(cwd, 'go.mod'));
          for (const dep of fw.detectionDeps) {
            if (goMod.includes(dep)) { confidence = Math.max(confidence, 0.90); }
          }
        }
        if (lang === 'rust' && fw.detectionDeps?.length) {
          const cargoToml = _readFile(path.join(cwd, 'Cargo.toml'));
          for (const dep of fw.detectionDeps) {
            if (cargoToml.includes(dep)) { confidence = Math.max(confidence, 0.88); }
          }
        }

        // Special rules
        if (fw.specialRule === 'django-app-dirs' && fs.existsSync(path.join(cwd, 'manage.py'))) {
          confidence = Math.max(confidence, 0.95);
        }
        if (fw.specialRule === 'swift-project-dir' && _existsAnywhere(cwd, '.xcodeproj', 2)) {
          confidence = Math.max(confidence, 0.90);
        }

        if (confidence > 0) detected.push({ name, language: lang, confidence });
      }
    }

    return detected.sort((a, b) => b.confidence - a.confidence);
  }

  function _readDeps(cwd) {
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      return new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})]);
    } catch { return new Set(); }
  }

  function _readFile(p) {
    try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
  }

  function _existsAnywhere(cwd, filename, maxDepth) {
    const parts = filename.split('/');
    if (parts.length > 1) return fs.existsSync(path.join(cwd, filename));
    return _walkFind(cwd, filename, maxDepth);
  }

  function _walkFind(dir, name, depth) {
    if (depth <= 0) return false;
    try {
      const entries = fs.readdirSync(dir, { withFileTypes: true });
      for (const e of entries) {
        if (e.name === name) return true;
        if (e.isDirectory() && depth > 1) {
          if (_walkFind(path.join(dir, e.name), name, depth - 1)) return true;
        }
      }
    } catch (_) {}
    return false;
  }
  
};

// ── ./src/discovery/language-detector ──
__factories["./src/discovery/language-detector"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');
  const { REGISTRY } = __require('./src/discovery/source-root-registry');

  module.exports = { detectLanguages };

  const SKIP_DIRS = new Set([
    'node_modules','dist','build','.git','venv','.venv','target',
    'DerivedData','Pods','.build','Carthage','coverage','.next','.nuxt',
    '__pycache__','.pytest_cache','vendor','.bundle','Carthage',
  ]);

  const EXT_TO_LANG = {
    '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
    '.ts': 'typescript', '.tsx': 'typescript', '.jsx': 'javascript',
    '.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust',
    '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
    '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
    '.dart': 'dart', '.scala': 'scala', '.php': 'php',
    '.gd': 'gdscript',
    '.r': 'r', '.R': 'r',
  };

  function detectLanguages(cwd) {
    const weights = {};

    // Signal 1: manifest files (+3 each)
    for (const [lang, reg] of Object.entries(REGISTRY)) {
      for (const mf of (reg.manifestFiles || [])) {
        if (fs.existsSync(path.join(cwd, mf))) {
          weights[lang] = (weights[lang] || 0) + 3;
        }
      }
    }

    // Signal 2: TypeScript dep in package.json (+2)
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
      if (allDeps.typescript) { weights.typescript = (weights.typescript || 0) + 2; }
    } catch (_) {}

    // Signal 3: file extension count (walk depth 3, capped at +5 per language)
    const extCount = {};
    _walkDepth(cwd, 3, extCount);
    const maxCount = Math.max(1, ...Object.values(extCount));
    for (const [ext, count] of Object.entries(extCount)) {
      const lang = EXT_TO_LANG[ext];
      if (lang) {
        weights[lang] = (weights[lang] || 0) + Math.min(5, (count / maxCount) * 5);
      }
    }

    // Normalize to [0,1] and sort
    const maxW = Math.max(1, ...Object.values(weights));
    return Object.entries(weights)
      .map(([name, w]) => ({ name, weight: Math.round(w / maxW * 100) / 100 }))
      .sort((a, b) => b.weight - a.weight);
  }

  function _walkDepth(dir, depth, extCount) {
    if (depth <= 0) return;
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
    for (const e of entries) {
      if (SKIP_DIRS.has(e.name)) continue;
      if (e.isDirectory()) {
        _walkDepth(path.join(dir, e.name), depth - 1, extCount);
      } else if (e.isFile()) {
        const ext = path.extname(e.name).toLowerCase();
        if (EXT_TO_LANG[ext]) extCount[ext] = (extCount[ext] || 0) + 1;
      }
    }
  }
  
};

// ── ./src/discovery/r-manifest ──
__factories["./src/discovery/r-manifest"] = function(module, exports) {
  
  /**
   * Parsers for R package manifest files (DESCRIPTION + NAMESPACE).
   *
   * Zero-dependency, regex/line-based. Both readers are safe on missing files
   * (return null) and on malformed input (return whatever could be parsed).
   *
   * Exports:
   *   readDescription(cwd) → { package, version, imports[], depends[], suggests[], linkingTo[] } | null
   *   readNamespace(cwd)   → { exports: Set, exportPatterns: RegExp[], s3methods: [{generic,class}], importFrom: Map<pkg, Set<name>> } | null
   *   collectLocalDefs(rFiles) → Map<defName, absPath>
   */

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

  /**
   * Parse a DESCRIPTION file (Debian control format).
   *
   * Continuation lines start with whitespace and are appended to the previous
   * field. Comma-separated dependency lists are split and version constraints
   * `pkg (>= 1.0.0)` are stripped to just `pkg`.
   *
   * @param {string} cwd - project root containing DESCRIPTION
   * @returns {object|null}
   */
  function readDescription(cwd) {
    const p = path.join(cwd, 'DESCRIPTION');
    if (!fs.existsSync(p)) return null;
    let raw;
    try { raw = fs.readFileSync(p, 'utf8'); } catch (_) { return null; }

    const fields = {};
    let currentKey = null;
    for (const rawLine of raw.split('\n')) {
      if (/^\s/.test(rawLine) && currentKey) {
        // Continuation: append (with a space) to the current field.
        fields[currentKey] += ' ' + rawLine.trim();
        continue;
      }
      const m = rawLine.match(/^([A-Za-z][\w.]*)\s*:\s*(.*)$/);
      if (m) {
        currentKey = m[1];
        fields[currentKey] = m[2].trim();
      } else {
        currentKey = null;
      }
    }

    return {
      package:   fields.Package   || null,
      version:   fields.Version   || null,
      imports:   splitDeps(fields.Imports),
      depends:   splitDeps(fields.Depends),
      suggests:  splitDeps(fields.Suggests),
      linkingTo: splitDeps(fields.LinkingTo),
    };
  }

  /**
   * Split a DESCRIPTION dep list like
   *   "dplyr (>= 1.0.0), ggplot2, R (>= 4.0)"
   * into a clean array of package names, dropping `R` itself.
   */
  function splitDeps(value) {
    if (!value) return [];
    return value.split(',')
      .map((s) => s.trim().replace(/\s*\([^)]*\)\s*$/, '').trim())
      .filter((s) => s && s !== 'R');
  }

  /**
   * Parse a NAMESPACE file.
   *
   * Recognised directives (R writes NAMESPACE in Lisp-y notation):
   *   export(name) / export("name")
   *   exportPattern("^foo")
   *   exportMethods(generic1, generic2)
   *   S3method(generic, class)
   *   importFrom(pkg, name1, name2, ...)
   *   import(pkg)
   *   useDynLib(pkg)            — ignored (compiled code)
   *
   * @param {string} cwd
   * @returns {object|null}
   */
  function readNamespace(cwd) {
    const p = path.join(cwd, 'NAMESPACE');
    if (!fs.existsSync(p)) return null;
    let raw;
    try { raw = fs.readFileSync(p, 'utf8'); } catch (_) { return null; }

    // Strip comments.
    const text = raw.replace(/#.*$/gm, '');

    const exports = new Set();
    const exportPatterns = [];
    const s3methods = [];
    const importFrom = new Map();

    for (const m of text.matchAll(/\bexport\s*\(\s*([^)]+)\)/g)) {
      for (const name of splitArgs(m[1])) {
        const clean = stripQuotes(name);
        if (clean) exports.add(clean);
      }
    }
    for (const m of text.matchAll(/\bexportMethods\s*\(\s*([^)]+)\)/g)) {
      for (const name of splitArgs(m[1])) {
        const clean = stripQuotes(name);
        if (clean) exports.add(clean);
      }
    }
    for (const m of text.matchAll(/\bexportPattern\s*\(\s*["']([^"']+)["']\s*\)/g)) {
      try { exportPatterns.push(new RegExp(m[1])); } catch (_) {}
    }
    for (const m of text.matchAll(/\bS3method\s*\(\s*([\w.]+)\s*,\s*([\w.]+)\s*\)/g)) {
      s3methods.push({ generic: m[1], class: m[2] });
      // The generic itself is implicitly exported for the registered class.
      exports.add(m[1]);
    }
    for (const m of text.matchAll(/\bimportFrom\s*\(\s*([\w.]+)\s*,\s*([^)]+)\)/g)) {
      const pkg = m[1];
      if (!importFrom.has(pkg)) importFrom.set(pkg, new Set());
      for (const name of splitArgs(m[2])) {
        const clean = stripQuotes(name);
        if (clean) importFrom.get(pkg).add(clean);
      }
    }

    return { exports, exportPatterns, s3methods, importFrom };
  }

  function splitArgs(raw) {
    return raw.split(',').map((s) => s.trim()).filter(Boolean);
  }

  function stripQuotes(s) {
    return s.replace(/^["']|["']$/g, '').trim();
  }

  /**
   * Build a Map<symbolName, absFilePath> from the top-level definitions in a
   * set of R files. Used by the graph builder to resolve `localPkg::fn` to a
   * concrete file. Reads each file once and runs a single regex.
   *
   * @param {string[]} rFiles - absolute paths to .R/.r files
   * @returns {Map<string, string>}
   */
  function collectLocalDefs(rFiles) {
    const defs = new Map();
    // Matches `name <- function(`, `name <- R6Class(`, `name <- new_class(`,
    // `setGeneric("name"`, `setClass("name"`. First-write-wins.
    const reAssign = /^(?:[ \t]*)([\w.]+)\s*(?:<<-|<-|=)\s*(?:(?:R6::)?R6Class|(?:S7::)?new_class|function)\b/gm;
    const reS4Generic = /^[ \t]*setGeneric\s*\(\s*["']([\w.]+)["']/gm;
    const reS4Class   = /^[ \t]*setClass\s*\(\s*["']([\w.]+)["']/gm;
    for (const filePath of rFiles) {
      let content;
      try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
      const stripped = content.replace(/#.*$/gm, '');
      let m;
      while ((m = reAssign.exec(stripped)) !== null) {
        if (m[1].startsWith('.')) continue;
        if (!defs.has(m[1])) defs.set(m[1], filePath);
      }
      while ((m = reS4Generic.exec(stripped)) !== null) {
        if (!defs.has(m[1])) defs.set(m[1], filePath);
      }
      while ((m = reS4Class.exec(stripped)) !== null) {
        if (!defs.has(m[1])) defs.set(m[1], filePath);
      }
    }
    return defs;
  }

  module.exports = { readDescription, readNamespace, collectLocalDefs };
  
};

// ── ./src/discovery/sigmapignore ──
__factories["./src/discovery/sigmapignore"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');

  module.exports = { loadIgnorePatterns, matchesIgnorePattern };

  function loadIgnorePatterns(cwd) {
    for (const fname of ['.sigmapignore', '.contextignore']) {
      const p = path.join(cwd, fname);
      if (fs.existsSync(p)) {
        return fs.readFileSync(p, 'utf8')
          .split('\n')
          .map(l => l.trim())
          .filter(l => l && !l.startsWith('#'));
      }
    }
    return [];
  }

  function matchesIgnorePattern(dirName, patterns) {
    for (const pat of patterns) {
      const clean = pat.replace(/\/$/, '');
      if (clean === dirName) return true;
      if (clean.endsWith('/**') && dirName.startsWith(clean.slice(0, -3))) return true;
      if (clean.endsWith('/*') && dirName.startsWith(clean.slice(0, -2))) return true;
    }
    return false;
  }
  
};

// ── ./src/discovery/source-root-registry ──
__factories["./src/discovery/source-root-registry"] = function(module, exports) {
  
  const REGISTRY = {
    javascript: {
      manifestFiles: ['package.json'],
      frameworks: {
        nextjs:    { detectionFiles: ['next.config.js','next.config.ts','next.config.mjs'], detectionDeps: ['next'],               srcDirs: ['app','src/app','pages','src/pages','src','components','lib','hooks','utils'], entrypoints: ['app/page.tsx','pages/index.tsx'] },
        nestjs:    { detectionFiles: ['nest-cli.json'],                                    detectionDeps: ['@nestjs/core'],        srcDirs: ['src'], entrypoints: ['src/main.ts','src/app.module.ts'] },
        express:   { detectionFiles: [],                                                    detectionDeps: ['express'],             srcDirs: ['src','routes','middleware','controllers','services'], entrypoints: ['src/index.js','server.js','app.js'] },
        fastify:   { detectionFiles: [],                                                    detectionDeps: ['fastify'],             srcDirs: ['src','routes','plugins'], entrypoints: ['src/index.js'] },
        react:     { detectionFiles: [],                                                    detectionDeps: ['react'],               srcDirs: ['src','components','hooks','context','pages','app','lib','utils'] },
        vue:       { detectionFiles: ['vue.config.js','vue.config.ts'],                    detectionDeps: ['vue'],                 srcDirs: ['src','components','composables','pages','views'] },
        nuxt:      { detectionFiles: ['nuxt.config.js','nuxt.config.ts'],                  detectionDeps: ['nuxt'],                srcDirs: ['pages','components','composables','server','middleware','plugins'] },
        svelte:    { detectionFiles: ['svelte.config.js'],                                 detectionDeps: ['svelte','@sveltejs/kit'], srcDirs: ['src','src/routes','src/lib'] },
        angular:   { detectionFiles: ['angular.json'],                                     detectionDeps: ['@angular/core'],       srcDirs: ['src','src/app','projects','apps','libs'] },
        gatsby:    { detectionFiles: ['gatsby-config.js','gatsby-config.ts'],              detectionDeps: ['gatsby'],              srcDirs: ['src','gatsby'] },
        vite:      { detectionFiles: ['vite.config.js','vite.config.ts'],                  detectionDeps: ['vite'],                srcDirs: ['src'] },
        remix:     { detectionFiles: ['remix.config.js'],                                  detectionDeps: ['@remix-run/react'],    srcDirs: ['app'] },
        trpc:      { detectionFiles: [],                                                    detectionDeps: ['@trpc/server'],        srcDirs: ['src','server','routers'] },
      },
      srcDirs:  ['src','lib','index.js','server.js','app.js'],
      penalties: ['dist','build','.next','.nuxt','coverage','storybook-static'],
    },

    typescript: {
      manifestFiles: ['package.json','tsconfig.json'],
      frameworks: {
        nextjs:  { detectionFiles: ['next.config.ts','next.config.mjs'], detectionDeps: ['next'],            srcDirs: ['app','src/app','pages','src','components','lib','hooks','utils'] },
        nestjs:  { detectionFiles: ['nest-cli.json'],                     detectionDeps: ['@nestjs/core'],   srcDirs: ['src'], entrypoints: ['src/main.ts'] },
        angular: { detectionFiles: ['angular.json'],                      detectionDeps: ['@angular/core'],  srcDirs: ['src','src/app','projects','apps','libs'] },
      },
      srcDirs:  ['src','lib','packages'],
      penalties: ['dist','build','.next'],
    },

    python: {
      manifestFiles: ['requirements.txt','pyproject.toml','setup.py','Pipfile'],
      frameworks: {
        django:  { detectionFiles: ['manage.py'],           detectionDeps: ['Django'],  srcDirs: [],         specialRule: 'django-app-dirs', entrypoints: ['manage.py'] },
        fastapi: { detectionFiles: [],                       detectionDeps: ['fastapi'], srcDirs: ['app','src','routers','api'], entrypoints: ['main.py','app/main.py'] },
        flask:   { detectionFiles: ['wsgi.py','app.py'],    detectionDeps: ['Flask'],   srcDirs: ['app','src'], entrypoints: ['app.py','wsgi.py'] },
        celery:  { detectionFiles: [],                       detectionDeps: ['celery'],  srcDirs: ['tasks','workers','app'] },
      },
      srcDirs:  ['.'],
      penalties: ['venv','.venv','__pycache__','.pytest_cache','htmlcov'],
    },

    go: {
      manifestFiles: ['go.mod'],
      frameworks: {
        gin:   { detectionFiles: [], detectionDeps: ['github.com/gin-gonic/gin'],      srcDirs: ['internal','cmd','pkg','api','handler','middleware'] },
        echo:  { detectionFiles: [], detectionDeps: ['github.com/labstack/echo'],      srcDirs: ['internal','cmd','handler','middleware'] },
        fiber: { detectionFiles: [], detectionDeps: ['github.com/gofiber/fiber'],      srcDirs: ['internal','cmd','handler','routes'] },
        grpc:  { detectionFiles: [], detectionDeps: ['google.golang.org/grpc'],        srcDirs: ['internal','proto','server','client'] },
        chi:   { detectionFiles: [], detectionDeps: ['github.com/go-chi/chi'],         srcDirs: ['internal','cmd','handler'] },
      },
      srcDirs:  ['internal','cmd','pkg','api'],
      penalties: ['vendor'],
    },

    rust: {
      manifestFiles: ['Cargo.toml'],
      frameworks: {
        actix: { detectionFiles: [], detectionDeps: ['actix-web'], srcDirs: ['src'] },
        axum:  { detectionFiles: [], detectionDeps: ['axum'],      srcDirs: ['src'] },
        tauri: { detectionFiles: ['src-tauri/tauri.conf.json'], detectionDeps: ['tauri'], srcDirs: ['src','src-tauri/src'] },
      },
      srcDirs:  ['src'],
      penalties: ['target'],
    },

    java: {
      manifestFiles: ['pom.xml','build.gradle'],
      frameworks: {
        spring:   { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/java','src/main/kotlin','src/main/resources'] },
        quarkus:  { detectionFiles: [], detectionDeps: ['io.quarkus'],  srcDirs: ['src/main/java'] },
        android:  { detectionFiles: ['AndroidManifest.xml'],            srcDirs: ['app/src/main/java','app/src/main','src'] },
        micronaut:{ detectionFiles: [], detectionDeps: ['io.micronaut'],srcDirs: ['src/main/java'] },
      },
      srcDirs:  ['src/main/java','src'],
      penalties: ['target','build'],
    },

    kotlin: {
      manifestFiles: ['build.gradle.kts'],
      frameworks: {
        spring:  { detectionFiles: [], detectionDeps: ['spring-boot'],    srcDirs: ['src/main/kotlin'] },
        android: { detectionFiles: ['AndroidManifest.xml'],               srcDirs: ['app/src/main/kotlin','app/src/main/java'] },
        ktor:    { detectionFiles: [], detectionDeps: ['io.ktor'],         srcDirs: ['src'] },
        compose: { detectionFiles: [], detectionDeps: ['compose-runtime'], srcDirs: ['app/src/main/kotlin','src'] },
      },
      srcDirs:  ['src/main/kotlin','src'],
      penalties: ['build','.gradle'],
    },

    csharp: {
      manifestFiles: ['.csproj','.sln'],
      frameworks: {
        aspnet:  { detectionFiles: ['appsettings.json'], detectionDeps: ['Microsoft.AspNetCore'], srcDirs: ['Controllers','Services','Models','Middleware','Pages'] },
        blazor:  { detectionFiles: [], detectionDeps: ['Microsoft.AspNetCore.Components'],        srcDirs: ['Components','Pages','Services'] },
        unity:   { detectionFiles: ['ProjectSettings/ProjectSettings.asset'],                     srcDirs: ['Assets/Scripts','Assets'] },
        maui:    { detectionFiles: [], detectionDeps: ['Microsoft.Maui'],                          srcDirs: ['src','Pages','ViewModels'] },
      },
      srcDirs:  ['src','Controllers','Services','Models'],
      penalties: ['bin','obj','.vs'],
    },

    php: {
      manifestFiles: ['composer.json'],
      frameworks: {
        laravel:   { detectionFiles: ['artisan'],           srcDirs: ['app','routes','config','database','resources','tests'], entrypoints: ['artisan'] },
        symfony:   { detectionFiles: ['symfony.lock'],      srcDirs: ['src','config','templates'], specialRule: 'symfony-bundle-dirs' },
        wordpress: { detectionFiles: ['wp-config.php'],     srcDirs: ['wp-content/themes','wp-content/plugins','wp-content/mu-plugins'] },
        slim:      { detectionFiles: [],                    detectionDeps: ['slim/slim'], srcDirs: ['src','app','routes'] },
      },
      srcDirs:  ['src','app'],
      penalties: ['vendor'],
    },

    ruby: {
      manifestFiles: ['Gemfile'],
      frameworks: {
        rails:   { detectionFiles: ['config/routes.rb'],   srcDirs: ['app','lib','config','db','spec','test'], entrypoints: ['config/routes.rb'] },
        sinatra: { detectionFiles: ['config.ru','app.rb'], srcDirs: ['.','lib'],            entrypoints: ['app.rb','config.ru'] },
        hanami:  { detectionFiles: [],                     detectionDeps: ['hanami'],       srcDirs: ['apps','lib','slices'] },
      },
      srcDirs:  ['app','lib'],
      penalties: ['vendor','coverage','.bundle'],
    },

    swift: {
      manifestFiles: ['Package.swift'],
      frameworks: {
        vapor:   { detectionFiles: [],              detectionDeps: ['vapor/vapor'], srcDirs: ['Sources','App'] },
        swiftui: { detectionFiles: ['.xcodeproj'],  srcDirs: [],                    specialRule: 'swift-project-dir' },
        swiftpm: { detectionFiles: ['Package.swift'],srcDirs: ['Sources'] },
      },
      srcDirs:  ['Sources','Source'],
      penalties: ['.build','DerivedData','Pods','Carthage'],
    },

    dart: {
      manifestFiles: ['pubspec.yaml'],
      frameworks: {
        flutter:    { detectionFiles: [],                   detectionDeps: ['flutter'],    srcDirs: ['lib','lib/src'], entrypoints: ['lib/main.dart'] },
        serverpod:  { detectionFiles: [],                   detectionDeps: ['serverpod'],  srcDirs: ['lib','endpoints','models'] },
        'dart-frog':{ detectionFiles: ['dart_frog.yaml'],   srcDirs: ['routes','lib'] },
      },
      srcDirs:  ['lib','lib/src'],
      penalties: ['.dart_tool','build'],
    },

    scala: {
      manifestFiles: ['build.sbt'],
      frameworks: {
        akka:  { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
        play:  { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
        spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
        zio:   { detectionFiles: [], detectionDeps: ['zio'],  srcDirs: ['src/main/scala'] },
      },
      srcDirs:  ['src/main/scala','src'],
      penalties: ['target'],
    },

    r: {
      manifestFiles: ['DESCRIPTION','renv.lock'],
      frameworks: {
        shiny: { detectionFiles: ['app.R','ui.R','server.R'], srcDirs: ['R','inst','tests'], entrypoints: ['app.R','server.R'] },
      },
      srcDirs:  ['R','src','inst'],
      penalties: ['renv','packrat','.Rcheck'],
    },
  };

  module.exports = { REGISTRY };
  
};

// ── ./src/discovery/source-root-resolver ──
__factories["./src/discovery/source-root-resolver"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');
  const { REGISTRY }              = __require('./src/discovery/source-root-registry');
  const { detectLanguages }       = __require('./src/discovery/language-detector');
  const { detectFrameworks }      = __require('./src/discovery/framework-detector');
  const { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS } = __require('./src/discovery/source-root-scorer');
  const { loadIgnorePatterns, matchesIgnorePattern } = __require('./src/discovery/sigmapignore');

  module.exports = { resolveSourceRoots };

  const MONOREPO_MARKERS = ['pnpm-workspace.yaml','turbo.json','nx.json','lerna.json'];
  const MAX_ROOTS = 6;

  function resolveSourceRoots(cwd, opts = {}) {
    const ignorePatterns = loadIgnorePatterns(cwd);
    const languages      = detectLanguages(cwd);
    const frameworks     = detectFrameworks(cwd);
    const recentDirs     = getRecentlyChangedDirs(cwd);
    const isMonorepo     = _detectMonorepo(cwd);

    const primaryLang   = languages[0]?.name;
    const primaryFw     = frameworks[0];
    const registry      = primaryLang ? REGISTRY[primaryLang] : null;

    // Build framework-derived context
    const fwEntry        = primaryFw && registry?.frameworks?.[primaryFw.name];
    const frameworkSrcDirs   = new Set(fwEntry?.srcDirs || registry?.srcDirs || []);
    const entrypoints        = fwEntry?.entrypoints || [];
    const frameworkPenalties = registry?.penalties || [];

    const context = { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties };

    // Enumerate candidates
    const candidates = _enumerateCandidates(cwd, isMonorepo, ignorePatterns, opts.exclude || []);

    // Score each candidate
    const scored = candidates
      .map(({ name, full }) => ({
        dir:   name,
        full,
        score: scoreCandidate(name, full, context),
      }))
      .filter(c => c.score > 0)
      .sort((a, b) => b.score - a.score);

    // Handle special rules
    let roots = _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks);

    // Dedupe nested paths (prefer parent)
    roots = _dedupeNested(roots);

    // Cap at MAX_ROOTS
    roots = roots.slice(0, MAX_ROOTS).map(r => r.dir);

    // Fallback: if nothing scored, return empty (caller falls back to legacy)
    const confidence = _computeConfidence(frameworks, languages, scored.length);

    return {
      roots,
      languages,
      frameworks,
      confidence,
      explanation: scored.slice(0, 8).map(c => ({
        dir:   c.dir,
        score: c.score,
        reason: `score: ${c.score}`,
      })),
      isMonorepo,
    };
  }

  function _detectMonorepo(cwd) {
    for (const m of MONOREPO_MARKERS) {
      if (fs.existsSync(path.join(cwd, m))) return true;
    }
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      if (pkg.workspaces) return true;
    } catch (_) {}
    return false;
  }

  function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList) {
    const candidates = [];
    const excSet     = new Set(excludeList);

    // Root-level dirs
    try {
      for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
        if (!e.isDirectory()) continue;
        if (excSet.has(e.name)) continue;
        if (matchesIgnorePattern(e.name, ignorePatterns)) continue;
        candidates.push({ name: e.name, full: path.join(cwd, e.name) });
      }
    } catch (_) {}

    // Monorepo sub-packages: packages/*/src, apps/*/src, services/*/src
    if (isMonorepo) {
      for (const top of ['packages','apps','services','modules']) {
        const topFull = path.join(cwd, top);
        if (!fs.existsSync(topFull)) continue;
        try {
          for (const pkg of fs.readdirSync(topFull, { withFileTypes: true })) {
            if (!pkg.isDirectory()) continue;
            const srcFull = path.join(topFull, pkg.name, 'src');
            if (fs.existsSync(srcFull)) {
              candidates.push({ name: `${top}/${pkg.name}/src`, full: srcFull });
            }
            // Also consider the package root itself
            candidates.push({ name: `${top}/${pkg.name}`, full: path.join(topFull, pkg.name) });

            // JVM project structures in monorepo packages (Java, Kotlin, Scala)
            for (const jvmLang of ['java', 'kotlin', 'scala']) {
              const srcMainJvm = path.join(topFull, pkg.name, 'src', 'main', jvmLang);
              if (fs.existsSync(srcMainJvm)) {
                candidates.push({ name: `${top}/${pkg.name}/src/main/${jvmLang}`, full: srcMainJvm });
              }
              const appSrcMainJvm = path.join(topFull, pkg.name, 'app', 'src', 'main', jvmLang);
              if (fs.existsSync(appSrcMainJvm)) {
                candidates.push({ name: `${top}/${pkg.name}/app/src/main/${jvmLang}`, full: appSrcMainJvm });
              }
            }
          }
        } catch (_) {}
      }
    }

    // Deep paths known by language/framework (e.g. src/main/java, src-tauri/src)
    const DEEP_PATHS = [
      'src/main/java','src/main/kotlin','src/main/scala',
      'src-tauri/src','Sources/App','app/src/main/java','app/src/main/kotlin','app/src/main/scala',
      'src/test/java','src/test/kotlin',
    ];
    for (const dp of DEEP_PATHS) {
      const full = path.join(cwd, dp);
      if (fs.existsSync(full)) candidates.push({ name: dp, full });
    }

    return candidates;
  }

  function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
    let roots = [...scored];

    // Django: walk root dirs for any containing models.py or views.py
    if (primaryFw?.name === 'django' || frameworks.some(f => f.name === 'django')) {
      try {
        for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
          if (!e.isDirectory()) continue;
          const d = path.join(cwd, e.name);
          if (fs.existsSync(path.join(d, 'models.py')) || fs.existsSync(path.join(d, 'views.py'))) {
            if (!roots.find(r => r.dir === e.name)) {
              roots.push({ dir: e.name, full: d, score: 5.0 });
            }
          }
        }
      } catch (_) {}
      roots.sort((a, b) => b.score - a.score);
    }

    // Swift project dir: dirs with ≥3 .swift files
    if (frameworks.some(f => f.name === 'swiftui')) {
      try {
        for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
          if (!e.isDirectory()) continue;
          const d = path.join(cwd, e.name);
          const swiftCount = (fs.readdirSync(d).filter(f => f.endsWith('.swift'))).length;
          if (swiftCount >= 3 && !roots.find(r => r.dir === e.name)) {
            roots.push({ dir: e.name, full: d, score: 4.0 });
          }
        }
      } catch (_) {}
      roots.sort((a, b) => b.score - a.score);
    }

    return roots;
  }

  function _dedupeNested(scored) {
    const result = [];
    for (const c of scored) {
      const cNorm = c.dir.replace(/\\/g, '/');
      const isNested = result.some(r => {
        const rNorm = r.dir.replace(/\\/g, '/');
        return cNorm.startsWith(rNorm + '/');
      });
      if (!isNested) result.push(c);
    }
    return result;
  }

  function _computeConfidence(frameworks, languages, scoredCount) {
    if (frameworks.length > 0 && frameworks[0].confidence >= 0.90) return 'high';
    if (languages.length > 0 && scoredCount > 0) return 'medium';
    return 'low';
  }
  
};

// ── ./src/discovery/source-root-scorer ──
__factories["./src/discovery/source-root-scorer"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');
  const { git } = __require('./src/util/git');

  const CODE_EXTS = new Set([
    '.js','.mjs','.cjs','.ts','.tsx','.jsx',
    '.py','.rb','.go','.rs','.java','.kt',
    '.cs','.cpp','.c','.h','.swift','.dart','.scala','.php',
  ]);

  const AUTO_SKIP = new Set([
    'node_modules','dist','build','.git','.next','.nuxt','vendor',
    'DerivedData','Pods','target','coverage','__pycache__','.venv','venv',
    '.build','Carthage','storybook-static','.gradle','bin','obj','.vs',
  ]);

  const PENALTY_DIRS = new Set([
    'test','tests','spec','__tests__','e2e','docs','doc','docs-vp',
    'examples','example','fixtures','mocks','__mocks__','demo','samples','migrations',
    'benchmarks','scripts',
  ]);

  const JVM_PATH_PATTERN = /^(src\/main\/(java|kotlin|scala)|app\/src\/main\/(java|kotlin|scala))$/;

  const ROOT_ENTRYPOINTS = {
    go:         ['main.go'],
    python:     ['app.py','main.py','wsgi.py','asgi.py'],
    javascript: ['index.js','server.js','app.js'],
    typescript: ['index.ts','main.ts'],
    rust:       [],
    php:        ['index.php'],
  };

  function getRecentlyChangedDirs(cwd) {
    try {
      const out = git(['log', '--name-only', '--format=', 'HEAD~10'], { cwd, timeout: 3000 }).toString();
      return new Set(out.split('\n').filter(Boolean).map(f => f.split('/')[0]));
    } catch { return new Set(); }
  }

  function scoreCandidate(dirName, fullPath, context) {
    const { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties } = context;

    // Auto-skip noise
    if (AUTO_SKIP.has(dirName)) return -99;
    if (!fs.existsSync(fullPath)) return -99;

    let score = 0;

    // JVM paths (Java, Kotlin, Scala) get highest priority: +5.0
    if (JVM_PATH_PATTERN.test(dirName)) score += 5.0;

    // Framework match: +3.0 if this dir is in the framework's srcDirs
    if (frameworkSrcDirs.has(dirName)) score += 3.0;

    // Count source files in dir (depth 2)
    const sourceFileCount = _countSourceFiles(fullPath, 2);
    const density = Math.min(1.0, sourceFileCount / 10);

    // Language density: +2.5
    score += density * 2.5;

    // Symbol density: +2.0 if ≥3 source files
    if (sourceFileCount >= 3) score += 2.0;

    // Entrypoint: +1.5 if a known entrypoint lives in this dir
    if ((entrypoints || []).some(ep => ep.startsWith(dirName + '/'))) score += 1.5;

    // Manifest proximity: +1.0 if a manifest file is in this dir
    if (fs.existsSync(path.join(fullPath, 'package.json')) ||
        fs.existsSync(path.join(fullPath, 'go.mod')) ||
        fs.existsSync(path.join(fullPath, 'Cargo.toml')) ||
        fs.existsSync(path.join(fullPath, 'pom.xml'))) {
      score += 1.0;
    }

    // Git activity bonus: +2.0 if recently committed files exist here
    if (recentDirs.has(dirName)) score += 2.0;

    // Noise penalty: -3.0 (unless directory is in framework's srcDirs)
    if (PENALTY_DIRS.has(dirName.toLowerCase()) && !frameworkSrcDirs.has(dirName)) score -= 3.0;

    // Framework penalty dirs
    if ((frameworkPenalties || []).includes(dirName)) score -= 3.0;

    return Math.round(score * 100) / 100;
  }

  function _countSourceFiles(dir, depth) {
    if (depth <= 0) return 0;
    let count = 0;
    try {
      for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
        if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) count++;
        else if (e.isDirectory() && depth > 1) count += _countSourceFiles(path.join(dir, e.name), depth - 1);
      }
    } catch (_) {}
    return count;
  }

  module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS, JVM_PATH_PATTERN };
  
};

// ── ./src/doctor/diagnose ──
__factories["./src/doctor/diagnose"] = function(module, exports) {
  
  /**
   * sigmap doctor (v8.0 E3).
   *
   * One-shot diagnostic for a SigMap setup: each check reports ok/warn/fail with
   * an actionable fix, so a cold user can reach a useful answer in minutes. Pure,
   * resilient (no check ever throws), zero new runtime deps. Composes the config
   * loader, coverage scorer, signature index, and the known adapter-output /
   * MCP-config paths.
   */

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

  // Generated context files a `read_context`/`ask` flow can consume.
  const ADAPTER_OUTPUTS = [
    ['.github', 'copilot-instructions.md'],
    ['CLAUDE.md'],
    ['AGENTS.md'],
    ['.cursorrules'],
    ['.windsurfrules'],
    ['.github', 'openai-context.md'],
    ['.github', 'gemini-context.md'],
    ['llm-full.txt'],
    ['llm.txt'],
  ];

  const EXCLUDE_DIRS = new Set([
    'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
    '.next', 'coverage', 'target', 'vendor', '.context',
  ]);

  const ICON = { ok: '✓', warn: '⚠', fail: '✗' };

  function _short(p, cwd) {
    const rel = path.relative(cwd, p);
    return rel && !rel.startsWith('..') ? rel : p.replace(os.homedir(), '~');
  }

  function _contextFiles(cwd) {
    const out = [];
    for (const parts of ADAPTER_OUTPUTS) {
      const p = path.join(cwd, ...parts);
      try { if (fs.existsSync(p)) out.push(p); } catch (_) {}
    }
    return out;
  }

  function _mcpTargets(cwd) {
    return [
      path.join(cwd, '.mcp.json'),
      path.join(cwd, '.claude', 'settings.json'),
      path.join(cwd, '.cursor', 'mcp.json'),
      path.join(cwd, '.windsurf', 'mcp.json'),
      path.join(cwd, '.vscode', 'mcp.json'),
      path.join(cwd, 'opencode.json'),
      path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
      path.join(os.homedir(), '.config', 'opencode', 'config.json'),
      path.join(os.homedir(), '.gemini', 'settings.json'),
      path.join(os.homedir(), '.codex', 'config.yaml'),
      path.join(os.homedir(), '.config', 'zed', 'settings.json'),
    ];
  }

  /** Count code files under srcDirs modified after the context was generated. */
  function _countChangedSince(cwd, srcDirs, config, ctxMtime) {
    const { CODE_EXTS } = __require('./src/analysis/coverage-score');
    const exclude = new Set(EXCLUDE_DIRS);
    if (config && Array.isArray(config.exclude)) for (const x of config.exclude) exclude.add(String(x));

    let changed = 0;
    let seen = 0;
    const walk = (dir, depth) => {
      if (depth > 8 || seen > 5000) return;
      let entries;
      try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
      for (const e of entries) {
        if (exclude.has(e.name)) continue;
        const full = path.join(dir, e.name);
        if (e.isDirectory()) walk(full, depth + 1);
        else if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) {
          seen++;
          try { if (fs.statSync(full).mtimeMs > ctxMtime) changed++; } catch (_) {}
        }
      }
    };
    for (const d of srcDirs) {
      const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
      if (fs.existsSync(abs)) walk(abs, 0);
    }
    return changed;
  }

  /**
   * Run all diagnostic checks.
   * @param {string} cwd
   * @returns {{ checks: Array<{id,label,status,detail,fix}>, ok: boolean, errors: number, warnings: number }}
   */
  function diagnose(cwd, opts = {}) {
    const checks = [];
    const add = (id, label, status, detail, fix) => checks.push({ id, label, status, detail: detail || '', fix: fix || null });

    // 1. Git repository
    try {
      const { tryGit } = __require('./src/util/git');
      const inside = tryGit(['rev-parse', '--is-inside-work-tree'], { cwd });
      if (inside === 'true') add('git', 'Git repository', 'ok', 'recency boost + impact analysis enabled');
      else add('git', 'Git repository', 'warn', 'not a git repository', 'git init — enables recency boost and impact analysis');
    } catch (_) {
      add('git', 'Git repository', 'warn', 'git not available', 'install git for recency boost + impact analysis');
    }

    // 2. Config & source roots
    let config = {};
    try {
      const cfgPath = path.join(cwd, 'gen-context.config.json');
      let configBroken = false;
      if (fs.existsSync(cfgPath)) {
        try { JSON.parse(fs.readFileSync(cfgPath, 'utf8')); }
        catch (e) {
          configBroken = true;
          add('config', 'Config & source roots', 'fail', `gen-context.config.json is invalid JSON: ${e.message}`, 'fix the JSON syntax, or delete the file to fall back to defaults');
        }
      }
      const { loadConfig } = __require('./src/config/loader');
      config = loadConfig(cwd) || {};
      if (!configBroken) {
        // Distinguish explicitly-configured srcDirs (validate each) from the
        // default candidate list (just report which ones actually exist).
        let explicitSrcDirs = null;
        if (fs.existsSync(cfgPath)) {
          try { const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); if (Array.isArray(raw.srcDirs)) explicitSrcDirs = raw.srcDirs; } catch (_) {}
        }
        const exists = (d) => { try { return fs.existsSync(path.isAbsolute(d) ? d : path.join(cwd, d)); } catch (_) { return false; } };
        const srcDirs = Array.isArray(config.srcDirs) ? config.srcDirs : [];
        const present = srcDirs.filter(exists);

        if (explicitSrcDirs) {
          const missing = explicitSrcDirs.filter((d) => !exists(d));
          if (missing.length) add('config', 'Config & source roots', 'warn', `configured srcDirs not found: ${missing.join(', ')}`, 'fix "srcDirs" in gen-context.config.json');
          else add('config', 'Config & source roots', 'ok', `srcDirs: ${explicitSrcDirs.join(', ')}`);
        } else if (present.length === 0) {
          add('config', 'Config & source roots', 'warn', 'no source directories found (looked for src/, app/, lib/, …)', 'create a src/ dir, set "srcDirs" in gen-context.config.json, or run: sigmap roots --fix');
        } else {
          add('config', 'Config & source roots', 'ok', `source roots: ${present.slice(0, 8).join(', ')}${present.length > 8 ? `, +${present.length - 8} more` : ''}`);
        }
      }
    } catch (e) {
      if (!checks.some((c) => c.id === 'config')) add('config', 'Config & source roots', 'warn', `could not load config: ${e.message}`);
    }

    // 3. Generated context file
    const ctxFiles = _contextFiles(cwd);
    if (ctxFiles.length === 0) {
      add('context', 'Generated context', 'fail', 'no context file found', 'run: npx sigmap   (generates the signature map)');
    } else {
      add('context', 'Generated context', 'ok', `${ctxFiles.length} file(s): ${ctxFiles.map((f) => _short(f, cwd)).join(', ')}`);
    }

    // 4. Signature index
    let indexSize = 0;
    try {
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      indexSize = buildSigIndex(cwd).size;
    } catch (_) {}
    if (indexSize === 0) {
      add('index', 'Signature index', ctxFiles.length === 0 ? 'fail' : 'warn', 'no signatures indexed', 'run: npx sigmap   then: sigmap ask "<query>"');
    } else {
      add('index', 'Signature index', 'ok', `${indexSize} file(s) indexed`);
    }

    // 5. Index freshness
    try {
      if (ctxFiles.length) {
        const ctxMtime = Math.max(...ctxFiles.map((f) => { try { return fs.statSync(f).mtimeMs; } catch (_) { return 0; } }));
        const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length) ? config.srcDirs : ['src', 'app', 'lib'];
        const changed = _countChangedSince(cwd, srcDirs, config, ctxMtime);
        if (changed > 0) add('freshness', 'Index freshness', 'warn', `${changed} source file(s) changed since last generate`, 'run: sigmap   (or: sigmap --watch to auto-refresh)');
        else add('freshness', 'Index freshness', 'ok', 'index is up to date with sources');
      }
    } catch (_) {}

    // 6. Coverage
    try {
      if (indexSize > 0) {
        const { coverageScore } = __require('./src/analysis/coverage-score');
        const { buildSigIndex } = __require('./src/retrieval/ranker');
        const entries = [...buildSigIndex(cwd).keys()].map((rel) => ({ filePath: path.resolve(cwd, rel) }));
        const cov = coverageScore(cwd, entries, config);
        if (cov.score < 70) add('coverage', 'Coverage', 'warn', `${cov.score}% of source files in context (grade ${cov.grade})`, 'increase maxTokens or expand srcDirs in gen-context.config.json');
        else add('coverage', 'Coverage', 'ok', `${cov.score}% of source files in context (grade ${cov.grade})`);
      }
    } catch (_) {}

    // 7. MCP wiring
    try {
      let wired = null;
      for (const t of _mcpTargets(cwd)) {
        try {
          if (!fs.existsSync(t)) continue;
          if (/sigmap/.test(fs.readFileSync(t, 'utf8'))) { wired = t; break; }
        } catch (_) {}
      }
      if (wired) add('mcp', 'MCP wiring', 'ok', `registered in ${_short(wired, cwd)}`);
      else add('mcp', 'MCP wiring', 'warn', 'MCP server not registered in any editor config', 'run: sigmap --setup   (auto-wires Claude, Cursor, Windsurf, VS Code, …)');
    } catch (_) {}

    const errors = checks.filter((c) => c.status === 'fail').length;
    const warnings = checks.filter((c) => c.status === 'warn').length;
    return { checks, ok: errors === 0, errors, warnings };
  }

  /** Human-readable checklist. */
  function formatDoctor(result) {
    const lines = ['sigmap doctor', ''];
    for (const c of result.checks) {
      lines.push(`${ICON[c.status] || '?'} ${c.label}${c.detail ? ' — ' + c.detail : ''}`);
      if (c.status !== 'ok' && c.fix) lines.push(`    ↳ ${c.fix}`);
    }
    lines.push('');
    lines.push(
      result.errors === 0 && result.warnings === 0
        ? '✓ All checks passed.'
        : `${result.errors} error(s), ${result.warnings} warning(s).`
    );
    return lines.join('\n');
  }

  /** Machine-readable result. */
  function formatDoctorJSON(result) {
    return JSON.stringify(result, null, 2);
  }

  module.exports = { diagnose, formatDoctor, formatDoctorJSON };
  
};

// ── ./src/eval/analyzer ──
__factories["./src/eval/analyzer"] = function(module, exports) {
  
  /**
   * SigMap file analyzer — per-file diagnostic statistics.
   * Zero npm dependencies.
   *
   * Exports:
   *   analyzeFiles(files, cwd, opts) → stats[]
   *   formatAnalysisTable(stats)    → markdown table string
   *   formatAnalysisJSON(stats)     → plain object suitable for JSON.stringify
   */

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

  // Extension → extractor name (mirrors EXT_MAP in gen-context.js)
  const EXT_MAP = {
    '.ts': 'typescript', '.tsx': 'typescript',
    '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
    '.py': 'python',     '.pyw': 'python',
    '.java': 'java',
    '.kt': 'kotlin',     '.kts': 'kotlin',
    '.go': 'go',
    '.rs': 'rust',
    '.cs': 'csharp',
    '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
    '.rb': 'ruby',       '.rake': 'ruby',
    '.php': 'php',
    '.swift': 'swift',
    '.dart': 'dart',
    '.scala': 'scala',   '.sc': 'scala',
    '.gd': 'gdscript',
    '.r': 'r',           '.R': 'r',
    '.vue': 'vue',
    '.svelte': 'svelte',
    '.html': 'html',     '.htm': 'html',
    '.css': 'css',       '.scss': 'css', '.sass': 'css', '.less': 'css',
    '.yml': 'yaml',      '.yaml': 'yaml',
    '.sh': 'shell',      '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
    '.toml': 'toml',
    '.properties': 'properties',
    '.xml': 'xml',
    '.md': 'markdown',
    // Phase C specialized extractors
    '.tsx': 'typescript_react',
    '.vue': 'vue_sfc',
  };

  function isDockerfile(name) {
    return name === 'Dockerfile' || name.startsWith('Dockerfile.');
  }

  function getExtractorName(filePath) {
    const base = path.basename(filePath);
    const ext  = path.extname(base).toLowerCase();
    if (EXT_MAP[ext]) return EXT_MAP[ext];
    if (isDockerfile(base)) return 'dockerfile';
    return null;
  }

  /** Rough token estimate: chars / 4 */
  function tokenCount(sigs) {
    return Math.ceil(sigs.reduce((sum, s) => sum + s.length, 0) / 4);
  }

  /**
   * Check whether a test file exists for this source file by looking for
   * *.test.* / *.spec.* patterns in the test/ directory tree.
   */
  function hasCoverage(filePath, cwd) {
    const rel   = path.relative(cwd, filePath);
    const base  = path.basename(rel, path.extname(rel));  // e.g. "python"
    const testDirs = ['test', 'tests', '__tests__', 'spec'];
    for (const d of testDirs) {
      const abs = path.join(cwd, d);
      if (!fs.existsSync(abs)) continue;
      // Walk only one depth for speed
      let entries;
      try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch (_) { continue; }
      for (const e of entries) {
        if (e.name.includes(base)) return true;
      }
    }
    return false;
  }

  /**
   * Load an extractor module from src/extractors/ relative to cwd.
   * Falls back to requiring from the module directory itself.
   */
  function loadExtractor(name, cwd) {
    // Try repo-local src/extractors first (for projects that embed sigmap)
    const local = path.join(cwd, 'src', 'extractors', `${name}.js`);
    if (fs.existsSync(local)) {
      try { return require(local); } catch (_) {}
    }
    // Then standard node resolution from the current package
    try { return require(path.join(__dirname, '..', 'extractors', `${name}.js`)); } catch (_) {}
    return null;
  }

  /**
   * Analyze a list of absolute file paths.
   *
   * @param {string[]} files   - absolute paths to analyze
   * @param {string}   cwd     - project root
   * @param {object}  [opts]
   * @param {boolean} [opts.slow=false]  - if true, measure extraction time per file
   * @param {number}  [opts.slowMs=50]   - threshold (ms) before a file is "slow"
   * @param {number}  [opts.maxSigs=25]  - max sigs per file
   * @returns {object[]} array of per-file stat objects
   */
  function analyzeFiles(files, cwd, opts) {
    const slow    = (opts && opts.slow)   || false;
    const slowMs  = (opts && opts.slowMs) || 50;
    const maxSigs = (opts && opts.maxSigs) || 25;

    const stats = [];
    const extractorCache = {};

    for (const filePath of files) {
      const extractorName = getExtractorName(filePath);
      if (!extractorName) continue;

      // Load extractor (cached)
      if (!extractorCache[extractorName]) {
        extractorCache[extractorName] = loadExtractor(extractorName, cwd);
      }
      const extractor = extractorCache[extractorName];
      if (!extractor || typeof extractor.extract !== 'function') continue;

      let content;
      try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }

      let sigs;
      let elapsedMs = 0;

      if (slow) {
        const t0 = Date.now();
        try { sigs = extractor.extract(content); } catch (_) { sigs = []; }
        elapsedMs = Date.now() - t0;
      } else {
        try { sigs = extractor.extract(content); } catch (_) { sigs = []; }
      }

      sigs = (Array.isArray(sigs) ? sigs : []).slice(0, maxSigs);

      const rel      = path.relative(cwd, filePath);
      const tokens   = tokenCount(sigs);
      const covered  = hasCoverage(filePath, cwd);
      const isSlow   = slow && elapsedMs > slowMs;
      // v4.0: signal quality = sigs per line-of-code (higher = more informative to LLMs)
      const linesOfCode    = content.split('\n').length;
      const signalQuality  = linesOfCode > 0 ? parseFloat((sigs.length / linesOfCode).toFixed(4)) : 0;

      stats.push({
        file:          rel,
        extractor:     extractorName,
        sigs:          sigs.length,
        tokens,
        covered,
        linesOfCode,
        signalQuality,
        elapsedMs:     slow ? elapsedMs : undefined,
        slow:          slow ? isSlow : undefined,
      });
    }

    return stats;
  }

  /**
   * Format stats as a markdown table.
   *
   * @param {object[]} stats - output of analyzeFiles()
   * @param {boolean}  showSlow - whether to include the Elapsed column
   * @returns {string}
   */
  function formatAnalysisTable(stats, showSlow) {
    if (!stats || stats.length === 0) return '_(no files analyzed)_\n';

    // Column widths
    const maxFile = Math.max(4, ...stats.map((s) => s.file.length));

    const header = showSlow
      ? `| ${'File'.padEnd(maxFile)} | Sigs | Tokens | Extractor   | Coverage   | Elapsed  |`
      : `| ${'File'.padEnd(maxFile)} | Sigs | Tokens | Extractor   | Coverage   |`;

    const sep = showSlow
      ? `|${'-'.repeat(maxFile + 2)}|------|--------|-------------|------------|----------|`
      : `|${'-'.repeat(maxFile + 2)}|------|--------|-------------|------------|`;

    const rows = stats.map((s) => {
      const cov  = s.covered ? '✓ tested  ' : '✗ untested';
      const file = s.file.padEnd(maxFile);
      const ext  = (s.extractor || '').padEnd(11);
      const base = `| ${file} | ${String(s.sigs).padStart(4)} | ${String(s.tokens).padStart(6)} | ${ext} | ${cov} |`;
      if (showSlow) {
        const ms = s.elapsedMs !== undefined ? `${s.elapsedMs}ms` : '';
        const flag = s.slow ? ' ⚠️' : '';
        return `${base} ${ms.padStart(6)}${flag} |`;
      }
      return base;
    });

    const totalSigs   = stats.reduce((n, s) => n + s.sigs,   0);
    const totalTokens = stats.reduce((n, s) => n + s.tokens, 0);
    const slotFile    = ''.padEnd(maxFile);
    const baseFoot    = `| ${slotFile} | ${String(totalSigs).padStart(4)} | ${String(totalTokens).padStart(6)} | **Total**   |            |`;
    const footer = showSlow ? `${baseFoot} ${' '.padStart(8)} |` : baseFoot;

    return [header, sep, ...rows, sep, footer].join('\n') + '\n';
  }

  /**
   * Format stats as a plain-object suitable for JSON.stringify.
   *
   * @param {object[]} stats
   * @returns {object}
   */
  function formatAnalysisJSON(stats) {
    const totalSigs   = stats.reduce((n, s) => n + s.sigs,   0);
    const totalTokens = stats.reduce((n, s) => n + s.tokens, 0);
    const slowFiles   = stats.filter((s) => s.slow);

    return {
      files:       stats,
      totalSigs,
      totalTokens,
      slowFiles:   slowFiles.map((s) => ({ file: s.file, elapsedMs: s.elapsedMs })),
      fileCount:   stats.length,
    };
  }

  module.exports = { analyzeFiles, formatAnalysisTable, formatAnalysisJSON };
  
};

// ── ./src/eval/llm-ablation ──
__factories["./src/eval/llm-ablation"] = function(module, exports) {
  
  /**
   * LLM A/B hallucination ablation (IMPL.md §9) — the honest measurement.
   *
   * Runs a model twice per task — (A) no SigMap context, (B) with SigMap
   * grounding — pipes both outputs through the hallucination guard, and reports
   * the measured delta in flagged codebase-fact errors. The model call is
   * INJECTED (`complete(prompt) → text`), so the harness itself is pure and
   * offline-testable; the live model adapter lives in `scripts/run-llm-ablation.mjs`.
   * Zero-dependency, bundle-safe (no network here).
   */

  const { verify } = __require('./src/verify/hallucination-guard');

  const path = require('path');

  /** Strip a signature's trailing line anchor (` :12-20`) for prompt cleanliness. */
  function _cleanSig(sig) {
    return String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
  }

  /**
   * Build the SigMap grounding block for a repo — what we prepend to a task
   * prompt in arm B. Conventions (the house style) + **exact signatures** grouped
   * by file (what `get_callee_signatures` returns), so the model references the
   * real surface instead of guessing — the actual product behavior, not a flat
   * name dump.
   * @param {string} cwd
   * @param {object} [opts]
   * @param {number} [opts.maxSignatures=150] cap on signature lines (bounds prompt size)
   * @returns {string}
   */
  function buildGrounding(cwd, opts = {}) {
    const maxSignatures = opts.maxSignatures != null ? opts.maxSignatures : 150;
    const parts = [];

    let index = null;
    try {
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      index = buildSigIndex(cwd);
    } catch (_) {}

    try {
      const { extractConventions } = __require('./src/conventions/extract');
      const { renderConventionsBlock } = __require('./src/conventions/inject');
      const files = index ? [...index.keys()] : [];
      parts.push(renderConventionsBlock(extractConventions(cwd, files)));
    } catch (_) {}

    if (index) {
      const lines = ['## Exact signatures (use these — do not invent symbols or paths)'];
      let count = 0;
      for (const [file, sigs] of index) {
        if (count >= maxSignatures) break;
        const rel = path.relative(cwd, file).replace(/\\/g, '/');
        const clean = (sigs || []).map(_cleanSig).filter(Boolean);
        if (!clean.length) continue;
        lines.push(`### ${rel}`);
        for (const s of clean) {
          if (count >= maxSignatures) break;
          lines.push(s);
          count++;
        }
      }
      if (count > 0) parts.push(lines.join('\n'));
    }

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

  /**
   * Score an answer: flagged codebase-fact errors + the issue list (the §9 metric).
   * @param {string} answerText
   * @param {string} cwd
   * @returns {{ total: number, issues: object[] }}
   */
  function scoreAnswerDetail(answerText, cwd) {
    try {
      const { issues, summary } = verify(String(answerText || ''), cwd);
      return { total: summary.total || 0, issues: issues || [] };
    } catch (_) {
      return { total: 0, issues: [] };
    }
  }

  /** Count flagged codebase-fact errors in an answer (the §9 metric). */
  function scoreAnswer(answerText, cwd) {
    return scoreAnswerDetail(answerText, cwd).total;
  }

  /**
   * Run the A/B ablation over a task corpus.
   * @param {Array<{id:string, prompt:string}>} tasks
   * @param {string} cwd
   * @param {(prompt:string, meta:object)=>string} complete injected model call
   * @param {object} [opts]
   * @param {string} [opts.grounding] precomputed grounding (else built from cwd)
   * @param {boolean} [opts.collectIssues] attach `aIssues`/`bIssues` per task
   * @returns {{ tasks: object[], aggregate: object }}
   */
  function runAblation(tasks, cwd, complete, opts = {}) {
    const grounding = opts.grounding != null ? opts.grounding : buildGrounding(cwd);
    const rows = [];
    let sumA = 0;
    let sumB = 0;

    for (const task of tasks || []) {
      const basePrompt = task.prompt || '';
      const groundedPrompt = grounding ? `${grounding}\n\n---\n\n${basePrompt}` : basePrompt;

      const outA = String(complete(basePrompt, { id: task.id, grounded: false }) || '');
      const outB = String(complete(groundedPrompt, { id: task.id, grounded: true }) || '');

      const a = scoreAnswerDetail(outA, cwd);
      const b = scoreAnswerDetail(outB, cwd);
      sumA += a.total;
      sumB += b.total;
      const row = { id: task.id, aFlagged: a.total, bFlagged: b.total };
      if (opts.collectIssues) { row.aIssues = a.issues; row.bIssues = b.issues; }
      rows.push(row);
    }

    const n = rows.length;
    const per100 = (sum) => (n > 0 ? (sum / n) * 100 : 0);
    return {
      tasks: rows,
      aggregate: {
        n,
        withoutFlagged: sumA,
        withFlagged: sumB,
        delta: sumA - sumB,
        withoutPer100: per100(sumA),
        withPer100: per100(sumB),
      },
    };
  }

  /** mean/min/max of a number list (0s for an empty list). */
  function _stats(nums) {
    if (!nums.length) return { mean: 0, min: 0, max: 0 };
    const sum = nums.reduce((a, b) => a + b, 0);
    return { mean: sum / nums.length, min: Math.min(...nums), max: Math.max(...nums) };
  }

  /**
   * Aggregate several `runAblation` passes into a stable estimate — mean ± range
   * of the without/with per-100 flag rates and their delta. At N=40 with tiny raw
   * counts a single pass is noisy; averaging repeated passes gives a publishable
   * number with an honest spread.
   * @param {object[]} aggregates the `.aggregate` object from each runAblation pass
   * @returns {{ runs:number, n:number, withoutPer100:object, withPer100:object, deltaPer100:object }}
   */
  function aggregateRuns(aggregates) {
    const runs = (aggregates || []).filter(Boolean);
    const without = runs.map((a) => a.withoutPer100);
    const withG = runs.map((a) => a.withPer100);
    const delta = runs.map((a) => a.withoutPer100 - a.withPer100);
    return {
      runs: runs.length,
      n: runs.length ? runs[0].n : 0,
      withoutPer100: _stats(without),
      withPer100: _stats(withG),
      deltaPer100: _stats(delta),
    };
  }

  module.exports = { buildGrounding, scoreAnswer, scoreAnswerDetail, runAblation, aggregateRuns };
  
};

// ── ./src/eval/runner ──
__factories["./src/eval/runner"] = function(module, exports) {
  
  /**
   * SigMap benchmark runner.
   * Zero npm dependencies.
   *
   * Loads evaluation tasks from a JSONL file, runs signature-based retrieval
   * against a target repo, and returns scored results.
   *
   * Usage (programmatic):
   *   const { run } = __require('./src/eval/src/eval/runner');
   *   const results = run('benchmarks/tasks/retrieval.jsonl', cwd);
   *   // results: { tasks: [...], metrics: { hitAt5, mrr, precisionAt5, avgTokens, tasks } }
   *
   * Usage (CLI via gen-context.js --benchmark):
   *   node gen-context.js --benchmark
   *   node gen-context.js --benchmark --json
   */

  const fs = require('fs');
  const path = require('path');
  const { aggregate } = __require('./src/eval/scorer');
  const { bm25rank } = __require('./src/retrieval/bm25');

  // ---------------------------------------------------------------------------
  // Context file reader
  // ---------------------------------------------------------------------------

  /**
   * Read the generated context file and build a simple signature index:
   *   Map<filePath, string[]>  — file → list of signature strings
   *
   * The context file uses section headers like:
   *   ### src/extractors/python.js
   * followed by ``` blocks containing signatures.
   *
   * @param {string} cwd
   * @returns {Map<string, string[]>}
   */
  function buildSigIndex(cwd) {
    const contextPath = path.join(cwd, '.github', 'copilot-instructions.md');
    const index = new Map();

    if (!fs.existsSync(contextPath)) return index;

    const content = fs.readFileSync(contextPath, 'utf8');
    const lines = content.split('\n');

    let currentFile = null;
    let inBlock = false;
    let sigs = [];

    for (const line of lines) {
      // Section header: ### path/to/file.js
      const headerMatch = line.match(/^###\s+(\S+\.\w+)\s*$/);
      if (headerMatch) {
        if (currentFile !== null) {
          index.set(currentFile, sigs);
        }
        currentFile = headerMatch[1];
        sigs = [];
        inBlock = false;
        continue;
      }

      if (line.startsWith('```')) {
        inBlock = !inBlock;
        continue;
      }

      if (inBlock && currentFile && line.trim()) {
        sigs.push(line.trim());
      }
    }

    // Flush last file
    if (currentFile !== null) {
      index.set(currentFile, sigs);
    }

    return index;
  }

  // ---------------------------------------------------------------------------
  // Identifier-aware BM25 ranking (v7.31; see src/retrieval/bm25.js and #395)
  // ---------------------------------------------------------------------------

  const { tokenize } = __require('./src/retrieval/bm25');

  /**
   * Rank all files in the index against a query with the identifier-aware BM25
   * re-ranker. Returns file entries sorted by relevance score descending; ties
   * are broken by file path alphabetically (deterministic).
   * @param {string} query
   * @param {Map<string, string[]>} index
   * @param {number} topK
   * @returns {{ file: string, score: number, sigs: string[] }[]}
   */
  function rank(query, index, topK = 10) {
    const candidates = [];
    for (const [file, sigs] of index.entries()) {
      candidates.push({ file, sigs });
    }
    return bm25rank(query, candidates).slice(0, topK);
  }

  // ---------------------------------------------------------------------------
  // Token estimation
  // ---------------------------------------------------------------------------

  /**
   * Estimate token count from character count (chars/4, ±5%).
   * @param {string[]} sigs
   * @returns {number}
   */
  function estimateTokens(sigs) {
    const text = (sigs || []).join('\n');
    return Math.ceil(text.length / 4);
  }

  // ---------------------------------------------------------------------------
  // Task loader
  // ---------------------------------------------------------------------------

  /**
   * Load tasks from a JSONL file.
   * Each line: { id, query, expected_files, repo }
   * Invalid or blank lines are silently skipped.
   * @param {string} tasksFile - absolute or relative path
   * @returns {Array<{id:string, query:string, expected:string[], repo:string}>}
   */
  function loadTasks(tasksFile) {
    if (!fs.existsSync(tasksFile)) return [];
    const lines = fs.readFileSync(tasksFile, 'utf8').split('\n');
    const tasks = [];
    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      try {
        const obj = JSON.parse(trimmed);
        if (obj.query && Array.isArray(obj.expected_files)) {
          tasks.push({
            id: obj.id || String(tasks.length + 1),
            query: obj.query,
            expected: obj.expected_files,
            repo: obj.repo || '.',
          });
        }
      } catch {
        // skip invalid JSON lines
      }
    }
    return tasks;
  }

  // ---------------------------------------------------------------------------
  // Main runner
  // ---------------------------------------------------------------------------

  /**
   * Run all tasks in tasksFile against the repo at cwd.
   *
   * @param {string} tasksFile - path to JSONL task file (absolute or relative to cwd)
   * @param {string} cwd       - project root
   * @param {object} [opts]
   * @param {number} [opts.topK=10] - how many results to rank per query
   * @returns {{
   *   tasks: Array<{id, query, expected, ranked, hit5, rr, precAt5, tokens}>,
   *   metrics: { hitAt5, mrr, precisionAt5, avgTokens, tasks }
   * }}
   */
  function run(tasksFile, cwd, opts = {}) {
    const topK = opts.topK || 10;

    // Resolve paths
    const resolvedTasksFile = path.isAbsolute(tasksFile)
      ? tasksFile
      : path.resolve(cwd, tasksFile);

    const tasks = loadTasks(resolvedTasksFile);
    if (tasks.length === 0) {
      return {
        tasks: [],
        metrics: { hitAt5: 0, mrr: 0, precisionAt5: 0, avgTokens: 0, tasks: 0 },
      };
    }

    // Build index once (re-used across all tasks in the same repo)
    const index = buildSigIndex(cwd);

    const taskResults = [];
    for (const task of tasks) {
      const ranked = rank(task.query, index, topK).map((r) => r.file);
      const topResult = rank(task.query, index, topK);
      const tokens = topResult.reduce((sum, r) => sum + estimateTokens(r.sigs), 0);

      const { hitAtK, reciprocalRank, precisionAtK } = __require('./src/eval/scorer');
      const hit5 = hitAtK(ranked, task.expected, 5);
      const rr = reciprocalRank(ranked, task.expected);
      const precAt5 = precisionAtK(ranked, task.expected, 5);

      taskResults.push({
        id: task.id,
        query: task.query,
        expected: task.expected,
        ranked,
        hit5,
        rr,
        precAt5,
        tokens,
      });
    }

    const metrics = aggregate(
      taskResults.map((r) => ({ ranked: r.ranked, expected: r.expected, tokens: r.tokens })),
    );

    return { tasks: taskResults, metrics };
  }

  // ---------------------------------------------------------------------------
  // Table formatter
  // ---------------------------------------------------------------------------

  /**
   * Format task results as a markdown table string.
   * @param {Array} taskResults - from run()
   * @returns {string}
   */
  function formatTable(taskResults) {
    const header = '| Task | Query | hit@5 | RR | Tokens |';
    const divider = '|---|---|:---:|:---:|---:|';
    const rows = taskResults.map((r) => {
      const q = r.query.length > 40 ? r.query.slice(0, 37) + '...' : r.query;
      return `| ${r.id} | ${q} | ${r.hit5 ? '✓' : '✗'} | ${r.rr.toFixed(2)} | ${r.tokens} |`;
    });
    return [header, divider, ...rows].join('\n');
  }

  /**
   * Format aggregate metrics as a human-readable string.
   * @param {object} metrics - from aggregate()
   * @returns {string}
   */
  function formatMetrics(metrics) {
    return [
      `[sigmap] benchmark results:`,
      `  tasks       : ${metrics.tasks}`,
      `  hit@5       : ${(metrics.hitAt5 * 100).toFixed(1)}%`,
      `  MRR         : ${metrics.mrr.toFixed(3)}`,
      `  precision@5 : ${(metrics.precisionAt5 * 100).toFixed(1)}%`,
      `  avg tokens  : ${metrics.avgTokens}`,
    ].join('\n');
  }

  module.exports = { run, rank, loadTasks, buildSigIndex, formatTable, formatMetrics, tokenize };
  
};

// ── ./src/eval/scorer ──
__factories["./src/eval/scorer"] = function(module, exports) {
  
  /**
   * SigMap evaluation scorer.
   * Zero npm dependencies.
   *
   * Computes retrieval quality metrics:
   *   hit@k     — fraction of tasks where the correct file appears in top-k results
   *   MRR       — mean reciprocal rank (1/rank of first correct result)
   *   precision@k — fraction of top-k results that are correct
   *
   * All functions are pure and never throw.
   */

  /**
   * Return the 1-based rank of the first expected file found in the ranked
   * result list, or Infinity if none found.
   * @param {string[]} ranked   - Ordered list of file paths returned by the system
   * @param {string[]} expected - List of acceptable correct files (any match counts)
   * @returns {number}
   */
  function firstRank(ranked, expected) {
    if (!Array.isArray(ranked) || !Array.isArray(expected)) return Infinity;
    const expSet = new Set(expected.map((f) => normalizePath(f)));
    for (let i = 0; i < ranked.length; i++) {
      if (expSet.has(normalizePath(ranked[i]))) return i + 1;
    }
    return Infinity;
  }

  /**
   * Normalize a file path for comparison (trim leading ./, lowercase on
   * case-insensitive platforms is intentionally NOT done — keep paths as-is
   * but trim leading directory separators and ./).
   * @param {string} p
   * @returns {string}
   */
  function normalizePath(p) {
    return String(p).replace(/^\.\//, '').replace(/\\/g, '/');
  }

  /**
   * Compute hit@k for one task.
   * @param {string[]} ranked   - Ordered results
   * @param {string[]} expected - Correct files
   * @param {number}   k        - Cut-off (default 5)
   * @returns {0|1}
   */
  function hitAtK(ranked, expected, k = 5) {
    return firstRank(ranked, expected) <= k ? 1 : 0;
  }

  /**
   * Compute reciprocal rank for one task.
   * @param {string[]} ranked
   * @param {string[]} expected
   * @returns {number} value in (0, 1]
   */
  function reciprocalRank(ranked, expected) {
    const rank = firstRank(ranked, expected);
    return rank === Infinity ? 0 : 1 / rank;
  }

  /**
   * Compute precision@k for one task.
   * Fraction of the top-k results that appear in expected.
   * @param {string[]} ranked
   * @param {string[]} expected
   * @param {number}   k
   * @returns {number} value in [0, 1]
   */
  function precisionAtK(ranked, expected, k = 5) {
    if (!ranked || ranked.length === 0) return 0;
    const expSet = new Set(expected.map((f) => normalizePath(f)));
    const topK = ranked.slice(0, k);
    const hits = topK.filter((f) => expSet.has(normalizePath(f))).length;
    return hits / topK.length;
  }

  /**
   * Aggregate metrics across all task results.
   *
   * @param {Array<{ranked: string[], expected: string[], tokens: number}>} results
   * @param {number} k - cut-off (default 5)
   * @returns {{
   *   hitAt5: number,    // fraction [0,1]
   *   mrr: number,       // mean reciprocal rank [0,1]
   *   precisionAt5: number,
   *   avgTokens: number,
   *   tasks: number
   * }}
   */
  function aggregate(results, k = 5) {
    if (!Array.isArray(results) || results.length === 0) {
      return { hitAt5: 0, mrr: 0, precisionAt5: 0, avgTokens: 0, tasks: 0 };
    }

    let totalHit = 0;
    let totalRR = 0;
    let totalPrec = 0;
    let totalTokens = 0;

    for (const r of results) {
      const ranked = r.ranked || [];
      const expected = r.expected || [];
      totalHit += hitAtK(ranked, expected, k);
      totalRR += reciprocalRank(ranked, expected);
      totalPrec += precisionAtK(ranked, expected, k);
      totalTokens += (typeof r.tokens === 'number' ? r.tokens : 0);
    }

    const n = results.length;
    return {
      hitAt5: round(totalHit / n),
      mrr: round(totalRR / n),
      precisionAt5: round(totalPrec / n),
      avgTokens: Math.round(totalTokens / n),
      tasks: n,
    };
  }

  function round(x) {
    return Math.round(x * 1000) / 1000;
  }

  module.exports = { hitAtK, reciprocalRank, precisionAtK, aggregate, firstRank };
  
};

// ── ./src/eval/usefulness-scorer ──
__factories["./src/eval/usefulness-scorer"] = function(module, exports) {
  
  module.exports = { scoreUsefulness, computeUsefulnessStats };

  /**
   * Score answer usefulness based on:
   * 1. Whether right file was retrieved (retrieval hit)
   * 2. Whether retrieved context covered the answer (coverage)
   * 3. Confidence in answer quality (from ranking score)
   */
  function scoreUsefulness(taskResult, rankingScore) {
    const { hitRank } = taskResult;

    // Tier 1: File not retrieved — context cannot be useful
    if (hitRank === -1 || hitRank > 5) {
      return {
        tier: 'not-useful',
        score: 0.0,
        reason: 'expected file not in top 5'
      };
    }

    // Tier 2: File retrieved but not top ranking — partially useful
    if (hitRank > 1) {
      return {
        tier: 'partially-useful',
        score: rankingScore * 0.5,  // Partial usefulness
        reason: `file ranked #${hitRank}`
      };
    }

    // Tier 3: File at top of ranking — fully useful
    return {
      tier: 'fully-useful',
      score: rankingScore,  // Full usefulness
      reason: 'file ranked first'
    };
  }

  function computeUsefulnessStats(taskResults) {
    const tiers = {
      'fully-useful': 0,
      'partially-useful': 0,
      'not-useful': 0
    };

    let totalScore = 0;
    let count = 0;

    taskResults.forEach(result => {
      const usefulness = scoreUsefulness(result, result.rankingScore || 1.0);
      tiers[usefulness.tier]++;
      totalScore += usefulness.score;
      count++;
    });

    return {
      fully_useful: tiers['fully-useful'],
      partially_useful: tiers['partially-useful'],
      not_useful: tiers['not-useful'],
      fully_useful_pct: count > 0 ? (tiers['fully-useful'] / count * 100).toFixed(1) : 0,
      partially_useful_pct: count > 0 ? (tiers['partially-useful'] / count * 100).toFixed(1) : 0,
      not_useful_pct: count > 0 ? (tiers['not-useful'] / count * 100).toFixed(1) : 0,
      average_usefulness_score: count > 0 ? (totalScore / count).toFixed(3) : 0
    };
  }
  
};

// ── ./src/evidence/pack ──
__factories["./src/evidence/pack"] = function(module, exports) {
  
  /**
   * Evidence Pack v1 (v8.0 E1).
   *
   * A deterministic, machine-consumable signature-and-evidence map. Replaces the
   * "paste this into your prompt" workflow with a byte-stable JSON artifact that
   * an agent or CI can ingest directly — every entry anchored to a real file,
   * symbol, and line range.
   *
   * Composed entirely from shipped zero-dep modules:
   *   - retrieval/ranker        → ranked files, scores, signals
   *   - extractors/line-anchor  → `:start-end` suffix parsing (sourceLines)
   *   - security/scanner        → secret redaction of symbols
   *   - crypto (node builtin)    → sha256 grounding hash
   *
   * Determinism: the pack carries NO wall-clock timestamp. Given an unchanged
   * repository, `buildEvidencePack` returns a byte-identical object, and
   * `grounding.contextHash` is stable. This is the point — the pack is auditable.
   */

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

  const { buildSigIndex, rank, detectIntent } = __require('./src/retrieval/ranker');
  const { scan } = __require('./src/security/scanner');

  const SCHEMA_VERSION = '1.0';
  const DEFAULT_BUDGET = 6000;
  const DEFAULT_TOP = 12;

  const GENERATED_RE = /(^|\/)(dist|build|out|vendor|node_modules)\/|\.(generated|min|bundle)\.|\.(pb|_pb)\.|\.pb\.go$|_pb2\.py$/;
  const TEST_RE = /(^|\/)(tests?|__tests__|spec|specs)\/|\.(test|spec)\.[a-z]+$|(^|\/)test_[^/]+\.py$|_test\.(go|py|rb)$/;
  const CONFIG_RE = /\.(json|ya?ml|toml|ini|conf|config|properties|env)$|(^|\/)(\.?[a-z]+rc)$|\.config\.[a-z]+$/i;
  // DB migrations: framework dirs (Rails/Alembic/Prisma), Flyway `V1__x.sql`,
  // timestamped migration files, and `*_migration.*` naming.
  const MIGRATION_RE = /(^|\/)(migrations?|alembic\/versions|prisma\/migrations)(\/|$)|(^|\/)db\/migrate\/|(^|\/)V\d+(_\d+)*__[^/]+\.(sql|java)$|(^|\/)\d{8,}[_-][^/]+\.(sql|rb|py|js|ts)$|[._-]migration[s]?[._-]/i;
  const PAYMENT_RE = /(^|\/|[._-])(payment|payments|billing|checkout|invoice|invoicing|subscription|stripe|paypal|braintree|charge|refund|payout)([._-]|\/|$)/i;
  const AUTH_RE = /(^|\/|[._-])(auth|authn|authz|login|logout|signin|signup|password|passwd|session|oauth|jwt|permission|permissions|acl|rbac|credential|credentials)([._-]|\/|$)/i;
  const SECURITY_RE = /(^|\/|[._-])(secret|secrets|crypto|cipher|encrypt|decrypt|token|signing|keystore|vault)([._-]|\/|$)/i;
  // Public API surface: `api/` dirs, `public-api`, and module barrel entrypoints.
  const PUBLIC_API_RE = /(^|\/)api(\/|$)|(^|\/)public[-_]?api(\/|$)|(^|\/)index\.(js|ts|mjs|cjs)$/i;

  /**
   * Split a signature's `  :start-end` line anchor from its symbol text.
   * @param {string} sig
   * @returns {{ symbol: string, start: number|null, end: number|null }}
   */
  function parseAnchor(sig) {
    const m = /\s*:(\d+)-(\d+)\s*$/.exec(sig);
    if (!m) return { symbol: sig.trim(), start: null, end: null };
    return {
      symbol: sig.slice(0, m.index).trim(),
      start: parseInt(m[1], 10),
      end: parseInt(m[2], 10),
    };
  }

  /**
   * Classify a file into a risk label (C3, v8.5). Path-based, deterministic.
   * Precedence is strict, most-specific-risk first: a migration touching payments
   * is labeled `migration` (a schema change is the dominant risk), payment/auth
   * outrank the generic `security` bucket, and `config`/`public-api` resolve
   * before the `source` fallback. `test`/`generated` semantics are preserved so
   * existing consumers (findRelatedTests, verifier) keep working.
   * @param {string} relPath
   * @returns {'generated'|'test'|'migration'|'payment'|'auth'|'security'|'config'|'public-api'|'source'}
   */
  function riskLabelFor(relPath) {
    const p = relPath.replace(/\\/g, '/');
    if (GENERATED_RE.test(p)) return 'generated';
    if (TEST_RE.test(p)) return 'test';
    if (MIGRATION_RE.test(p)) return 'migration';
    if (PAYMENT_RE.test(p)) return 'payment';
    if (AUTH_RE.test(p)) return 'auth';
    if (SECURITY_RE.test(p)) return 'security';
    if (CONFIG_RE.test(p)) return 'config';
    if (PUBLIC_API_RE.test(p)) return 'public-api';
    return 'source';
  }

  /** Filename stem (basename minus the first extension chain). */
  function stemOf(relPath) {
    const base = path.basename(relPath);
    return base.replace(/\.[^.]+$/, '').replace(/\.(test|spec)$/i, '');
  }

  /**
   * Infer the implementation stem a test file targets, by stripping the
   * conventional test affixes across languages (measured in the C2 benchmark):
   *   foo.test.js / foo.spec.ts    → foo   (JS/TS)
   *   test_foo.py                  → foo   (Python / pytest)
   *   foo_test.go / foo_test.py    → foo   (Go, unittest)
   *   FooTest.java / BarSpec.scala → Foo   (JVM, PascalCase)
   * @param {string} relPath
   * @returns {string}
   */
  function testTargetStem(relPath) {
    let s = stemOf(relPath);               // strips ext + trailing .test/.spec
    s = s.replace(/^test[_-]/i, '');       // Python: test_foo
    s = s.replace(/[_-]test$/i, '');       // Go / unittest: foo_test
    s = s.replace(/(Tests?|Specs?)$/, ''); // JVM PascalCase: FooTest, BarSpec
    return s;
  }

  /**
   * Impl→test discovery (C2, v8.5). Matches test files back to their
   * implementation by normalizing conventional test affixes, so JS/TS, Python,
   * Go, and JVM naming conventions all resolve. Deterministic; accuracy is
   * measured by `scripts/run-test-discovery-benchmark.mjs`.
   * @param {string} relPath
   * @param {string[]} allFiles  - universe of indexed files (relative paths)
   * @returns {string[]}
   */
  function findRelatedTests(relPath, allFiles) {
    if (riskLabelFor(relPath) === 'test') return [];
    const stem = stemOf(relPath).toLowerCase();
    if (!stem) return [];
    const out = [];
    for (const f of allFiles) {
      if (f === relPath) continue;
      if (riskLabelFor(f) !== 'test') continue;
      if (testTargetStem(f).toLowerCase() === stem) out.push(f);
    }
    return out.sort();
  }

  /** Map a ranker `signals` object into a short human-readable reason string. */
  function reasonFor(signals) {
    if (!signals) return 'ranked match';
    const parts = [];
    if (signals.symbolMatch > 0) parts.push('symbol-name match');
    if (signals.exactToken > 0) parts.push('exact token match');
    if (signals.prefixMatch > 0) parts.push('prefix match');
    if (signals.pathMatch > 0) parts.push('path match');
    if (signals.graphBoost > 0) parts.push('dependency-graph neighbor');
    if (signals.recencyBoost > 1) parts.push('recently changed');
    if (signals.learnedWeights && signals.learnedWeights !== 1) parts.push('learned weight');
    return parts.length ? parts.join('; ') : 'ranked match';
  }

  /** Token estimate for a signature block (matches the ranker's heuristic). */
  function sigTokens(sigs) {
    return Math.ceil(sigs.join('\n').length / 4);
  }

  /**
   * Stable stringify with recursively sorted object keys, for hashing.
   * @param {*} value
   * @returns {string}
   */
  function canonicalize(value) {
    return JSON.stringify(sortKeys(value));
  }

  function sortKeys(value) {
    if (Array.isArray(value)) return value.map(sortKeys);
    if (value && typeof value === 'object') {
      const out = {};
      for (const k of Object.keys(value).sort()) out[k] = sortKeys(value[k]);
      return out;
    }
    return value;
  }

  /**
   * Build an Evidence Pack for a query.
   *
   * @param {string} query
   * @param {string} cwd
   * @param {object} [opts]
   * @param {number} [opts.budget=6000]      - token budget for included files
   * @param {number} [opts.top=12]           - max ranked files to consider
   * @param {Map<string,string[]>} [opts.sigIndex] - pre-built index (else built from cwd)
   * @returns {object} Evidence Pack v1
   */
  function buildEvidencePack(query, cwd, opts = {}) {
    const budget = Number.isFinite(opts.budget) ? opts.budget : DEFAULT_BUDGET;
    const top = Number.isFinite(opts.top) ? opts.top : DEFAULT_TOP;

    const sigIndex = opts.sigIndex instanceof Map ? opts.sigIndex : buildSigIndex(cwd);
    const intent = detectIntent(query);
    const allFiles = Array.from(sigIndex.keys());

    const ranked = rank(query, sigIndex, { topK: top, cwd })
      .filter((r) => r.score > 0 || ranked0Empty(query));
    const maxScore = ranked.reduce((m, r) => Math.max(m, r.score), 0);

    // Greedy budget fill in rank order; the remainder is reported as dropped.
    const files = [];
    const droppedFiles = [];
    let used = 0;

    for (const r of ranked) {
      const tokens = sigTokens(r.sigs);
      if (files.length > 0 && used + tokens > budget) {
        droppedFiles.push({ path: r.file, reason: `budget: would exceed ${budget}-token limit` });
        continue;
      }
      used += tokens;

      const safe = scan(r.sigs, r.file).safe;
      const symbols = [];
      const sourceLines = [];
      for (const sig of safe) {
        const { symbol, start, end } = parseAnchor(sig);
        symbols.push(symbol);
        if (start !== null) sourceLines.push({ symbol, start, end });
      }

      files.push({
        path: r.file,
        symbols,
        reason: reasonFor(r.signals),
        confidence: maxScore > 0 ? Math.round((r.score / maxScore) * 100) / 100 : 0,
        sourceLines,
        relatedTests: findRelatedTests(r.file, allFiles),
        riskLabel: riskLabelFor(r.file),
      });
    }

    const symbolCount = files.reduce((n, f) => n + f.symbols.length, 0);
    const anchoredSymbols = files.reduce((n, f) => n + f.sourceLines.length, 0);

    const pack = {
      schemaVersion: SCHEMA_VERSION,
      query,
      intent,
      files,
      tokenBudget: { limit: budget, used, remaining: Math.max(0, budget - used) },
      droppedFiles,
      grounding: {
        symbolCount,
        anchoredSymbols,
        anchorCoverage: symbolCount > 0 ? Math.round((anchoredSymbols / symbolCount) * 1000) / 1000 : 0,
        contextHash: null,
        deterministic: true,
      },
    };

    // Hash everything except the hash field itself.
    const forHash = Object.assign({}, pack, {
      grounding: Object.assign({}, pack.grounding, { contextHash: undefined }),
    });
    pack.grounding.contextHash = 'sha256:' + crypto.createHash('sha256').update(canonicalize(forHash)).digest('hex');

    return pack;
  }

  // rank() returns [] for an empty/whitespace query; keep the filter readable.
  function ranked0Empty(query) {
    return !query || !query.trim();
  }

  /** Pretty-printed canonical JSON rendering of a pack. */
  function formatJSON(pack) {
    return JSON.stringify(pack, null, 2);
  }

  /** Markdown handoff rendering of a pack. */
  function formatMarkdown(pack) {
    const L = [];
    L.push(`# Evidence Pack — \`${pack.query}\``);
    L.push('');
    L.push(`- **Schema:** v${pack.schemaVersion}`);
    L.push(`- **Intent:** ${pack.intent}`);
    L.push(`- **Budget:** ${pack.tokenBudget.used} / ${pack.tokenBudget.limit} tokens used (${pack.tokenBudget.remaining} remaining)`);
    L.push(`- **Grounding:** ${pack.grounding.anchoredSymbols}/${pack.grounding.symbolCount} symbols anchored (${Math.round(pack.grounding.anchorCoverage * 100)}%)`);
    L.push(`- **Hash:** \`${pack.grounding.contextHash}\``);
    L.push('');

    for (const f of pack.files) {
      L.push(`## \`${f.path}\`  _(${f.riskLabel}, confidence ${f.confidence})_`);
      L.push(`_${f.reason}_`);
      if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.map((t) => `\`${t}\``).join(', ')}`);
      L.push('');
      L.push('```');
      for (const s of f.symbols) L.push(s);
      L.push('```');
      L.push('');
    }

    if (pack.droppedFiles.length) {
      L.push('## Dropped (over budget)');
      for (const d of pack.droppedFiles) L.push(`- \`${d.path}\` — ${d.reason}`);
      L.push('');
    }

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

  module.exports = {
    buildEvidencePack,
    formatJSON,
    formatMarkdown,
    parseAnchor,
    riskLabelFor,
    findRelatedTests,
    SCHEMA_VERSION,
  };
  
};

// ── ./src/extractors/coverage ──
__factories["./src/extractors/coverage"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');

  function walkFiles(dir) {
    let out = [];
    let entries;
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch (_) {
      return out;
    }
    for (const entry of entries) {
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) out = out.concat(walkFiles(full));
      else if (entry.isFile()) out.push(full);
    }
    return out;
  }

  function buildTestIndex(cwd, testDirs) {
    const dirs = Array.isArray(testDirs) && testDirs.length ? testDirs : ['tests', 'test', '__tests__', 'spec'];
    const names = new Set();

    for (const dir of dirs) {
      const abs = path.join(cwd, dir);
      if (!fs.existsSync(abs)) continue;
      for (const file of walkFiles(abs)) {
        let src = '';
        try {
          src = fs.readFileSync(file, 'utf8');
        } catch (_) {
          continue;
        }

        // Extract tokens from JS/TS test name strings (it/test/describe calls)
        for (const m of src.matchAll(/\b(?:it|test|describe)\s*\(\s*['"\`]([\w_ ]+)['"\`]/g)) {
          if (!m[1]) continue;
          const full = m[1].replace(/[\s_]+/g, '_').toLowerCase();
          names.add(full); // also keep whole phrase: "validate_user"
          for (const word of m[1].split(/[\s_]+/)) {
            if (word.length >= 3) names.add(word.toLowerCase());
          }
        }
        // Python/Ruby style: def test_funcname() / def test_funcname: → index the suffix
        for (const m of src.matchAll(/\bdef\s+test_([\w]+)\s*[(:]/g)) {
          if (!m[1]) continue;
          names.add(m[1].toLowerCase()); // full suffix: "validate_user"
          for (const word of m[1].split('_')) {
            if (word.length >= 3) names.add(word.toLowerCase());
          }
        }
        // Also capture identifiers directly invoked in expect/assert calls
        for (const m of src.matchAll(/\b(?:expect|assert)\s*\(\s*(?:await\s+)?([\w]+)\s*\(/g)) {
          if (m[1] && m[1].length >= 3) names.add(m[1].toLowerCase());
        }
      }
    }

    return names;
  }

  function isTested(funcName, testIndex) {
    if (!funcName || funcName.length < 3 || !testIndex || testIndex.size === 0) return false;
    const lower = funcName.toLowerCase();
    // Direct match or test_ prefix match
    if (testIndex.has(lower) || testIndex.has(`test_${lower}`)) return true;
    // Token-level match: split camelCase and snake_case, check if all meaningful tokens are covered
    const tokens = funcName
      .replace(/([A-Z])/g, '_$1')
      .toLowerCase()
      .split(/[_\s]+/)
      .filter(t => t.length >= 3);
    if (tokens.length >= 2 && tokens.every(t => testIndex.has(t))) return true;
    return false;
  }

  module.exports = { buildTestIndex, isTested };
  
};

// ── ./src/extractors/cpp ──
__factories["./src/extractors/cpp"] = function(module, exports) {
  
  /**
   * Extract signatures from C/C++ source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes and structs
    const classRe = /^(?:class|struct)\s+(\w+)(?:\s*:\s*(?:public|protected|private)\s+[\w:]+)?\s*\{/gm;
    for (const m of stripped.matchAll(classRe)) {
      const kind = m[0].trimStart().startsWith('class') ? 'class' : 'struct';
      sigs.push(`${kind} ${m[1]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const meth of extractMembers(block)) sigs.push(`  ${meth}`);
    }

    // Top-level function declarations/definitions (not inside a class)
    for (const m of stripped.matchAll(/^(?!class|struct|if|for|while|switch)([\w:*&<> ]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?\{/gm)) {
      if (m[2].startsWith('_')) continue;
      const ret = normalizeType(m[1]);
      const retStr = ret ? ` → ${ret}` : '';
      sigs.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    const methodRe = /^\s+(?:virtual\s+|static\s+|inline\s+)?(?!private:|protected:|public:)([\w:*&<> ]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:=\s*0\s*)?;/gm;
    for (const m of block.matchAll(methodRe)) {
      if (m[2].startsWith('_')) continue;
      const ret = normalizeType(m[1]);
      const retStr = ret ? ` → ${ret}` : '';
      members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/\s+/g, ' ').slice(0, 30);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/csharp ──
__factories["./src/extractors/csharp"] = function(module, exports) {
  
  /**
   * Extract signatures from C# source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes and interfaces
    const typeRe = /^\s*(?:public\s+|internal\s+|protected\s+)?(?:abstract\s+|sealed\s+|static\s+)?(class|interface|enum|record|struct)\s+(\w+)(?:<[^{]*>)?(?:\s*:\s*[\w<>, .]+)?\s*\{/gm;
    for (const m of stripped.matchAll(typeRe)) {
      sigs.push(`${m[1]} ${m[2]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const meth of extractMembers(block)) sigs.push(`  ${meth}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 5000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    const methodRe = /^\s+(?:public|internal|protected)\s+(?:static\s+|virtual\s+|override\s+|async\s+)*(?:where\s+\w+\s*:\s*[^\n]+\s+)?([\w<>\[\]?., ]+)\s+(\w+)\s*\(([^)]*)\)/gm;
    for (const m of block.matchAll(methodRe)) {
      const ret = normalizeType(m[1]);
      const retStr = ret ? ` → ${ret}` : '';
      members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/\s+/g, ' ').slice(0, 30);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/css ──
__factories["./src/extractors/css"] = function(module, exports) {
  
  /**
   * Extract signatures from CSS/SCSS/SASS/Less source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // CSS custom properties (variables)
    const rootMatch = stripped.match(/:root\s*\{([^}]*)\}/);
    if (rootMatch) {
      for (const m of rootMatch[1].matchAll(/(--[\w-]+)\s*:/g)) {
        sigs.push(`var ${m[1]}`);
      }
    }

    // SCSS/Less variables
    for (const m of stripped.matchAll(/^(\$[\w-]+)\s*:/gm)) {
      sigs.push(`$var ${m[1]}`);
    }

    // SCSS mixins
    for (const m of stripped.matchAll(/^@mixin\s+([\w-]+)(?:\s*\(([^)]*)\))?/gm)) {
      const params = m[2] ? `(${m[2].trim()})` : '';
      sigs.push(`@mixin ${m[1]}${params}`);
    }

    // SCSS functions
    for (const m of stripped.matchAll(/^@function\s+([\w-]+)\s*\(([^)]*)\)/gm)) {
      sigs.push(`@function ${m[1]}(${m[2].trim()})`);
    }

    // Key class names (top-level) — prefer hyphenated BEM/component names over utilities
    const allClassMatches = [...stripped.matchAll(/^\.([\w-]+)(?=[^{]*\{)/gm)];
    // Utility-class detection: classes are "utility-like" if they have no hyphen (e.g. .flex)
    // OR match Tailwind patterns: -digit suffix (.p-4, .bg-blue-500) or common size abbreviations
    // (.text-sm, .py-lg). Files where ≥70% of selectors are utility-like are skipped to avoid noise.
    function looksLikeUtility(name) {
      if (!name.includes('-')) return true;
      if (/-\d/.test(name)) return true;
      if (/-(?:sm|md|lg|xl|xs|2xl|3xl|px|py|full|auto|none|screen)$/.test(name)) return true;
      return false;
    }
    const utilityCount = allClassMatches.filter(m => looksLikeUtility(m[1])).length;
    const isUtilityFile = allClassMatches.length >= 5 && (utilityCount / allClassMatches.length) >= 0.70;
    if (!isUtilityFile) {
      const hyphenated = [];
      const singleWord = [];
      for (const m of allClassMatches) {
        if (m[1].includes('__') || m[1].includes('--')) hyphenated.push(m[1]); // BEM names first
        else if (m[1].includes('-')) hyphenated.push(m[1]); // other hyphenated component names
        else singleWord.push(m[1]);
      }
      // Up to 8 slots: hyphenated classes (semantic) first, then single-word to fill remaining
      const selected = [...hyphenated, ...singleWord].slice(0, 8);
      for (const name of selected) sigs.push(`.${name}`);
    }

    return sigs.slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/dart ──
__factories["./src/extractors/dart"] = function(module, exports) {
  
  /**
   * Extract signatures from Dart source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes and abstract classes
    for (const m of stripped.matchAll(/^(?:abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, ]+)?(?:\s+(?:implements|with|on)\s+[\w<>, ]+)?\s*\{/gm)) {
      const abs = m[0].trimStart().startsWith('abstract') ? 'abstract ' : '';
      sigs.push(`${abs}class ${m[1]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const meth of extractMembers(block)) sigs.push(`  ${meth}`);
    }

    // Top-level functions — capture return type (prefix before name) and show as suffix
    for (const m of stripped.matchAll(/^((?:Future<[\w<>?,\s]*>|[\w<>?]+))\s+(\w+)\s*\(([^)]*)\)/gm)) {
      if (m[2].startsWith('_')) continue;
      const retStr = (m[1] && m[1] !== 'void') ? ` \u2192 ${m[1].replace(/\s+/g, '').slice(0, 25)}` : '';
      sigs.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    for (const m of block.matchAll(/^\s+(?:@override\s+)?(?:@\w+\s+)*((?:Future<[\w<>?,\s]*>|[\w<>?]+))\s+(\w+)\s*\(([^)]*)\)/gm)) {
      if (m[2].startsWith('_')) continue;
      const retStr = (m[1] && m[1] !== 'void') ? ` \u2192 ${m[1].replace(/\s+/g, '').slice(0, 25)}` : '';
      members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\{[^}]*\}/g, '').replace(/\s+/g, ' ').trim();
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/deps ──
__factories["./src/extractors/deps"] = function(module, exports) {
  
  /**
   * Extract import dependencies from Python and TypeScript/JavaScript files.
   * Returns compact dependency arrays for the dep-map section of the context output.
   */

  const PYTHON_STDLIB = new Set([
    'os', 'sys', 're', 'json', 'time', 'threading', 'logging', 'typing',
    'dataclasses', 'datetime', 'uuid', 'pathlib', 'collections', 'functools',
    'itertools', 'math', 'random', 'string', 'struct', 'io', 'copy', 'pprint',
    'traceback', 'inspect', 'abc', 'enum', 'contextlib', 'weakref', 'gc',
    'socket', 'ssl', 'http', 'urllib', 'email', 'html', 'xml', 'csv', 'sqlite3',
    'argparse', 'subprocess', 'shutil', 'tempfile', 'glob', 'fnmatch', 'stat',
    'hashlib', 'hmac', 'base64', 'binascii', 'codecs', 'unicodedata', 'locale',
    'decimal', 'fractions', 'numbers', 'cmath', 'heapq', 'bisect', 'array',
    'queue', 'asyncio', 'concurrent', 'multiprocessing', 'signal', 'mmap',
    'builtins', 'warnings', 'operator', 'textwrap', 'difflib', 'readline',
  ]);

  /**
   * Extract project-level import dependencies from Python source.
   * @param {string} src
   * @returns {string[]}
   */
  function extractPythonDeps(src) {
    const deps = new Set();
    for (const m of src.matchAll(/^from\s+([\w.]+)\s+import/gm)) {
      const mod = m[1];
      const root = mod.replace(/^\.+/, '').split('.')[0];
      // Include relative imports and non-stdlib modules
      if (mod.startsWith('.') || (root && !PYTHON_STDLIB.has(root))) {
        deps.add(root || mod);
      }
    }
    for (const m of src.matchAll(/^import\s+([\w.]+)/gm)) {
      const root = m[1].split('.')[0];
      if (root && !PYTHON_STDLIB.has(root)) deps.add(root);
    }
    return [...deps].filter(Boolean).slice(0, 5);
  }

  /**
   * Extract relative import dependencies from TypeScript/JavaScript source.
   * @param {string} src
   * @returns {string[]}
   */
  function extractTSDeps(src) {
    // Strip single-line comments to avoid matching commented-out imports
    const stripped = src.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
    const deps = new Set();
    for (const m of stripped.matchAll(/from\s+['"](\.[\/\w.-]+)['"]/g)) {
      // Normalise: '../store/authStore' → store/authStore, './utils' → utils
      const clean = m[1]
        .replace(/^\.\.\//, '')
        .replace(/^\.\//,  '')
        .replace(/\.\w+$/, '');
      if (clean) deps.add(clean);
    }
    return [...deps].slice(0, 5);
  }

  // R base packages — present in every install, not informative as deps.
  const R_BASE_PKGS = new Set([
    'base', 'stats', 'utils', 'graphics', 'grDevices', 'methods', 'datasets',
    'parallel', 'splines', 'stats4', 'tools', 'tcltk', 'grid', 'compiler',
  ]);

  /**
   * Extract project-level import dependencies from R source.
   * Captures `library(pkg)`, `require(pkg)`, `requireNamespace("pkg")`, and
   * `pkg::fn` references, skipping base packages.
   * @param {string} src
   * @returns {string[]}
   */
  function extractRDeps(src) {
    const deps = new Set();
    // Strip line comments so commented-out library() calls don't match.
    const stripped = (src || '').replace(/#.*$/gm, '');
    for (const m of stripped.matchAll(/\b(?:library|require)\s*\(\s*["']?([\w.]+)["']?\s*\)/g)) {
      if (m[1] && !R_BASE_PKGS.has(m[1])) deps.add(m[1]);
    }
    for (const m of stripped.matchAll(/\brequireNamespace\s*\(\s*["']([\w.]+)["']/g)) {
      if (m[1] && !R_BASE_PKGS.has(m[1])) deps.add(m[1]);
    }
    for (const m of stripped.matchAll(/\b([A-Za-z][\w.]*)::[A-Za-z]/g)) {
      if (m[1] && !R_BASE_PKGS.has(m[1])) deps.add(m[1]);
    }
    return [...deps].slice(0, 5);
  }

  /**
   * Build reverse dependency map from forward map.
   * @param {Map<string, string[]>} forwardMap
   * @returns {Map<string, string[]>}
   */
  function buildReverseDepMap(forwardMap) {
    const reverse = new Map();
    if (!forwardMap || typeof forwardMap.entries !== 'function') return reverse;
    for (const [file, deps] of forwardMap.entries()) {
      if (!Array.isArray(deps)) continue;
      for (const dep of deps) {
        if (!reverse.has(dep)) reverse.set(dep, []);
        reverse.get(dep).push(file);
      }
    }
    return reverse;
  }

  module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, buildReverseDepMap };
  
};

// ── ./src/extractors/dispatch ──
__factories["./src/extractors/dispatch"] = function(module, exports) {
  
  /**
   * Bundle-safe extractor dispatch.
   *
   * `packages/core`'s extract() resolves extractors via dynamic `require(path.join(…))`,
   * which cannot run inside the standalone bundle (no filesystem `src/`). This module
   * uses STATIC requires so the bundler rewrites them to `__require` and the extractors
   * resolve from the bundled factories. Used by the live-index MCP write hooks.
   */

  const path = require('path');

  // Static language → extractor map (every entry is a bundled factory).
  const EXTRACTORS = {
    typescript: __require('./src/extractors/typescript'),
    typescript_react: __require('./src/extractors/typescript_react'),
    javascript: __require('./src/extractors/javascript'),
    python: __require('./src/extractors/python'),
    java: __require('./src/extractors/java'),
    kotlin: __require('./src/extractors/kotlin'),
    go: __require('./src/extractors/go'),
    rust: __require('./src/extractors/rust'),
    csharp: __require('./src/extractors/csharp'),
    cpp: __require('./src/extractors/cpp'),
    ruby: __require('./src/extractors/ruby'),
    php: __require('./src/extractors/php'),
    swift: __require('./src/extractors/swift'),
    dart: __require('./src/extractors/dart'),
    scala: __require('./src/extractors/scala'),
    gdscript: __require('./src/extractors/gdscript'),
    r: __require('./src/extractors/r'),
    vue: __require('./src/extractors/vue'),
    vue_sfc: __require('./src/extractors/vue_sfc'),
    svelte: __require('./src/extractors/svelte'),
    html: __require('./src/extractors/html'),
    css: __require('./src/extractors/css'),
    yaml: __require('./src/extractors/yaml'),
    shell: __require('./src/extractors/shell'),
    sql: __require('./src/extractors/sql'),
    graphql: __require('./src/extractors/graphql'),
    terraform: __require('./src/extractors/terraform'),
    protobuf: __require('./src/extractors/protobuf'),
    toml: __require('./src/extractors/toml'),
    properties: __require('./src/extractors/properties'),
    xml: __require('./src/extractors/xml'),
    markdown: __require('./src/extractors/markdown'),
    dockerfile: __require('./src/extractors/dockerfile'),
    generic: __require('./src/extractors/generic'),
  };

  const EXT_MAP = {
    '.ts': 'typescript', '.tsx': 'typescript_react',
    '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
    '.py': 'python', '.pyw': 'python',
    '.java': 'java',
    '.kt': 'kotlin', '.kts': 'kotlin',
    '.go': 'go',
    '.rs': 'rust',
    '.cs': 'csharp',
    '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
    '.rb': 'ruby', '.rake': 'ruby',
    '.php': 'php',
    '.swift': 'swift',
    '.dart': 'dart',
    '.scala': 'scala', '.sc': 'scala',
    '.gd': 'gdscript',
    '.r': 'r', '.R': 'r',
    '.vue': 'vue_sfc',
    '.svelte': 'svelte',
    '.html': 'html', '.htm': 'html',
    '.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
    '.yml': 'yaml', '.yaml': 'yaml',
    '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
    '.sql': 'sql',
    '.graphql': 'graphql', '.gql': 'graphql',
    '.tf': 'terraform', '.tfvars': 'terraform',
    '.proto': 'protobuf',
    '.toml': 'toml',
    '.properties': 'properties',
    '.xml': 'xml',
    '.md': 'markdown',
  };

  /** Resolve a language key from a file path/name. */
  function langFor(filePathOrName) {
    const base = path.basename(String(filePathOrName || ''));
    if (base === 'Dockerfile' || base.startsWith('Dockerfile.')) return 'dockerfile';
    const ext = path.extname(base).toLowerCase();
    return EXT_MAP[ext] || null;
  }

  /**
   * Extract signatures from a file's content using the right extractor.
   * @param {string} filePathOrName - path or name (extension drives the extractor)
   * @param {string} src - file content
   * @returns {string[]}
   */
  function extractFile(filePathOrName, src) {
    if (!src || typeof src !== 'string') return [];
    const lang = langFor(filePathOrName);
    const mod = lang ? EXTRACTORS[lang] : null;
    if (!mod || typeof mod.extract !== 'function') return [];
    try {
      const out = mod.extract(src);
      return Array.isArray(out) ? out : [];
    } catch (_) {
      return [];
    }
  }

  module.exports = { extractFile, langFor };
  
};

// ── ./src/extractors/dockerfile ──
__factories["./src/extractors/dockerfile"] = function(module, exports) {
  
  /**
   * Extract signatures from Dockerfiles.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const lines = src.split('\n').filter((l) => l.trim() && !l.trimStart().startsWith('#'));

    // FROM stages
    for (const line of lines) {
      const m = line.match(/^FROM\s+([^\s]+)(?:\s+AS\s+(\w+))?/i);
      if (m) sigs.push(`FROM ${m[1]}${m[2] ? ` AS ${m[2]}` : ''}`);
    }

    // EXPOSE ports
    const exposePorts = [];
    for (const line of lines) {
      const m = line.match(/^EXPOSE\s+([\d\s/]+)/i);
      if (m) exposePorts.push(...m[1].trim().split(/\s+/));
    }
    if (exposePorts.length > 0) sigs.push(`EXPOSE ${exposePorts.join(' ')}`);

    // ENTRYPOINT and CMD
    for (const line of lines) {
      if (/^ENTRYPOINT\s+/i.test(line)) sigs.push(line.trim());
      if (/^CMD\s+/i.test(line)) sigs.push(line.trim());
    }

    // ENV variables
    for (const line of lines) {
      const m = line.match(/^ENV\s+([\w]+)/i);
      if (m) sigs.push(`ENV ${m[1]}`);
    }

    // ARG variables
    for (const line of lines) {
      const m = line.match(/^ARG\s+([\w]+)/i);
      if (m) sigs.push(`ARG ${m[1]}`);
    }

    return sigs.slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/gdscript ──
__factories["./src/extractors/gdscript"] = function(module, exports) {
  
  /**
   * Extract signatures from Godot GDScript source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */

  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src.replace(/#.*$/gm, '');

    let className = null;
    let baseName = null;
    const addedClasses = new Set();

    const cm = stripped.match(/^class_name\s+(\w+)(?:\s+extends\s+([\w.]+))?/m);
    if (cm) {
      className = cm[1];
      if (cm[2]) baseName = cm[2];
    }
    if (!baseName) {
      const em = stripped.match(/^extends\s+([\w."/]+)/m);
      if (em) baseName = em[1];
    }

    if (className) {
      sigs.push(baseName ? `class ${className}(${baseName})` : `class ${className}`);
      addedClasses.add(className);
    } else if (baseName) {
      sigs.push(`extends ${baseName}`);
    }

    const indent = (className || baseName) ? '  ' : '';

    for (const m of stripped.matchAll(/^signal\s+(\w+)(?:\s*\(([^)]*)\))?/gm)) {
      sigs.push(`${indent}signal ${m[1]}(${normalizeParams(m[2] || '')})`);
    }

    for (const m of stripped.matchAll(/^enum\s+(\w+)\s*\{([^}]*)\}/gm)) {
      const members = m[2]
        .split(',')
        .map((s) => s.trim().split(/\s*=/)[0].trim())
        .filter(Boolean);
      sigs.push(`${indent}enum ${m[1]} { ${members.slice(0, 6).join(', ')} }`);
    }

    let constCount = 0;
    for (const m of stripped.matchAll(/^const\s+(\w+)(?:\s*:\s*[^=\n]+)?\s*:?=\s*([^\n]+)$/gm)) {
      let val = m[2].trim();
      const preloadMatch = val.match(/^preload\s*\(([^)]+)\)/);
      if (preloadMatch) {
        val = `preload(${preloadMatch[1]})`;
      } else if (val.length > 40) {
        val = val.slice(0, 37) + '...';
      }
      sigs.push(`${indent}const ${m[1]} = ${val}`);
      if (++constCount >= 5) break;
    }

    for (const m of stripped.matchAll(/^((?:@\w+(?:\([^)]*\))?\s+)*)var\s+(\w+)(?:\s*:\s*([^=\n]+?))?(?:\s*:?=\s*[^\n]+)?$/gm)) {
      const decorators = m[1] || '';
      const name = m[2];
      const hasDecorator = /@\w+/.test(decorators);
      if (!hasDecorator && name.startsWith('_')) continue;
      
      let prefix = decorators.replace(/\([^)]*\)/g, '').trim().split(/\s+/).join(' ');
      if (prefix) prefix += ' ';
      
      const type = (m[3] || '').trim();
      const typeStr = type ? `: ${type}` : '';
      sigs.push(`${indent}${prefix}var ${name}${typeStr}`);
    }

    for (const m of stripped.matchAll(/^(static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:\n]+))?\s*:/gm)) {
      const params = normalizeParams(m[3]);
      const ret = (m[4] || '').trim();
      const retStr = ret ? ` → ${ret.slice(0, 30)}` : '';
      const staticKw = m[1] ? 'static ' : '';
      sigs.push(`${indent}${staticKw}func ${m[2]}(${params})${retStr}`);
    }

    for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*:/gm)) {
      if (addedClasses.has(m[1])) continue;
      addedClasses.add(m[1]);
      sigs.push(m[2] ? `class ${m[1]}(${m[2]})` : `class ${m[1]}`);
      const startIdx = m.index + m[0].length;
      for (const meth of extractInnerMembers(stripped, startIdx)) {
        sigs.push(`  ${meth}`);
      }
    }

    return sigs.slice(0, 25);
  }

  function extractInnerMembers(stripped, startIndex) {
    const members = [];
    const lines = stripped.slice(startIndex).split('\n');
    for (const line of lines) {
      if (line.trim() === '') continue;
      if (!/^\s+/.test(line)) break;
      const fm = line.match(/^\s+(static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:\n]+))?\s*:/);
      if (fm) {
        const params = normalizeParams(fm[3]);
        const ret = (fm[4] || '').trim();
        const retStr = ret ? ` → ${ret.slice(0, 30)}` : '';
        const staticKw = fm[1] ? 'static ' : '';
        members.push(`${staticKw}func ${fm[2]}(${params})${retStr}`);
      }
    }
    return members.slice(0, 6);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim()
      .split(',')
      .map((p) => {
        const part = p.trim();
        if (!part) return '';
        const eqIdx = part.indexOf('=');
        const noDefault = eqIdx !== -1 ? part.slice(0, eqIdx).trim() : part;
        return noDefault.split(':')[0].trim();
      })
      .filter(Boolean)
      .join(', ');
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/generic ──
__factories["./src/extractors/generic"] = function(module, exports) {
  
  module.exports = { extract };

  const PATTERNS = [
    /^(?:pub\s+)?(?:async\s+)?function\s+\w+\s*\(/,
    /^(?:pub\s+)?(?:async\s+)?fn\s+\w+[\s(<]/,
    /^def\s+\w+[\s(|:]/,
    /^(?:pub\s+)?func\s+\w+\s*\(/,
    /^(?:let|let\s+rec)\s+\w+\s*[=(]/,
    /^class\s+\w+/,
    /^(?:proc|sub|method)\s+\w+\s*\(/,
  ];

  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const results = [];
    for (const raw of src.split('\n')) {
      const line = raw.trim();
      if (!line || /^[#\-]/.test(line) || /^\/\//.test(line) || line.includes('\0')) continue;
      for (const pat of PATTERNS) {
        if (pat.test(line)) { results.push(line.slice(0, 120)); break; }
      }
      if (results.length >= 15) break;
    }
    return results;
  }
  
};

// ── ./src/extractors/go ──
__factories["./src/extractors/go"] = function(module, exports) {
  
  /**
   * Extract signatures from Go source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Structs
    for (const m of stripped.matchAll(/^type\s+(\w+)\s+struct\s*\{/gm)) {
      sigs.push(`type ${m[1]} struct`);
    }

    // Interfaces
    for (const m of stripped.matchAll(/^type\s+(\w+)\s+interface\s*\{/gm)) {
      sigs.push(`type ${m[1]} interface`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const method of extractInterfaceMethods(block)) sigs.push(`  ${method}`);
    }

    // Functions and methods — capture return type between ) and {
    for (const m of stripped.matchAll(/^func\s+(?:\((\w+)\s+[\w*]+\)\s+)?(\w+)\s*\(([^)]*)\)([^{]*)\{/gm)) {
      const receiver = m[1] ? `(${m[1]}) ` : '';
      const retType = m[4] ? m[4].trim().replace(/\s+/g, ' ') : '';
      const retStr = retType ? ` \u2192 ${retType.slice(0, 30)}` : '';
      sigs.push(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 2000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractInterfaceMethods(block) {
    const methods = [];
    for (const m of block.matchAll(/^\s+(\w+)\s*\(([^)]*)\)([^\n]*)/gm)) {
      const retType = m[3] ? m[3].trim().replace(/\s+/g, ' ') : '';
      const retStr = retType ? ` \u2192 ${retType.slice(0, 30)}` : '';
      methods.push(`${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }
    return methods.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/graphql ──
__factories["./src/extractors/graphql"] = function(module, exports) {
  
  /**
   * Extract signatures from GraphQL schema / operation files.
   * Captures type, interface, enum, input, union, scalar, query, mutation,
   * subscription, fragment definitions.
   *
   * @param {string} src - Raw GraphQL content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Strip comments (# style)
    const stripped = src.replace(/#[^\n]*/g, '');

    // Schema type definitions: type Foo [implements Bar] { ... }
    for (const m of stripped.matchAll(
      /\b(type|interface|input)\s+(\w+)(?:\s+implements\s+([\w\s&]+))?\s*\{/g
    )) {
      const implements_ = m[3] ? ` implements ${m[3].trim().replace(/\s+/g, ' ')}` : '';
      sigs.push(`${m[1]} ${m[2]}${implements_}`);
    }

    // enum
    for (const m of stripped.matchAll(/\benum\s+(\w+)\s*\{/g)) {
      sigs.push(`enum ${m[1]}`);
    }

    // union
    for (const m of stripped.matchAll(/\bunion\s+(\w+)\s*=/g)) {
      sigs.push(`union ${m[1]}`);
    }

    // scalar
    for (const m of stripped.matchAll(/\bscalar\s+(\w+)/g)) {
      sigs.push(`scalar ${m[1]}`);
    }

    // extend type / extend interface
    for (const m of stripped.matchAll(/\bextend\s+(type|interface)\s+(\w+)/g)) {
      sigs.push(`extend ${m[1]} ${m[2]}`);
    }

    // Query / Mutation / Subscription operations
    for (const m of stripped.matchAll(
      /\b(query|mutation|subscription)\s+(\w+)\s*(?:\([^)]*\))?\s*\{/g
    )) {
      sigs.push(`${m[1]} ${m[2]}`);
    }

    // Named fragments
    for (const m of stripped.matchAll(/\bfragment\s+(\w+)\s+on\s+(\w+)/g)) {
      sigs.push(`fragment ${m[1]} on ${m[2]}`);
    }

    // Top-level schema { query: ... }
    if (/\bschema\s*\{/.test(stripped)) {
      sigs.push('schema { ... }');
    }

    return sigs;
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/html ──
__factories["./src/extractors/html"] = function(module, exports) {
  
  /**
   * Extract signatures from HTML files.
   * Focuses on id/class attributes, forms, and script tags.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Page title
    const titleMatch = src.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
    if (titleMatch) sigs.push(`title: ${titleMatch[1].trim()}`);

    // Forms with id/action
    for (const m of src.matchAll(/<form\s+([^>]*)>/gi)) {
      const attrs = m[1];
      const id = attrs.match(/id=["']?(\w+)/i);
      const action = attrs.match(/action=["']?([^"'\s>]+)/i);
      if (id) sigs.push(`form#${id[1]}${action ? ` action="${action[1]}"` : ''}`);
    }

    // Elements with id
    for (const m of src.matchAll(/<(\w+)\s+[^>]*id=["'](\w+)["'][^>]*>/gi)) {
      if (['html', 'head', 'body', 'script', 'style', 'link', 'meta'].includes(m[1].toLowerCase())) continue;
      sigs.push(`${m[1]}#${m[2]}`);
    }

    // Data attributes (data-component, data-controller etc)
    for (const m of src.matchAll(/data-(?:component|controller|view|page)=["'](\w[\w-]*)/gi)) {
      sigs.push(`data-${m[0].match(/data-(\w[\w-]*)/i)[1]}: ${m[1]}`);
    }

    return sigs.slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/java ──
__factories["./src/extractors/java"] = function(module, exports) {
  
  /**
   * Extract signatures from Java source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes and interfaces
    const typeRegex = /^(?:public\s+|protected\s+)?(?:abstract\s+|final\s+)?(class|interface|enum)\s+(\w+)(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<>, .]+)?\s*\{/gm;
    for (const m of stripped.matchAll(typeRegex)) {
      sigs.push(`${m[1]} ${m[2]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const meth of extractMembers(block)) sigs.push(`  ${meth}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1;
    let i = startIndex;
    const end = Math.min(src.length, startIndex + 5000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:<[^>]+>\s+)?([\w<>\[\], ?.]+)\s+(\w+)\s*\(([^)]*)\)/gm;
    for (const m of block.matchAll(methodRe)) {
      const ret = normalizeType(m[1]);
      const retStr = ret ? ` → ${ret}` : '';
      members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/\s+/g, ' ').slice(0, 30);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/javascript ──
__factories["./src/extractors/javascript"] = function(module, exports) {
  
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');

  /**
   * Extract signatures from JavaScript source code.
   * Top-level declarations and class members carry a `:start-end` line anchor
   * (see line-anchor.js); kept parallel to `sigs` and applied once at return.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];
    const anchors = [];
    const returnHints = buildReturnHints(src);

    // Block comments are blanked newline-by-newline (non-newline chars → spaces)
    // so character offsets AND line numbers stay exact for anchors.
    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));

    const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
    // End line for a function whose match ends at `matchEnd` (before its body brace).
    const fnEndLine = (matchEnd, startLn) => {
      const brace = stripped.indexOf('{', matchEnd);
      return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
    };

    // Classes
    const classRegex = /^(export\s+(?:default\s+)?)?class\s+(\w+)(?:\s+extends\s+[\w.]+)?\s*\{/gm;
    for (const m of stripped.matchAll(classRegex)) {
      const prefix = m[1] ? m[1].trim() + ' ' : '';
      const bodyStart = m.index + m[0].length;
      sigs.push(`${prefix}class ${m[2]}`);
      anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
      const block = extractBlock(stripped, bodyStart);
      for (const meth of extractClassMembers(block, returnHints)) {
        sigs.push(`  ${meth.text}`);
        anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
      }
    }

    // Exported named functions
    for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
      const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
      const retStr = formatReturnHint(returnHints.get(m[1]));
      const startLn = lineAt(stripped, m.index);
      sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
      anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
    }

    // Exported arrow functions
    for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = formatReturnHint(returnHints.get(m[1]));
      const startLn = lineAt(stripped, m.index);
      sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
      anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
    }

    // module.exports = { ... }
    const moduleExports = stripped.match(/^module\.exports\s*=\s*\{([^}]+)\}/m);
    if (moduleExports) {
      const names = moduleExports[1].split(',').map((s) => s.trim()).filter(Boolean);
      if (names.length > 0) {
        const startLn = lineAt(stripped, moduleExports.index);
        sigs.push(`module.exports = { ${names.join(', ')} }`);
        anchors.push([startLn, lineAt(stripped, moduleExports.index + moduleExports[0].length)]);
      }
    }

    // Top-level named functions (non-exported)
    for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
      const asyncKw = m[0].startsWith('async') ? 'async ' : '';
      const retStr = formatReturnHint(returnHints.get(m[1]));
      const startLn = lineAt(stripped, m.index);
      sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
      anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
    }

    return sigs
      .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
      .slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1;
    let i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  // Returns members as { text, start, end } where start/end are char offsets
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
  // per-method line anchors that span the method body.
  function extractClassMembers(block, returnHints) {
    const members = [];
    for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
      if (/^_/.test(m[1])) continue;
      const bodyStart = m.index + m[0].length; // just past the opening brace
      const end = bodyStart + extractBlock(block, bodyStart).length;
      const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
      if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
      const isAsync = m[0].includes('async ') ? 'async ' : '';
      const isStatic = m[0].includes('static ') ? 'static ' : '';
      const retStr = formatReturnHint(returnHints.get(m[1]));
      members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
    }
    return members.slice(0, 8);
  }

  function buildReturnHints(src) {
    const hints = new Map();
    for (const m of src.matchAll(/\/\*\*[\s\S]*?@returns?\s+\{([^}]+)\}[\s\S]*?\*\/\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g)) {
      hints.set(m[2], normalizeType(m[1]));
    }
    for (const m of src.matchAll(/\/\*\*[\s\S]*?@returns?\s+\{([^}]+)\}[\s\S]*?\*\/\s*export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g)) {
      hints.set(m[2], normalizeType(m[1]));
    }
    for (const m of src.matchAll(/\/\*\*[\s\S]*?@returns?\s+\{([^}]+)\}[\s\S]*?\*\/\s*(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(/g)) {
      hints.set(m[2], normalizeType(m[1]));
    }
    return hints;
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/\s+/g, ' ').slice(0, 25);
  }

  function formatReturnHint(type) {
    return type ? ` → ${type}` : '';
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/kotlin ──
__factories["./src/extractors/kotlin"] = function(module, exports) {
  
  /**
   * Extract signatures from Kotlin source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes, objects, interfaces
    for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:data\s+|sealed\s+|abstract\s+|open\s+)?(class|object|interface)\s+(\w+)(?:[^{]*)\{/gm)) {
      sigs.push(`${m[1]} ${m[2]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const meth of extractMembers(block)) sigs.push(`  ${meth}`);
    }

    // Top-level functions — capture `: RetType` after params
    for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{=]+))?/gm)) {
      const suspend = m[0].includes('suspend') ? 'suspend ' : '';
      const retType = m[3] ? m[3].trim().replace(/\s+/g, ' ') : '';
      const retStr = retType ? ` \u2192 ${retType.slice(0, 25)}` : '';
      sigs.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    for (const m of block.matchAll(/^\s+(?:public\s+|internal\s+|override\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{=]+))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const suspend = m[0].includes('suspend') ? 'suspend ' : '';
      const retType = m[3] ? m[3].trim().replace(/\s+/g, ' ') : '';
      const retStr = retType ? ` \u2192 ${retType.slice(0, 25)}` : '';
      members.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim()
      .split(',')
      .map((p) => p.trim().split(':')[0].trim())
      .filter(Boolean)
      .join(', ');
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/line-anchor ──
__factories["./src/extractors/line-anchor"] = function(module, exports) {
  
  /**
   * Line-anchor helpers for Surgical Context (v6.11.0).
   *
   * Signatures carry their source location as a `:start-end` suffix so an agent
   * can read the exact lines instead of re-opening the whole file. The anchor is
   * a plain string suffix, which keeps the existing `string[]` signature contract
   * intact — ranker, adapters, and CLAUDE.md render it for free.
   */

  /**
   * 1-based line number of character index `idx` within `src`.
   * Counts newlines in the prefix, so it stays correct as long as the source
   * being indexed preserves every newline (see the newline-preserving comment
   * strips in the extractors).
   *
   * @param {string} src
   * @param {number} idx
   * @returns {number}
   */
  function lineAt(src, idx) {
    let line = 1;
    const end = Math.min(idx, src.length);
    for (let i = 0; i < end; i++) {
      if (src.charCodeAt(i) === 10) line++;
    }
    return line;
  }

  /**
   * Render an anchor suffix: `  :start-end`.
   * @param {number} start
   * @param {number} end
   * @returns {string}
   */
  function anchor(start, end) {
    return `  :${start}-${end}`;
  }

  /**
   * Append a line anchor to a signature string.
   * @param {string} sig
   * @param {number} start
   * @param {number} end
   * @returns {string}
   */
  function withAnchor(sig, start, end) {
    return `${sig}${anchor(start, end)}`;
  }

  module.exports = { lineAt, anchor, withAnchor };
  
};

// ── ./src/extractors/markdown ──
__factories["./src/extractors/markdown"] = function(module, exports) {
  
  /**
   * Lightweight markdown technical indexer.
   * Captures headings and fenced code block language hints only.
   *
   * @param {string} src - Raw markdown content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Headings: # .. ######
    for (const m of src.matchAll(/^(#{1,6})\s+(.+)$/gm)) {
      const level = m[1].length;
      const title = m[2].trim().replace(/\s+/g, ' ');
      if (title) sigs.push(`h${level} ${title}`);
    }

    // Fenced code blocks: ```lang
    for (const m of src.matchAll(/^```\s*([A-Za-z0-9_+-]*)\s*$/gm)) {
      const lang = m[1] ? m[1].toLowerCase() : 'plain';
      sigs.push(`code-fence ${lang}`);
    }

    return Array.from(new Set(sigs)).slice(0, 40);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/patterns ──
__factories["./src/extractors/patterns"] = function(module, exports) {
  
  /**
   * Phase D: Cross-module pattern inference and architectural detection.
   * Identifies DI patterns, service/repo layers, circular deps, type linkage, and unsafe patterns.
   *
   * @param {string} src - Raw source code
   * @returns {string[]} Array of pattern signatures
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // ────────────────────────────────────────────────────────────────
    // Dependency Injection Pattern Detection
    // ────────────────────────────────────────────────────────────────

    // Service container / factory patterns
    const containerRe = /(?:class|function|const)\s+([A-Z]\w*(?:Container|Factory|Registry|Provider|Injector))\b/g;
    for (const m of src.matchAll(containerRe)) {
      sigs.push(`di-container ${m[1]}`);
    }

    // Service decorators: @Injectable, @Service, @Singleton, @Provide
    const decoratorRe = /@(?:Injectable|Service|Singleton|Provide|Module|Component)\s*(?:\([^)]*\))?\s*(?:class|export\s+class|const\s+)\s+([A-Z]\w*)/g;
    for (const m of src.matchAll(decoratorRe)) {
      sigs.push(`service-decorated ${m[1]}`);
    }

    // Dependency injection via constructor: constructor(private readonly ...: Service)
    const ctorDiRe = /constructor\s*\([^)]*(?:private|protected)?\s+(?:readonly\s+)?([a-z_]\w*)\s*:\s*([A-Z]\w*)/g;
    const diServices = new Set();
    for (const m of src.matchAll(ctorDiRe)) {
      diServices.add(m[1]);
    }
    if (diServices.size > 0) {
      sigs.push(`di-injection ${diServices.size} params`);
    }

    // ────────────────────────────────────────────────────────────────
    // Service/Repository/Middleware Layer Detection
    // ────────────────────────────────────────────────────────────────

    // Repository pattern: extends Repository, implements IRepository
    const repoRe = /class\s+([A-Z]\w*(?:Repository|Repo|DataAccess))\b/g;
    for (const m of src.matchAll(repoRe)) {
      sigs.push(`repo ${m[1]}`);
    }

    // Service layer: @Service or ServiceImpl pattern
    const serviceRe = /(?:export\s+)?class\s+([A-Z]\w*Service\b)/g;
    for (const m of src.matchAll(serviceRe)) {
      sigs.push(`service ${m[1]}`);
    }

    // Middleware detection: app.use(), router.use(), middleware function
    if (/app\.use\s*\(|router\.use\s*\(|\.use\s*\(\s*function|middleware|app\.get\s*\(\s*['"`]\/[^'"`]*['"`]/.test(src)) {
      sigs.push('middleware-present');
    }

    // ────────────────────────────────────────────────────────────────
    // Type Linkage: Exported types → Implementations
    // ────────────────────────────────────────────────────────────────

    // Export type/interface followed by class implementing it
    const exportTypeRe = /export\s+(?:type|interface)\s+([A-Z]\w*)/g;
    const exportedTypes = new Set();
    for (const m of src.matchAll(exportTypeRe)) {
      exportedTypes.add(m[1]);
    }

    // Check if exported types have implementations
    for (const type of exportedTypes) {
      const implRe = new RegExp(`class\\s+([A-Z]\\w*)\\s+(?:extends|implements)\\s+(?:.*\\s+)?${type}\\b`, 'i');
      if (implRe.test(src)) {
        sigs.push(`type-impl ${type}`);
      }
    }

    // ────────────────────────────────────────────────────────────────
    // Unsafe Pattern Detection
    // ────────────────────────────────────────────────────────────────

    // Unchecked nulls / optional without validation
    if (/\?\s*{|Optional\s*<|\?\s*\.get\(\)|\?\s*\[|\?\s*\.length|\.split\(\)\.filter\(Boolean\)|if\s*\(\s*!.*\).*throw/.test(src)) {
      sigs.push('unsafe-null-check');
    }

    // Missing validation (direct use of user input)
    const userInputRe = /(?:request|input|params|body|query|args)\s*\[\s*['"][^'"]*['"]\s*\]|req(?:uest)?\..*\s*==|params\.split|String\(.*\)\.toLowerCase/;
    if (userInputRe.test(src)) {
      sigs.push('unsafe-input-validation');
    }

    // Weak error handling (catch {} or empty catch)
    if (/catch\s*\(\s*\)\s*\{|\}\s*catch\s*\{(\s*\/\/|\s*\})/.test(src)) {
      sigs.push('weak-error-handling');
    }

    // Direct error exposure
    if (/throw\s+new\s+Error\(|console\s*\.\s*error|res\.status\(500\)\.send\(err\)/.test(src)) {
      sigs.push('unsafe-error-exposure');
    }

    // ────────────────────────────────────────────────────────────────
    // Circular Dependency Hints
    // ────────────────────────────────────────────────────────────────

    // Mutual imports detected (A imports B, B imports A) — can't directly detect in single file,
    // but we can flag suspicious patterns
    const importRe = /(?:import|require)\s+(?:{[^}]*}|[a-zA-Z_]\w*)\s+from\s+['"]\.?\.?\/[^'"]+['"]/g;
    const importCount = (src.match(importRe) || []).length;
    if (importCount > 5) {
      sigs.push(`heavy-imports ${importCount}`);
    }

    // ────────────────────────────────────────────────────────────────
    // Layer/Module Organization Hints
    // ────────────────────────────────────────────────────────────────

    // Controller/Handler layer
    const controllerRe = /(?:class|export)\s+([A-Z]\w*(?:Controller|Handler|Route))\b/g;
    for (const m of src.matchAll(controllerRe)) {
      sigs.push(`controller ${m[1]}`);
    }

    // Use case / Domain logic
    if (/UseCase|Command|Query|UseCase\b|Interactor/.test(src)) {
      sigs.push('domain-usecase');
    }

    return Array.from(new Set(sigs)).slice(0, 60);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/php ──
__factories["./src/extractors/php"] = function(module, exports) {
  
  /**
   * Extract signatures from PHP source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/#.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes and interfaces
    const typeRe = /^(?:abstract\s+)?(?:class|interface|trait)\s+(\w+)(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w, ]+)?\s*\{/gm;
    for (const m of stripped.matchAll(typeRe)) {
      const kind = m[0].trimStart().startsWith('interface') ? 'interface' :
        m[0].trimStart().startsWith('trait') ? 'trait' : 'class';
      sigs.push(`${kind} ${m[1]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const meth of extractMembers(block)) sigs.push(`  ${meth}`);
    }

    // Top-level functions
    for (const m of stripped.matchAll(/^function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/gm)) {
      const ret = normalizeType(m[3]);
      const retStr = ret ? ` → ${ret}` : '';
      sigs.push(`function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/gm;
    for (const m of block.matchAll(methodRe)) {
      if (m[1].startsWith('_')) continue;
      const isStatic = m[0].includes('static ') ? 'static ' : '';
      const ret = normalizeType(m[3]);
      const retStr = ret ? ` → ${ret}` : '';
      members.push(`${isStatic}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/[;\s]+$/g, '').replace(/\s+/g, ' ').slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/prdiff ──
__factories["./src/extractors/prdiff"] = function(module, exports) {
  
  /**
   * Compare signature arrays and produce compact diff markers.
   * @param {string[]} baseSigs
   * @param {string[]} currentSigs
   * @returns {{added:string[], removed:string[], modified:string[]}}
   */
  function diffSignatures(baseSigs, currentSigs) {
    const base = new Set(baseSigs || []);
    const curr = new Set(currentSigs || []);

    const added = [...curr].filter((s) => !base.has(s));
    const removed = [...base].filter((s) => !curr.has(s));

    const byName = (arr) => {
      const m = new Map();
      for (const s of arr) {
        const n = extractName(s);
        if (!n) continue;
        if (!m.has(n)) m.set(n, []);
        m.get(n).push(s);
      }
      return m;
    };

    const aBy = byName(added);
    const rBy = byName(removed);
    const modified = [];

    for (const [name] of aBy) {
      if (rBy.has(name)) modified.push(name);
    }

    return { added, removed, modified };
  }

  /**
   * Extract the declared symbol name from a signature line.
   *
   * Robust to the real forms SigMap emits — `export class X`, `const x = () =>`,
   * `async function x`, members, a trailing `:start-end` anchor, and `→ return`
   * suffixes. Anchored so it never returns a mid-string fragment (the old regex
   * could turn a signature into a 2-char name like `is`), and returns '' for
   * non-symbol lines (`module.exports = {…}`, markdown headers) instead of guessing.
   */
  function extractName(sig) {
    if (!sig) return '';
    // Drop a trailing `:start-end` (or `:line`) line anchor.
    let t = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
    // Re-export / barrel lines carry no single declared name.
    if (/^(?:module\.)?exports\b/.test(t) || /^export\s*\{/.test(t) || /^export\s+\*/.test(t)) return '';
    // Strip leading modifiers so the keyword/name is at the start.
    t = t.replace(/^export\s+/, '')
         .replace(/^default\s+/, '')
         .replace(/^(?:public|private|protected|static|abstract|final|override|readonly)\s+/g, '')
         .replace(/^async\s+/, '');
    let m;
    // Declared forms: function/def/func/fn/class/interface/trait/struct/enum/record/type <name>
    if ((m = t.match(/^(?:def|function|func|fn|class|interface|trait|struct|enum|record|type)\s+([A-Za-z_$][\w$]*)/))) return m[1];
    // const/let/var/val <name> = …  (arrow functions, assigned values)
    if ((m = t.match(/^(?:const|let|var|val)\s+([A-Za-z_$][\w$]*)/))) return m[1];
    // Call / method form: <name>(…)
    if ((m = t.match(/^([A-Za-z_$][\w$]*)\s*\(/))) return m[1];
    // Lone identifier (e.g. a collapsed `symbol` after the anchor was stripped).
    if ((m = t.match(/^([A-Za-z_$][\w$]*)$/))) return m[1];
    return '';
  }

  module.exports = { diffSignatures, extractName };
  
};

// ── ./src/extractors/properties ──
__factories["./src/extractors/properties"] = function(module, exports) {
  
  /**
   * Extract signatures from .properties configuration files.
   * Captures key names, grouped by prefixes where possible.
   *
   * @param {string} src - Raw properties content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const lines = src.split(/\r?\n/);
    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('!')) continue;

      const idxEq = trimmed.indexOf('=');
      const idxColon = trimmed.indexOf(':');
      const idx = idxEq >= 0 && idxColon >= 0 ? Math.min(idxEq, idxColon) : Math.max(idxEq, idxColon);
      if (idx <= 0) continue;

      const key = trimmed.slice(0, idx).trim();
      if (!key) continue;

      const parts = key.split('.').filter(Boolean);
      if (parts.length >= 2) {
        sigs.push(`group ${parts[0]}.${parts[1]}`);
      }
      sigs.push(`key ${key}`);
    }

    return Array.from(new Set(sigs)).slice(0, 50);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/protobuf ──
__factories["./src/extractors/protobuf"] = function(module, exports) {
  
  /**
   * Extract signatures from Protocol Buffer (.proto) files.
   * Captures message, enum, service, rpc, oneof, extend definitions.
   *
   * @param {string} src - Raw .proto content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Strip single-line and block comments
    const stripped = src
      .replace(/\/\/[^\n]*/g, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // syntax / package / option (top-level metadata)
    const syntaxM = stripped.match(/\bsyntax\s*=\s*"([^"]+)"/);
    if (syntaxM) sigs.push(`syntax = "${syntaxM[1]}"`);

    const pkgM = stripped.match(/\bpackage\s+([\w.]+)\s*;/);
    if (pkgM) sigs.push(`package ${pkgM[1]}`);

    // message <Name> { ... }
    for (const m of stripped.matchAll(/\bmessage\s+(\w+)\s*\{/g)) {
      sigs.push(`message ${m[1]}`);
    }

    // enum <Name> { ... }
    for (const m of stripped.matchAll(/\benum\s+(\w+)\s*\{/g)) {
      sigs.push(`enum ${m[1]}`);
    }

    // service <Name> { ... }
    for (const m of stripped.matchAll(/\bservice\s+(\w+)\s*\{/g)) {
      sigs.push(`service ${m[1]}`);
    }

    // rpc <Name>(<Request>) returns (<Response>)
    for (const m of stripped.matchAll(
      /\brpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s+returns\s*\(\s*(stream\s+)?(\w+)\s*\)/g
    )) {
      const req = `${m[2] || ''}${m[3]}`.trim();
      const res = `${m[4] || ''}${m[5]}`.trim();
      sigs.push(`rpc ${m[1]}(${req}) returns (${res})`);
    }

    // oneof <name>
    for (const m of stripped.matchAll(/\boneof\s+(\w+)\s*\{/g)) {
      sigs.push(`oneof ${m[1]}`);
    }

    // extend <TypeName>
    for (const m of stripped.matchAll(/\bextend\s+([\w.]+)\s*\{/g)) {
      sigs.push(`extend ${m[1]}`);
    }

    return sigs;
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/python ──
__factories["./src/extractors/python"] = function(module, exports) {
  
  const path = require('path');
  const { lineAt } = __require('./src/extractors/line-anchor');

  /**
   * 1-based line of the last source line belonging to a top-level (indent 0)
   * def/class body that starts at `startLine` (1-based). Trailing blank lines
   * are excluded.
   * @param {string[]} srcLines
   * @param {number} startLine
   * @returns {number}
   */
  function pyBlockEnd(srcLines, startLine) {
    let end = startLine;
    for (let i = startLine; i < srcLines.length; i++) {
      const line = srcLines[i];
      if (line.trim() === '') continue;
      const indent = line.match(/^(\s*)/)[1].length;
      if (indent === 0) break;
      end = i + 1;
    }
    return end;
  }

  /**
   * Try to extract signatures using the native Python AST extractor.
   * Returns null if Python3 is unavailable or the script returns empty results.
   * @param {string} filePath - Absolute path to the Python file
   * @returns {string[]|null}
   */
  function tryNativeExtract(filePath) {
    try {
      const { execFileSync } = require('child_process');
      const scriptPath = path.join(__dirname, 'python_ast.py');
      const result = execFileSync('python3', [scriptPath, filePath], {
        timeout: 5000,
        encoding: 'utf8',
      });
      const sigs = JSON.parse(result.trim());
      if (Array.isArray(sigs) && sigs.length > 0) return sigs;
    } catch (_) {}
    return null;
  }

  /**
   * Extract signatures from Python source code.
   * When a real file path is provided, tries the native Python AST extractor first
   * (more accurate for multiline signatures, stacked decorators, and type annotations).
   * Falls back to the regex approach if Python3 is unavailable or returns no results.
   * @param {string} src - Raw file content
   * @param {string} [filePath] - Optional absolute path to the source file
   * @returns {string[]} Array of signature strings
   */
  function extract(src, filePath) {
    // Prefer native AST extractor when a real file path is available
    if (filePath && typeof filePath === 'string') {
      const native = tryNativeExtract(filePath);
      if (native) return native;
    }
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // noComments: strip only # comments, keep docstrings (needed for @decorator detection)
    const noComments = src.replace(/#.*$/gm, '');
    // stripped: also strip docstrings (safe for regex matching). Docstrings are
    // blanked newline-by-newline (non-newline chars → spaces) so character
    // offsets and line numbers stay exact for line anchors.
    const stripped = noComments
      .replace(/"""[\s\S]*?"""/g, (m) => m.replace(/[^\n]/g, ' '))
      .replace(/'''[\s\S]*?'''/g, (m) => m.replace(/[^\n]/g, ' '));
    const srcLines = src.split('\n');

    // Classes
    for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s*\(([^)]*)\))?\s*:/gm)) {
      const className = m[1];
      const baseName = m[2] ? m[2].trim() : '';
      const bodyStart = m.index + m[0].length;
      const clsStart = lineAt(stripped, m.index);
      const clsAnchor = `  :${clsStart}-${pyBlockEnd(srcLines, clsStart)}`;

      // Try @dataclass collapse
      const dcFields = tryExtractDataclassFields(stripped, m.index);
      if (dcFields !== null) {
        sigs.push(`@dataclass ${className}(${dcFields})${clsAnchor}`);
        continue;
      }

      // Try BaseModel/BaseSettings collapse
      if (/(BaseModel|BaseSettings)/.test(baseName)) {
        const bmFields = tryExtractBaseModelFields(stripped, bodyStart);
        if (bmFields) {
          sigs.push(`class ${className}(${baseName}) ${bmFields}${clsAnchor}`);
          continue;
        }
      }

      const baseStr = baseName ? `(${baseName})` : '';
      sigs.push(`class ${className}${baseStr}${clsAnchor}`);

      // Class-level ALL_CAPS constants
      for (const c of extractClassConstants(stripped, bodyStart)) {
        sigs.push(`  ${c}`);
      }

      // Methods
      const methods = extractClassMethods(stripped, bodyStart);
      for (const meth of methods) sigs.push(`  ${meth}`);
    }

    // Top-level functions
    for (const m of stripped.matchAll(/^((?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*[^:]+)?)\s*:/gm)) {
      if (/^_/.test(m[2])) continue;
      const asyncKw = m[1].trimStart().startsWith('async') ? 'async ' : '';
      const params = normalizeParams(m[3]);
      const retType = extractReturnType(m[1] + ':');
      const retStr = retType ? ` → ${retType}` : '';
      const hint = extractDocHint(src, m[2], m[0]);
      const hintStr = hint ? `  # ${hint}` : '';
      const fnStart = lineAt(stripped, m.index);
      const fnAnchor = `  :${fnStart}-${pyBlockEnd(srcLines, fnStart)}`;
      sigs.push(`${asyncKw}def ${m[2]}(${params})${retStr}${fnAnchor}${hintStr}`);
    }

    // FastAPI router endpoints: @router.METHOD("path") + async def name(...)
    const lines = noComments.split('\n');
    for (let i = 0; i < lines.length - 1; i++) {
      const decLine = lines[i].trim();
      const rm = decLine.match(/^@\w+\.(get|post|put|patch|delete|head)\s*\(\s*['"]([^'"]+)['"]/);
      if (!rm) continue;
      for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
        const fl = lines[j].trim();
        const fm = fl.match(/^(?:async\s+)?def\s+(\w+)/);
        if (fm) { const rs = j + 1; sigs.push(`${rm[1].toUpperCase()} ${rm[2]}  →  ${fm[1]}()  :${rs}-${pyBlockEnd(srcLines, rs)}`); break; }
        if (fl && !fl.startsWith('@') && !fl.startsWith('#')) break;
      }
    }

    return sigs.slice(0, 30);
  }

  function extractClassMethods(stripped, startIndex) {
    const methods = [];
    const lines = stripped.slice(startIndex).split('\n');
    for (const line of lines) {
      if (line.trim() === '') continue;
      const indent = line.match(/^(\s+)/);
      if (!indent) break;
      const m = line.match(/^\s+(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:]+))?\s*:/);
      if (m) {
        if (m[1].startsWith('__') && m[1] !== '__init__') continue;
        if (m[1].startsWith('_') && !m[1].startsWith('__')) continue;
        const asyncKw = line.trimStart().startsWith('async') ? 'async ' : '';
        const params = normalizeParams(m[2]).replace(/^self,?\s*/, '').replace(/^cls,?\s*/, '');
        const retType = m[3] ? m[3].trim().replace(/Optional\[([^\]]+)\]/, '$1|None').slice(0, 30) : '';
        const retStr = retType ? ` → ${retType}` : '';
        methods.push(`${asyncKw}def ${m[1]}(${params})${retStr}`);
      }
    }
    return methods.slice(0, 8);
  }

  function tryExtractDataclassFields(stripped, classIndex) {
    const before = stripped.slice(Math.max(0, classIndex - 120), classIndex);
    if (!/@dataclass/.test(before)) return null;
    const lines = stripped.slice(classIndex).split('\n');
    const fields = [];
    for (const line of lines.slice(1)) {
      if (line.trim() === '') continue;
      if (!line.match(/^\s+/)) break;
      const f = line.match(/^\s+(\w+)\s*:\s*([^=\n]+?)(?:\s*=.*)?$/);
      if (!f) break;
      const isOptional = f[2].includes('Optional') || /=\s*None/.test(line);
      fields.push(isOptional ? `${f[1]}?` : f[1]);
    }
    return fields.length ? fields.join(', ') : null;
  }

  function tryExtractBaseModelFields(stripped, bodyStart) {
    const lines = stripped.slice(bodyStart, bodyStart + 800).split('\n');
    const fields = [];
    let foundFirst = false;
    for (const line of lines) {
      if (line.trim() === '') continue;
      if (foundFirst && !/^\s/.test(line)) break;
      if (!line.match(/^\s{4}\w/)) continue;
      foundFirst = true;
      const f = line.match(/^\s{4}(\w+)\s*(?::\s*([^=\n]+?))?(?:\s*=\s*(.*))?$/);
      if (!f || f[1].startsWith('_') || f[1] === 'class' || f[1] === 'def') continue;
      const isOptional = (f[2] || '').includes('Optional') || f[3] !== undefined;
      fields.push(isOptional ? `${f[1]}?` : `${f[1]}*`);
    }
    return fields.length ? `{${fields.slice(0, 6).join(', ')}}` : null;
  }

  function extractClassConstants(stripped, startIndex) {
    const lines = stripped.slice(startIndex).split('\n');
    const consts = [];
    for (const line of lines) {
      if (!line.match(/^\s+/)) break;
      const c = line.match(/^\s+([A-Z][A-Z0-9_]{2,})\s*=\s*(.+)/);
      if (!c) continue;
      let val = c[2].trim();
      const items = (val.match(/"([^"]+)"/g) || val.match(/'([^']+)'/g) || []);
      if (items.length > 3) val = `[${items[0].replace(/['"]/g, '')}..${items[items.length - 1].replace(/['"]/g, '')}]`;
      if (val.length > 40) val = val.slice(0, 37) + '...';
      consts.push(`${c[1]}=${val}`);
    }
    return consts.slice(0, 3);
  }

  function extractReturnType(sigLine) {
    const m = sigLine.match(/->\s*([^:]+):/);
    if (!m) return '';
    let rt = m[1].trim();
    rt = rt.replace(/Optional\[([^\]]+)\]/, '$1|None');
    return rt.length > 30 ? rt.slice(0, 27) + '...' : rt;
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim()
      .split(',')
      .map((p) => {
        const part = p.trim();
        if (!part) return '';
        const stars = part.match(/^(\*{1,2})/)?.[1] || '';
        const rest = part.slice(stars.length);
        const eqIdx = rest.indexOf('=');
        const noDefault = eqIdx !== -1 ? rest.slice(0, eqIdx).trim() : rest.trim();
        const clean = noDefault
          .replace(/Optional\[([^\]]+)\]/, '$1?')
          .replace(/Union\[([^\]]+),\s*None\]/, '$1?');
        return stars + clean;
      })
      .filter((p) => p && p !== 'self' && p !== 'cls')
      .join(', ');
  }

  function extractDocHint(src, fnName, fnSigLine) {
    if (!src || !fnName || !fnSigLine) return '';
    const escSig = fnSigLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(`^${escSig.replace(/\s+/g, '\\s+')}`, 'm');
    const pos = src.search(re);
    if (pos === -1) return '';
    const afterSig = src.slice(pos + fnSigLine.length, pos + fnSigLine.length + 600);

    // Find first non-empty line after function signature indentation
    const m = afterSig.match(/^\s*\n\s*(?:[rubfRUBF]{0,2})?("""|''')([\s\S]*?)\1/m);
    if (!m) return '';

    const firstLine = m[2]
      .split('\n')
      .map((l) => l.trim())
      .find(Boolean);
    if (!firstLine) return '';

    const sentence = firstLine.split(/[.!?]/)[0].trim();
    return sentence.slice(0, 60);
  }

  module.exports = { extract, tryNativeExtract };
  
};

// ── ./src/extractors/python_dataclass ──
__factories["./src/extractors/python_dataclass"] = function(module, exports) {
  
  /**
   * Extract Python dataclass, Pydantic model, and SQLAlchemy ORM metadata.
   * Focuses on model fields, validation, and relationships.
   *
   * @param {string} src - Raw Python content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Dataclass definitions
    const dataclassRe = /@dataclass(?:\([^)]*\))?[\s\n]+class\s+([A-Z]\w*)/g;
    for (const m of src.matchAll(dataclassRe)) {
      sigs.push(`dataclass ${m[1]}`);
    }

    // Pydantic BaseModel classes
    const pydanticRe = /class\s+([A-Z]\w*)\s*\([^)]*BaseModel[^)]*\)/g;
    for (const m of src.matchAll(pydanticRe)) {
      sigs.push(`model ${m[1]}`);
    }

    // Pydantic v2 model_validate / field definitions
    if (/model_validate|field_validator|computed_field/.test(src)) {
      sigs.push('pydantic v2+');
    }

    // SQLAlchemy model classes
    const sqlalchemyRe = /class\s+([A-Z]\w*)\s*\([^)]*(?:Base|declarative_base)[^)]*\)/g;
    for (const m of src.matchAll(sqlalchemyRe)) {
      sigs.push(`orm ${m[1]}`);
    }

    // Model fields with type hints (for dataclass/Pydantic)
    const fieldRe = /^\s+([a-z_]\w*)\s*:\s*([A-Z]\w*|List|Dict|Optional|Union)[^=]*/gm;
    const fields = new Set();
    for (const m of src.matchAll(fieldRe)) {
      if (fields.size < 15) fields.add(m[1]);
    }
    for (const f of fields) {
      sigs.push(`field ${f}`);
    }

    // SQLAlchemy Column definitions
    const columnRe = /([a-z_]\w*)\s*=\s*Column\s*\([^)]*\)/g;
    for (const m of src.matchAll(columnRe)) {
      sigs.push(`column ${m[1]}`);
    }

    // Relationships (SQLAlchemy ForeignKey, relationship)
    const relRe = /(?:ForeignKey|relationship)\s*\(\s*['"]([a-zA-Z_]\w*)['"]/g;
    for (const m of src.matchAll(relRe)) {
      sigs.push(`relation ${m[1]}`);
    }

    // Validators (@validator, @field_validator)
    const validatorRe = /@(?:validator|field_validator)\s*\(\s*['"]?([a-z_]\w*)(?:['"]|,|\s|\))/g;
    const validators = new Set();
    for (const m of src.matchAll(validatorRe)) {
      validators.add(m[1]);
    }
    for (const v of validators) {
      sigs.push(`validator ${v}`);
    }

    // Config class (Pydantic v1 / SQLAlchemy)
    if (/class\s+Config\s*:/.test(src)) {
      sigs.push('config-class');
    }

    return Array.from(new Set(sigs)).slice(0, 50);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/r ──
__factories["./src/extractors/r"] = function(module, exports) {
  
  /**
   * Extract signatures from R source code.
   *
   * Recognised constructs:
   *   - Function definitions: `name <- function(args)`, `name = function(args)`,
   *     `name <<- function(args)`
   *   - S4: setClass / setGeneric / setMethod
   *   - R6: `Name <- R6Class("Name", public = list(method = function(...)))`
   *   - S7: `Name <- new_class("Name", ...)` and `method(generic, Name) <- function(...)`
   *   - roxygen2 docstring hint (first non-tag `#'` line) appended as `  # hint`
   *
   * The extractor stays regex-only and zero-dependency. Output uses two-space
   * indentation for class members (matching python/typescript/scala fixtures).
   *
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Collect roxygen2 hints from the original source (before stripping `#`).
    const docHints = collectRoxygenHints(src);

    // Strip line comments for the rest of the parsing. R uses `#` comments and
    // roxygen2 `#'` is consumed alongside them — its content already lives in
    // docHints.
    const stripped = src.replace(/#.*$/gm, '');

    // Track byte ranges already accounted for by R6 / setClass blocks so the
    // top-level function regex doesn't re-emit their methods as bare functions.
    const consumedRanges = [];

    // ── R6 classes ────────────────────────────────────────────────────────────
    //   ClassName <- R6Class("ClassName", public = list(method = function(...)))
    //   ClassName <- R6::R6Class(...)
    const r6Re = /([\w.]+)\s*(?:<<-|<-|=)\s*(?:R6::)?R6Class\s*\(/g;
    let m;
    while ((m = r6Re.exec(stripped)) !== null && sigs.length < 30) {
      const name = m[1];
      if (name.startsWith('.')) continue;
      const openIdx = r6Re.lastIndex - 1;
      const body = readBalancedParens(stripped, openIdx);
      if (body === null) continue;
      const closeIdx = openIdx + body.length + 1;
      const classNameLit = readFirstStringArg(body) || name;
      sigs.push(`${name} <- R6Class("${classNameLit}")` + applyHint(docHints, name));
      for (const memberSig of extractListMethods(body, 8)) {
        sigs.push('  ' + memberSig);
        if (sigs.length >= 30) break;
      }
      consumedRanges.push([m.index, closeIdx]);
      r6Re.lastIndex = closeIdx;
    }

    // ── S7 classes ────────────────────────────────────────────────────────────
    //   ClassName <- new_class("ClassName", properties = list(...))
    const s7Classes = new Set();
    const s7Re = /([\w.]+)\s*(?:<<-|<-|=)\s*(?:S7::)?new_class\s*\(/g;
    while ((m = s7Re.exec(stripped)) !== null && sigs.length < 30) {
      const name = m[1];
      if (name.startsWith('.')) continue;
      const openIdx = s7Re.lastIndex - 1;
      const body = readBalancedParens(stripped, openIdx);
      if (body === null) continue;
      const closeIdx = openIdx + body.length + 1;
      const classNameLit = readFirstStringArg(body) || name;
      s7Classes.add(classNameLit);
      s7Classes.add(name);
      sigs.push(`${name} <- new_class("${classNameLit}")` + applyHint(docHints, name));
      consumedRanges.push([m.index, closeIdx]);
      s7Re.lastIndex = closeIdx;
    }

    // S7 method dispatch: `method(generic, ClassName) <- function(args)`
    const s7MethodRe = /^[ \t]*method\s*\(\s*([\w.]+)\s*,\s*([\w.]+)\s*\)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
    while ((m = s7MethodRe.exec(stripped)) !== null && sigs.length < 30) {
      if (!s7Classes.has(m[2])) continue;
      const argsStart = s7MethodRe.lastIndex - 1;
      const args = readBalancedParens(stripped, argsStart);
      if (args === null) continue;
      sigs.push(`  method(${m[1]}, ${m[2]}) <- function(${normalizeParams(args)})`);
    }

    // ── Top-level function definitions ────────────────────────────────────────
    //   name <- function(args), name = function(args), name <<- function(args)
    // Skip matches whose position falls inside an R6/S7 class body — those have
    // already been emitted as indented members.
    const funcRe = /^(?:[ \t]*)([\w.]+)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
    while ((m = funcRe.exec(stripped)) !== null && sigs.length < 30) {
      const name = m[1];
      if (name.startsWith('.')) continue;
      if (inAnyRange(m.index, consumedRanges)) continue;
      const argsStart = funcRe.lastIndex - 1;
      const args = readBalancedParens(stripped, argsStart);
      if (args === null) continue;
      sigs.push(`${name} <- function(${normalizeParams(args)})` + applyHint(docHints, name));
    }

    // ── S4 ────────────────────────────────────────────────────────────────────
    for (const sm of stripped.matchAll(/^[ \t]*setGeneric\s*\(\s*["']([\w.]+)["']/gm)) {
      if (sigs.length >= 30) break;
      sigs.push(`setGeneric("${sm[1]}")`);
    }
    for (const sm of stripped.matchAll(/^[ \t]*setMethod\s*\(\s*["']([\w.]+)["']\s*,\s*["']([\w.]+)["']/gm)) {
      if (sigs.length >= 30) break;
      sigs.push(`setMethod("${sm[1]}", "${sm[2]}")`);
    }
    for (const sm of stripped.matchAll(/^[ \t]*setClass\s*\(\s*["']([\w.]+)["']/gm)) {
      if (sigs.length >= 30) break;
      sigs.push(`setClass("${sm[1]}")`);
    }

    return sigs.slice(0, 30);
  }

  /**
   * Collect roxygen2 docstring hints from the original (uncommented) source.
   * Returns Map<symbolName, hint> where hint is the first @title line, else
   * @description, else the first non-tag content line. Trimmed to 60 chars,
   * trailing punctuation removed.
   */
  function collectRoxygenHints(src) {
    const hints = new Map();
    const lines = src.split('\n');
    let block = [];
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      if (/^\s*#'/.test(line)) {
        block.push(line.replace(/^\s*#'\s?/, ''));
        continue;
      }
      if (block.length > 0) {
        const m = line.match(/^[ \t]*([\w.]+)\s*(?:<<-|<-|=)\s*(?:R6::)?R6Class\b/)
               || line.match(/^[ \t]*([\w.]+)\s*(?:<<-|<-|=)\s*(?:S7::)?new_class\b/)
               || line.match(/^[ \t]*([\w.]+)\s*(?:<<-|<-|=)\s*function\b/);
        if (m) {
          const name = m[1];
          let hint = pickRoxygenLine(block, '@title')
                  || pickRoxygenLine(block, '@description')
                  || pickRoxygenLine(block, null);
          if (hint) {
            hint = hint.replace(/\s+/g, ' ').trim().slice(0, 60).replace(/[.,;:!?]+$/, '').trim();
            if (hint) hints.set(name, hint);
          }
        }
        block = [];
      }
    }
    return hints;
  }

  function pickRoxygenLine(block, tag) {
    for (const raw of block) {
      const b = raw.trim();
      if (!b) continue;
      if (tag) {
        if (b.startsWith(tag)) {
          const rest = b.slice(tag.length).trim();
          if (rest) return rest;
        }
      } else if (!b.startsWith('@')) {
        return b;
      }
    }
    return null;
  }

  function applyHint(hints, name) {
    const h = hints.get(name);
    return h ? `  # ${h}` : '';
  }

  /**
   * Extract method-like entries from the body of an R6/S7 list(...) argument.
   * Matches `name = function(args)` at any indentation. Caps at `cap` entries.
   */
  function extractListMethods(body, cap) {
    const out = [];
    const re = /(?:^|[\n,])\s*([\w.]+)\s*=\s*function\s*\(/g;
    let m;
    while ((m = re.exec(body)) !== null && out.length < cap) {
      const name = m[1];
      if (name.startsWith('.')) continue;
      const argsStart = re.lastIndex - 1;
      const args = readBalancedParens(body, argsStart);
      if (args === null) continue;
      out.push(`${name} <- function(${normalizeParams(args)})`);
    }
    return out;
  }

  function inAnyRange(pos, ranges) {
    for (const [s, e] of ranges) {
      if (pos >= s && pos < e) return true;
    }
    return false;
  }

  /** Extract the first quoted string from a comma-separated argument body. */
  function readFirstStringArg(body) {
    const m = body.match(/^\s*["']([\w.]+)["']/);
    return m ? m[1] : null;
  }

  /**
   * Read a parenthesis-balanced substring starting at the position of the
   * opening '(' character, returning the inner content (without the outer
   * parens). Returns null if no matching close paren is found within `cap`
   * characters, which guards against runaway scans on malformed input.
   */
  function readBalancedParens(src, openIdx, cap = 16384) {
    if (src[openIdx] !== '(') return null;
    let depth = 1;
    let i = openIdx + 1;
    const end = Math.min(src.length, openIdx + cap);
    let inString = null;
    while (i < end) {
      const ch = src[i];
      if (inString) {
        if (ch === '\\') { i += 2; continue; }
        if (ch === inString) inString = null;
        i++;
        continue;
      }
      if (ch === '"' || ch === "'") { inString = ch; i++; continue; }
      if (ch === '(') depth++;
      else if (ch === ')') {
        depth--;
        if (depth === 0) return src.slice(openIdx + 1, i);
      }
      i++;
    }
    return null;
  }

  /**
   * Compress whitespace inside a parameter list, collapse multi-line default
   * expressions onto a single line, and trim. String literals are protected so
   * that commas/equals inside default values like `sep = ","` don't get respaced.
   */
  function normalizeParams(raw) {
    let out = '';
    let inString = null;
    for (let i = 0; i < raw.length; i++) {
      const ch = raw[i];
      if (inString) {
        out += ch;
        if (ch === '\\' && i + 1 < raw.length) { out += raw[i + 1]; i++; continue; }
        if (ch === inString) inString = null;
        continue;
      }
      if (ch === '"' || ch === "'") { inString = ch; out += ch; continue; }
      if (/\s/.test(ch)) {
        if (out.length && !/\s$/.test(out)) out += ' ';
        continue;
      }
      if (ch === ',') {
        out = out.replace(/\s+$/, '') + ', ';
        continue;
      }
      if (ch === '=') {
        out = out.replace(/\s+$/, '') + ' = ';
        continue;
      }
      out += ch;
    }
    return out.trim();
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/ruby ──
__factories["./src/extractors/ruby"] = function(module, exports) {
  
  /**
   * Extract signatures from Ruby source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src.replace(/#.*$/gm, '');

    // Modules and classes
    for (const m of stripped.matchAll(/^(?:module|class)\s+([\w:]+)(?:\s*<\s*[\w:]+)?\s*$/gm)) {
      const kind = m[0].trimStart().startsWith('module') ? 'module' : 'class';
      sigs.push(`${kind} ${m[1]}`);
    }

    // Public methods (not private/protected)
    for (const m of stripped.matchAll(/^\s+def\s+(?:self\.)?(\w+)(?:\s*\(([^)]*)\))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const params = m[2] ? `(${normalizeParams(m[2])})` : '';
      const selfPrefix = m[0].includes('self.') ? 'self.' : '';
      const retStr = extractReturnHint(stripped, m.index);
      sigs.push(`  def ${selfPrefix}${m[1]}${params}${retStr}`);
    }

    // Top-level def
    for (const m of stripped.matchAll(/^def\s+(\w+)(?:\s*\(([^)]*)\))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const params = m[2] ? `(${normalizeParams(m[2])})` : '';
      const retStr = extractReturnHint(stripped, m.index);
      sigs.push(`def ${m[1]}${params}${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function extractReturnHint(stripped, index) {
    const start = Math.max(0, index - 180);
    const before = stripped.slice(start, index);
    const m = before.match(/sig\s*\{[\s\S]*?returns\(([^)]+)\)[\s\S]*?\}\s*$/);
    if (!m) return '';
    const type = m[1].trim().replace(/\s+/g, ' ').slice(0, 25);
    return type ? ` → ${type}` : '';
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/rust ──
__factories["./src/extractors/rust"] = function(module, exports) {
  
  /**
   * Extract signatures from Rust source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Structs
    for (const m of stripped.matchAll(/^pub\s+struct\s+(\w+)(?:<[^{]*>)?/gm)) {
      sigs.push(`pub struct ${m[1]}`);
    }

    // Enums
    for (const m of stripped.matchAll(/^pub\s+enum\s+(\w+)(?:<[^{]*>)?/gm)) {
      sigs.push(`pub enum ${m[1]}`);
    }

    // Traits
    for (const m of stripped.matchAll(/^pub\s+trait\s+(\w+)(?:<[^{]*>)?/gm)) {
      sigs.push(`pub trait ${m[1]}`);
    }

    // impl blocks
    for (const m of stripped.matchAll(/^impl(?:<[^>]*>)?\s+(?:[\w:]+\s+for\s+)?(\w+)(?:<[^{]*>)?\s*\{/gm)) {
      sigs.push(`impl ${m[1]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const fn of extractMethods(block)) sigs.push(`  ${fn}`);
    }

    // Top-level pub fns — capture everything after ) up to { or ; for return type
    for (const m of stripped.matchAll(/^pub(?:\s+async)?\s+fn\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)([^{;]*)/gm)) {
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = extractReturnType(m[3]);
      sigs.push(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 5000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMethods(block) {
    const methods = [];
    for (const m of block.matchAll(/^\s+pub(?:\s+async)?\s+fn\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)([^{;]*)/gm)) {
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = extractReturnType(m[3]);
      methods.push(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }
    return methods.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function extractReturnType(afterParen) {
    if (!afterParen) return '';
    const m = afterParen.match(/->\s*([^{;]+)/);
    if (!m) return '';
    const rt = m[1].trim().replace(/\s+/g, ' ');
    return ` \u2192 ${rt.length > 30 ? rt.slice(0, 27) + '...' : rt}`;
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/scala ──
__factories["./src/extractors/scala"] = function(module, exports) {
  
  /**
   * Extract signatures from Scala source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes, traits, objects
    const typeRe = /^(?:case\s+)?(?:class|trait|object)\s+(\w+)(?:\[[\w, ]+\])?(?:[^{]*)\{/gm;
    for (const m of stripped.matchAll(typeRe)) {
      const kind = m[0].trimStart().startsWith('case class') ? 'case class' :
        m[0].trimStart().startsWith('trait') ? 'trait' :
        m[0].trimStart().startsWith('object') ? 'object' : 'class';
      sigs.push(`${kind} ${m[1]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const fn of extractMembers(block)) sigs.push(`  ${fn}`);
    }

    // Top-level defs
    for (const m of stripped.matchAll(/^def\s+(\w+)(?:\[[\w, ]+\])?\s*(?:\(([^)]*)\))?(?:\s*:\s*([^=\n{]+))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const params = m[2] ? `(${normalizeParams(m[2])})` : '';
      const ret = normalizeType(m[3]);
      const retStr = ret ? ` → ${ret}` : '';
      sigs.push(`def ${m[1]}${params}${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    for (const m of block.matchAll(/^\s+def\s+(\w+)(?:\[[\w, ]+\])?\s*(?:\(([^)]*)\))?(?:\s*:\s*([^=\n{]+))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const params = m[2] ? `(${normalizeParams(m[2])})` : '';
      const ret = normalizeType(m[3]);
      const retStr = ret ? ` → ${ret}` : '';
      members.push(`def ${m[1]}${params}${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim()
      .split(',')
      .map((p) => p.trim().split(':')[0].trim())
      .filter(Boolean)
      .join(', ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/\s+/g, ' ').slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/shell ──
__factories["./src/extractors/shell"] = function(module, exports) {
  
  /**
   * Extract signatures from shell scripts (bash, zsh, fish).
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src.replace(/#.*$/gm, '');

    // Function definitions (bash: name() { and function name {)
    for (const m of stripped.matchAll(/^(?:function\s+)?([\w:-]+)\s*\(\s*\)\s*\{/gm)) {
      if (m[1].startsWith('_')) continue;
      sigs.push(`function ${m[1]}()`);
    }

    // Main entry point patterns
    if (/^\s*main\s*\(/m.test(src) || /^\s*main\s+"?\$@/m.test(src)) {
      sigs.push('main "$@"');
    }

    // Exported variables
    for (const m of stripped.matchAll(/^export\s+([\w]+)=/gm)) {
      sigs.push(`export ${m[1]}`);
    }

    // Script description (first non-comment line after shebang)
    const lines = src.split('\n');
    for (const line of lines.slice(0, 5)) {
      if (line.startsWith('#!')) continue;
      if (line.startsWith('#')) {
        const desc = line.replace(/^#+\s*/, '').trim();
        if (desc) { sigs.unshift(`# ${desc}`); break; }
      }
    }

    return sigs.slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/sql ──
__factories["./src/extractors/sql"] = function(module, exports) {
  
  /**
   * Extract signatures from SQL source files.
   * Captures CREATE TABLE, VIEW, INDEX, FUNCTION, PROCEDURE, TRIGGER, TYPE, SEQUENCE.
   *
   * @param {string} src - Raw SQL content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Strip single-line comments and block comments
    const stripped = src
      .replace(/--[^\n]*/g, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // CREATE TABLE [IF NOT EXISTS] <name> / CREATE [TEMP] TABLE ...
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)/gi
    )) {
      sigs.push(`TABLE ${_cleanName(m[1])}`);
    }

    // CREATE VIEW / MATERIALIZED VIEW
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)/gi
    )) {
      sigs.push(`VIEW ${_cleanName(m[1])}`);
    }

    // CREATE INDEX / UNIQUE INDEX
    for (const m of stripped.matchAll(
      /CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)\s+ON\s+([`"[\w.]+)/gi
    )) {
      sigs.push(`INDEX ${_cleanName(m[1])} ON ${_cleanName(m[2])}`);
    }

    // CREATE FUNCTION / CREATE OR REPLACE FUNCTION
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+([`"[\w.]+)\s*\(([^)]*)\)/gi
    )) {
      const params = _normalizeParams(m[2]);
      sigs.push(`FUNCTION ${_cleanName(m[1])}(${params})`);
    }

    // CREATE PROCEDURE
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?PROCEDURE\s+([`"[\w.]+)\s*\(([^)]*)\)/gi
    )) {
      const params = _normalizeParams(m[2]);
      sigs.push(`PROCEDURE ${_cleanName(m[1])}(${params})`);
    }

    // CREATE TRIGGER
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?(?:CONSTRAINT\s+)?TRIGGER\s+([`"[\w.]+)/gi
    )) {
      sigs.push(`TRIGGER ${_cleanName(m[1])}`);
    }

    // CREATE TYPE (composite, enum, domain)
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?TYPE\s+([`"[\w.]+)/gi
    )) {
      sigs.push(`TYPE ${_cleanName(m[1])}`);
    }

    // CREATE SEQUENCE
    for (const m of stripped.matchAll(
      /CREATE\s+(?:OR\s+REPLACE\s+)?SEQUENCE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)/gi
    )) {
      sigs.push(`SEQUENCE ${_cleanName(m[1])}`);
    }

    return sigs;
  }

  function _cleanName(raw) {
    return raw.replace(/^[`"[]|[`"\]]+$/g, '').trim();
  }

  function _normalizeParams(raw) {
    if (!raw || !raw.trim()) return '';
    return raw.trim()
      .split(',')
      .map((p) => p.trim().replace(/\s+/g, ' ').split(' ').slice(0, 2).join(' '))
      .filter(Boolean)
      .join(', ');
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/svelte ──
__factories["./src/extractors/svelte"] = function(module, exports) {
  
  /**
   * Extract signatures from Svelte components.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Extract <script> block
    const scriptMatch = src.match(/<script(?:\s[^>]*)?>(?:\s*)([\s\S]*?)<\/script>/i);
    if (!scriptMatch) return sigs;

    const script = scriptMatch[1]
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Exported props (writable)
    for (const m of script.matchAll(/^\s+export\s+let\s+(\w+)(?:\s*=\s*[^;]+)?;/gm)) {
      sigs.push(`export let ${m[1]}`);
    }

    // Exported functions
    for (const m of script.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{=\n]+))?/gm)) {
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = m[3] ? ` → ${normalizeType(m[3])}` : '';
      sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    // Top-level functions
    for (const m of script.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{=\n]+))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const asyncKw = m[0].startsWith('async') ? 'async ' : '';
      const retStr = m[3] ? ` → ${normalizeType(m[3])}` : '';
      sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    // Reactive declarations $:
    for (const m of script.matchAll(/^\s+\$:\s+(\w+)\s*=/gm)) {
      sigs.push(`$: ${m[1]}`);
    }

    return sigs.slice(0, 25);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/[;\s]+$/g, '').replace(/\s+/g, ' ').slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/swift ──
__factories["./src/extractors/swift"] = function(module, exports) {
  
  /**
   * Extract signatures from Swift source code.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Classes, structs, protocols, enums
    const typeRe = /^(?:public\s+|internal\s+|open\s+)?(?:final\s+)?(class|struct|protocol|enum|actor)\s+(\w+)(?:<[^{]*>)?(?:\s*:\s*[\w, <>.]+)?\s*\{/gm;
    for (const m of stripped.matchAll(typeRe)) {
      sigs.push(`${m[1]} ${m[2]}`);
      const block = extractBlock(stripped, m.index + m[0].length);
      for (const fn of extractMembers(block)) sigs.push(`  ${fn}`);
    }

    // Top-level public functions — capture everything after ) to end of line for arrow type
    for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:static\s+)?(?:async\s+)?func\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)([^{\n]*)/gm)) {
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = extractArrowType(m[3]);
      sigs.push(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    return sigs.slice(0, 25);
  }

  function extractBlock(src, startIndex) {
    let depth = 1, i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  function extractMembers(block) {
    const members = [];
    for (const m of block.matchAll(/^\s+(?:public\s+|internal\s+|open\s+)?(?:static\s+|class\s+)?(?:mutating\s+)?(?:async\s+)?func\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)([^{\n]*)/gm)) {
      if (m[1].startsWith('_')) continue;
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = extractArrowType(m[3]);
      members.push(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim()
      .split(',')
      .map((p) => p.trim().split(':')[0].trim())
      .filter(Boolean)
      .join(', ');
  }

  function extractArrowType(str) {
    if (!str) return '';
    const m = str.match(/->\s*([^\n{]+)/);
    if (!m) return '';
    const rt = m[1].trim().replace(/\s+/g, ' ');
    return ` \u2192 ${rt.length > 25 ? rt.slice(0, 22) + '...' : rt}`;
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/terraform ──
__factories["./src/extractors/terraform"] = function(module, exports) {
  
  /**
   * Extract signatures from Terraform (.tf / .tfvars) configuration files.
   * Captures resource, data, module, variable, output, locals, provider,
   * terraform blocks, and moved/import blocks.
   *
   * @param {string} src - Raw Terraform content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Strip single-line comments
    const stripped = src
      .replace(/\/\/[^\n]*/g, '')
      .replace(/#[^\n]*/g, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // resource "<type>" "<name>" { ... }
    for (const m of stripped.matchAll(/\bresource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g)) {
      sigs.push(`resource "${m[1]}" "${m[2]}"`);
    }

    // data "<type>" "<name>" { ... }
    for (const m of stripped.matchAll(/\bdata\s+"([^"]+)"\s+"([^"]+)"\s*\{/g)) {
      sigs.push(`data "${m[1]}" "${m[2]}"`);
    }

    // module "<name>" { ... }
    for (const m of stripped.matchAll(/\bmodule\s+"([^"]+)"\s*\{/g)) {
      sigs.push(`module "${m[1]}"`);
    }

    // variable "<name>" { ... }
    for (const m of stripped.matchAll(/\bvariable\s+"([^"]+)"\s*\{/g)) {
      sigs.push(`variable "${m[1]}"`);
    }

    // output "<name>" { ... }
    for (const m of stripped.matchAll(/\boutput\s+"([^"]+)"\s*\{/g)) {
      sigs.push(`output "${m[1]}"`);
    }

    // provider "<name>" { ... }
    for (const m of stripped.matchAll(/\bprovider\s+"([^"]+)"\s*\{/g)) {
      sigs.push(`provider "${m[1]}"`);
    }

    // locals { ... } (just mark presence; key names too noisy to enumerate)
    if (/\blocals\s*\{/.test(stripped)) {
      sigs.push('locals { ... }');
    }

    // terraform { required_providers / backend }
    if (/\bterraform\s*\{/.test(stripped)) {
      sigs.push('terraform { ... }');
    }

    // moved block
    for (const m of stripped.matchAll(/\bmoved\s*\{[\s\S]*?from\s*=\s*([^\n]+)/g)) {
      sigs.push(`moved from ${m[1].trim()}`);
    }

    // import block (Terraform 1.5+)
    for (const m of stripped.matchAll(/\bimport\s*\{[\s\S]*?to\s*=\s*([^\n]+)/g)) {
      sigs.push(`import to ${m[1].trim()}`);
    }

    return sigs;
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/todos ──
__factories["./src/extractors/todos"] = function(module, exports) {
  
  /**
   * Extract TODO/FIXME/HACK/XXX comments from source text.
   * @param {string} src - Raw file content
   * @returns {{line:number, tag:string, text:string}[]}
   */
  function extractTodos(src) {
    if (!src || typeof src !== 'string') return [];
    const todos = [];
    const lines = src.split('\n');

    for (let i = 0; i < lines.length; i++) {
      const m = lines[i].match(/(?:\/\/|#)\s*(TODO|FIXME|HACK|XXX)\s*:?\s*(.+)/i);
      if (!m) continue;
      todos.push({
        line: i + 1,
        tag: m[1].toUpperCase(),
        text: m[2].trim().slice(0, 70),
      });
    }

    return todos;
  }

  module.exports = { extractTodos };
  
};

// ── ./src/extractors/toml ──
__factories["./src/extractors/toml"] = function(module, exports) {
  
  /**
   * Extract signatures from TOML configuration files.
   * Focuses on section/table names and high-value keys.
   *
   * @param {string} src - Raw TOML content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Remove # comments while preserving values before comment markers.
    const stripped = src.replace(/\s+#.*$/gm, '');

    // [section] and [[array.section]]
    for (const m of stripped.matchAll(/^\s*(\[\[?[^\]]+\]\]?)\s*$/gm)) {
      sigs.push(`table ${m[1].trim()}`);
    }

    // Key-value lines (top-level and nested) — keep key names only.
    for (const m of stripped.matchAll(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(.+)$/gm)) {
      const key = m[1].trim();
      const value = m[2].trim();

      // Prefer common metadata/config keys for compact, useful output.
      if (/^(name|version|description|authors|license|requires-python|dependencies|optional-dependencies|scripts|tool\.|build-system\.|project\.)/.test(key)) {
        sigs.push(`key ${key}`);
        continue;
      }

      // Include booleans and simple scalar keys as generic config signal.
      if (/^(true|false|"[^"]*"|'[^']*'|[0-9._-]+)$/.test(value)) {
        sigs.push(`key ${key}`);
      }
    }

    return Array.from(new Set(sigs)).slice(0, 40);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/typescript ──
__factories["./src/extractors/typescript"] = function(module, exports) {
  
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');

  /**
   * Extract signatures from TypeScript source code.
   * Top-level declarations carry a `:start-end` line anchor (see line-anchor.js);
   * indented members do not.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];
    // anchors[i] is [start, end] for a top-level sig, or null for an indented member.
    // Kept parallel to `sigs` so existing push/mutation logic stays untouched;
    // anchors are applied once at return.
    const anchors = [];

    // Strip comments to simplify matching. Block comments are blanked
    // newline-by-newline (non-newline chars → spaces) so character offsets AND
    // line numbers stay exact. Line comments preserve their trailing newline.
    const stripped = src
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));

    // Index of the closing brace for a block whose body starts at bodyStart.
    const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;

    // Exported interfaces
    for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
      const bodyStart = m.index + m[0].length;
      sigs.push(`export interface ${m[1]}`);
      anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
      // Collect members
      const block = extractBlock(stripped, bodyStart);
      const members = extractInterfaceMembers(block);
      for (const mem of members) {
        sigs.push(`  ${mem.text}`);
        anchors.push([lineAt(stripped, bodyStart + mem.start), lineAt(stripped, bodyStart + mem.end)]);
      }
    }

    // Exported type aliases
    for (const m of stripped.matchAll(/^export\s+type\s+(\w+)(?:<[^=]*>)?\s*=/gm)) {
      sigs.push(`export type ${m[1]}`);
      anchors.push([lineAt(stripped, m.index), lineAt(stripped, m.index + m[0].length)]);
    }

    // Exported enums
    for (const m of stripped.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)\s*\{/gm)) {
      const bodyStart = m.index + m[0].length;
      sigs.push(`export enum ${m[1]}`);
      anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
    }

    // Classes (exported and internal)
    const classRegex = /^(export\s+)?(abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<> ,]+)?\s*\{/gm;
    for (const m of stripped.matchAll(classRegex)) {
      const prefix = m[1] ? 'export ' : '';
      const abs = m[2] ? 'abstract ' : '';
      const bodyStart = m.index + m[0].length;
      sigs.push(`${prefix}${abs}class ${m[3]}`);
      anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
      const block = extractBlock(stripped, bodyStart);
      const methods = extractClassMembers(block);
      for (const meth of methods) {
        sigs.push(`  ${meth.text}`);
        anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
      }
    }

    // Exported top-level functions (not methods)
    for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
      const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
      const params = normalizeParams(m[2]);
      const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
      const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
      const retStr = retType ? ` → ${retType}` : '';
      const bodyStart = m.index + m[0].length;
      sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
      anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);

      // Hooks: capture compact return object shape for use* functions.
      if (m[1].startsWith('use')) {
        const body = stripped.slice(bodyStart, bodyStart + 800);
        const ret = body.match(/return\s*\{([^}]{1,260})\}/);
        if (ret) {
          const keys = ret[1]
            .split(',')
            .map((s) => s.trim().split(':')[0].split('(')[0].trim())
            .filter(Boolean)
            .slice(0, 8);
          if (keys.length) {
            sigs[sigs.length - 1] += ` → { ${keys.join(', ')} }`;
          }
        }
      }
    }

    // Exported arrow functions / const functions
    for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
      const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
      const params = normalizeParams(m[2]);
      sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
      const bodyStart = stripped.indexOf('{', m.index + m[0].length);
      const endLn = bodyStart !== -1
        ? lineAt(stripped, blockEndIdx(bodyStart + 1))
        : lineAt(stripped, m.index + m[0].length);
      anchors.push([lineAt(stripped, m.index), endLn]);

      // Hooks: capture compact return object shape for use* functions.
      if (m[1].startsWith('use')) {
        if (bodyStart !== -1) {
          const body = stripped.slice(bodyStart, bodyStart + 800);
          const ret = body.match(/return\s*\{([^}]{1,260})\}/);
          if (ret) {
            const keys = ret[1]
              .split(',')
              .map((s) => s.trim().split(':')[0].split('(')[0].trim())
              .filter(Boolean)
              .slice(0, 8);
            if (keys.length) {
              sigs[sigs.length - 1] += ` → { ${keys.join(', ')} }`;
            }
          }
        }
      }
    }

    // Zustand stores: export const useXxxStore = create<State>()(...)
    for (const m of stripped.matchAll(/^export\s+const\s+(use\w+Store)\s*=\s*create(?:<[^>]*>)?\s*\(/gm)) {
      const stateType = m[0].match(/create<([\w]+)>/)?.[1] || '';
      const startLn = lineAt(stripped, m.index);
      sigs.push(`export const ${m[1]} = create<${stateType}>(...)`);
      anchors.push([startLn, startLn]);
      const ifaceRe = new RegExp(`interface\\s+${stateType}\\s*\\{([\\s\\S]*?)\\}`);
      const ifm = stripped.match(ifaceRe);
      if (ifm) {
        for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) { sigs.push(`  ${fm[1]}`); anchors.push(null); }
      }
    }

    // API client objects: const xxxApi = { method: async () => {} }
    for (const m of stripped.matchAll(/^(?:export\s+default\s+|const\s+)(\w*[Aa]pi\w*)\s*=\s*\{/gm)) {
      const bodyStart = m.index + m[0].length;
      const block = extractBlock(stripped, bodyStart);
      const methods = [...block.matchAll(/^\s+(\w+)\s*:\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/gm)].map(mm => mm[1]);
      if (methods.length) {
        sigs.push(`${m[1]}: { ${methods.join(', ')} }`);
        anchors.push([lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)]);
      }
    }

    return sigs
      .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
      .slice(0, 35);
  }

  function extractBlock(src, startIndex) {
    let depth = 1;
    let i = startIndex;
    const end = Math.min(src.length, startIndex + 4000);
    while (i < end && depth > 0) {
      if (src[i] === '{') depth++;
      else if (src[i] === '}') depth--;
      i++;
    }
    return src.slice(startIndex, i - 1);
  }

  // Returns members as { text, start, end } where start/end are char offsets
  // WITHIN `block`, so the caller can resolve member line anchors.
  function extractInterfaceMembers(block) {
    const members = [];
    for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
      const readonly = m[1] ? 'readonly ' : '';
      const optional = m[3] ? '?' : '';
      const typeStr = m[4].trim().replace(/\s+/g, ' ').slice(0, 35);
      const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
      members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
    }
    for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
      const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
      members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
    }
    return members.slice(0, 8);
  }

  const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);

  // Returns members as { text, start, end } where start/end are char offsets
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
  // per-method line anchors that span the method body.
  function extractClassMembers(block) {
    const members = [];
    // Public methods (skip private/protected/_ prefixed and control-flow keywords)
    const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
    for (const m of block.matchAll(methodRe)) {
      if (_CTRL_KEYWORDS.has(m[1])) continue;
      if (/^(private|protected|_)/.test(m[1])) continue;
      const bodyStart = m.index + m[0].length; // just past the opening brace
      const end = bodyStart + extractBlock(block, bodyStart).length;
      const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
      if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
      const isAsync = m[0].includes('async ') ? 'async ' : '';
      const isStatic = m[0].includes('static ') ? 'static ' : '';
      const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
      const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
      const retStr = retType ? ` → ${retType}` : '';
      members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
    }
    return members.slice(0, 8);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/typescript_react ──
__factories["./src/extractors/typescript_react"] = function(module, exports) {
  
  /**
   * Extract React component signatures from .tsx files.
   * Captures component props interfaces, hooks usage, and exports.
   *
   * @param {string} src - Raw TypeScript/TSX content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Remove comments to simplify matching
    const stripped = src
      .replace(/\/\*[\s\S]*?\*\//g, '')
      .replace(/\/\/.*$/gm, '');

    // Component function declarations
    const compRe = /(?:export\s+)?(?:const|function)\s+([A-Z]\w*)\s*(?:<[^>]*>)?\s*\(\s*(?:props|{\s*[^}]*})?/g;
    for (const m of stripped.matchAll(compRe)) {
      sigs.push(`component ${m[1]}`);
    }

    // Props interfaces: interface SomeProps { ... }
    const propsRe = /interface\s+(\w*Props)\s*(?:<[^>]*>)?\s*{/g;
    for (const m of stripped.matchAll(propsRe)) {
      sigs.push(`props ${m[1]}`);
    }

    // React hooks (useState, useEffect, useContext, useCallback, useMemo, useReducer)
    const hookRe = /use([A-Z]\w*)\s*(?:<[^>]*>)?\s*\(/g;
    const hooks = new Set();
    for (const m of stripped.matchAll(hookRe)) {
      hooks.add(m[1]);
    }
    for (const h of hooks) {
      sigs.push(`hook use${h}`);
    }

    // Import/export statements for components
    const exportRe = /export\s+(?:const|function|default|interface|type)\s+([A-Z]\w*)/g;
    for (const m of stripped.matchAll(exportRe)) {
      sigs.push(`export ${m[1]}`);
    }

    // Event handler patterns: onClick, onChange, onSubmit, etc
    const handlerRe = /on([A-Z]\w+)\s*=\s*{?\s*\(?[a-zA-Z_$]/g;
    const handlers = new Set();
    for (const m of stripped.matchAll(handlerRe)) {
      handlers.add(m[1]);
    }
    for (const h of handlers) {
      sigs.push(`handler on${h}`);
    }

    return Array.from(new Set(sigs)).slice(0, 50);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/vue ──
__factories["./src/extractors/vue"] = function(module, exports) {
  
  /**
   * Extract signatures from Vue single-file components.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Extract component name from filename hint if present or defineComponent
    const nameMatch = src.match(/name\s*:\s*['"](\w+)['"]/);
    if (nameMatch) sigs.push(`component ${nameMatch[1]}`);

    // Extract <script> block
    const scriptMatch = src.match(/<script(?:\s[^>]*)?>(?:\s*)([\s\S]*?)<\/script>/i);
    if (!scriptMatch) return sigs;

    const script = scriptMatch[1]
      .replace(/\/\/.*$/gm, '')
      .replace(/\/\*[\s\S]*?\*\//g, '');

    // Props
    const propsMatch = script.match(/props\s*:\s*(\{[\s\S]*?\})/);
    if (propsMatch) {
      const propNames = [];
      for (const m of propsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
        propNames.push(m[1]);
      }
      if (propNames.length > 0) sigs.push(`props: [${propNames.join(', ')}]`);
    }

    // Methods in options API
    const methodsMatch = script.match(/methods\s*:\s*\{([\s\S]*?)\},?\s*(?:computed|watch|mounted|created|data|\})/);
    if (methodsMatch) {
      for (const m of methodsMatch[1].matchAll(/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{=\n]+))?/gm)) {
        if (m[1].startsWith('_')) continue;
        const asyncKw = m[0].includes('async') ? 'async ' : '';
        const retStr = m[3] ? ` → ${normalizeType(m[3])}` : '';
        sigs.push(`  ${asyncKw}${m[1]}(${normalizeParams(m[2])})${retStr}`);
      }
    }

    // Top-level functions in <script> (e.g., composition API helpers)
    for (const m of script.matchAll(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^{=\n]+))?/gm)) {
      if (m[1].startsWith('_')) continue;
      const asyncKw = m[0].includes('async') ? 'async ' : '';
      const retStr = m[3] ? ` → ${normalizeType(m[3])}` : '';
      sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
    }

    // defineProps (Composition API)
    const definePropsMatch = script.match(/defineProps(?:<[^>]*>)?\s*\(\s*(\{[\s\S]*?\})\s*\)/);
    if (definePropsMatch) {
      const propNames = [];
      for (const m of definePropsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
        propNames.push(m[1]);
      }
      if (propNames.length > 0) sigs.push(`defineProps: [${propNames.join(', ')}]`);
    }

    // Emits
    const emitsMatch = script.match(/(?:defineEmits|emits)\s*(?::\s*|\(\s*)(\[[\s\S]*?\])/);
    if (emitsMatch) sigs.push(`emits: ${emitsMatch[1].replace(/\s+/g, ' ')}`);

    return sigs.slice(0, 25);
  }

  function normalizeParams(params) {
    if (!params) return '';
    return params.trim().replace(/\s+/g, ' ');
  }

  function normalizeType(type) {
    if (!type) return '';
    return type.trim().replace(/[;\s]+$/g, '').replace(/\s+/g, ' ').slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/vue_sfc ──
__factories["./src/extractors/vue_sfc"] = function(module, exports) {
  
  /**
   * Extract Vue Single-File Component (SFC) signatures from .vue files.
   * Captures component metadata: name, props, emits, slots, composables, lifecycle.
   *
   * @param {string} src - Raw Vue SFC content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Extract script block (both <script> and <script setup>)
    const scriptMatch = src.match(/<script[^>]*>([\s\S]*?)<\/script>/);
    if (!scriptMatch) return sigs;
    const script = scriptMatch[1];

    // Component name (from export default { name: '...' })
    const nameRe = /(?:name\s*:\s*['"`]([a-zA-Z0-9]+)['"`]|export\s+default\s+defineComponent\s*\(\s*{\s*name\s*:\s*['"`]([a-zA-Z0-9]+)['"`])/;
    const nameMatch = script.match(nameRe);
    if (nameMatch) {
      sigs.push(`component ${nameMatch[1] || nameMatch[2]}`);
    }

    // Props definition
    const propsRe = /props\s*:\s*{([^}]*)}/;
    const propsMatch = script.match(propsRe);
    if (propsMatch) {
      const propLines = propsMatch[1].split(',');
      for (const line of propLines) {
        const propName = line.trim().match(/([a-zA-Z_$]\w*)/);
        if (propName) sigs.push(`prop ${propName[1]}`);
      }
    }

    // Props in composition API (defineProps)
    const definePropsRe = /defineProps\s*(?:<([^>]+)>)?\s*\(/;
    if (definePropsRe.test(script)) {
      sigs.push('props composition-api');
    }

    // Emits definition
    const emitsRe = /emits\s*:\s*\[([^\]]+)\]/;
    const emitsMatch = script.match(emitsRe);
    if (emitsMatch) {
      const emitNames = emitsMatch[1].split(',').map(e => e.trim().replace(/['"`]/g, ''));
      for (const e of emitNames) {
        if (e) sigs.push(`emit ${e}`);
      }
    }

    // Emits in composition API (defineEmits)
    const defineEmitsRe = /defineEmits\s*(?:<([^>]+)>)?\s*\(/;
    if (defineEmitsRe.test(script)) {
      sigs.push('emits composition-api');
    }

    // Lifecycle hooks
    const lifecycleHooks = ['setup', 'created', 'mounted', 'updated', 'unmounted'];
    for (const hook of lifecycleHooks) {
      if (new RegExp(`\\b${hook}\\s*\\(`).test(script)) {
        sigs.push(`lifecycle ${hook}`);
      }
    }

    // Composable/hooks usage (useXxx)
    const composableRe = /(?:import|const)\s+(?:{[^}]*}|[a-zA-Z_$]\w*)\s+from\s+['"]/g;
    if (composableRe.test(script)) {
      const useRe = /use([A-Z]\w*)/g;
      for (const m of script.matchAll(useRe)) {
        sigs.push(`composable use${m[1]}`);
      }
    }

    // Slots definition — capture from <slot name="..."> and template slots
    const namedSlotRe = /<slot\s+name=['"]([a-zA-Z_$]\w*)['"][^>]*>/g;
    const templateSlotRe = /<template\s+#([a-zA-Z_$]\w*)|v-slot:([a-zA-Z_$]\w*)/g;
    const slots = new Set();
    
    // Named slots in template: <slot name="header">
    for (const m of src.matchAll(namedSlotRe)) {
      if (m[1]) slots.add(m[1]);
    }
    
    // Template slots with # or v-slot
    for (const m of src.matchAll(templateSlotRe)) {
      const slotName = m[1] || m[2];
      if (slotName) slots.add(slotName);
    }
    
    for (const s of slots) {
      sigs.push(`slot ${s}`);
    }

    return Array.from(new Set(sigs)).slice(0, 50);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/xml ──
__factories["./src/extractors/xml"] = function(module, exports) {
  
  /**
   * Lightweight XML config extractor.
   * Captures root tags, key config tags, and id/name/class attributes.
   *
   * @param {string} src - Raw XML content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    // Remove comments and XML declaration for simpler scanning.
    const stripped = src
      .replace(/<!--[\s\S]*?-->/g, '')
      .replace(/<\?xml[\s\S]*?\?>/gi, '');

    // Root element (first opening tag).
    const root = stripped.match(/<\s*([A-Za-z_][\w:.-]*)\b[^>]*>/);
    if (root) sigs.push(`root ${root[1]}`);

    // High-value config-like tags.
    const tagRe = /<\s*([A-Za-z_][\w:.-]*)\b([^>]*)>/g;
    for (const m of stripped.matchAll(tagRe)) {
      const tag = m[1];
      const attrs = m[2] || '';

      if (/^(bean|beans|route|routes|property|properties|dependency|dependencies|plugin|plugins|configuration|settings|profile|profiles|module|modules)$/i.test(tag)) {
        sigs.push(`tag ${tag}`);
      }

      const id = attrs.match(/\bid\s*=\s*"([^"]+)"/i);
      if (id) sigs.push(`${tag}#${id[1]}`);

      const name = attrs.match(/\bname\s*=\s*"([^"]+)"/i);
      if (name) sigs.push(`${tag}[name=${name[1]}]`);

      const cls = attrs.match(/\bclass\s*=\s*"([^"]+)"/i);
      if (cls) sigs.push(`${tag} -> ${cls[1]}`);
    }

    return Array.from(new Set(sigs)).slice(0, 50);
  }

  module.exports = { extract };
  
};

// ── ./src/extractors/yaml ──
__factories["./src/extractors/yaml"] = function(module, exports) {
  
  /**
   * Extract signatures from YAML configuration files.
   * @param {string} src - Raw file content
   * @returns {string[]} Array of signature strings
   */
  function extract(src) {
    if (!src || typeof src !== 'string') return [];
    const sigs = [];

    const lines = src.split('\n');

    // Top-level keys (no leading whitespace)
    const topKeys = [];
    for (const line of lines) {
      if (/^#/.test(line)) continue;
      const m = line.match(/^([\w-]+)\s*:/);
      if (m) topKeys.push(m[1]);
    }
    if (topKeys.length > 0) sigs.push(`keys: [${topKeys.slice(0, 12).join(', ')}]`);

    // GitHub Actions: jobs
    let inJobs = false;
    for (const line of lines) {
      if (/^jobs\s*:/.test(line)) { inJobs = true; continue; }
      if (inJobs && /^[a-z]/.test(line) && !line.startsWith('jobs')) inJobs = false;
      if (inJobs) {
        const m = line.match(/^  ([\w-]+)\s*:/);
        if (m) sigs.push(`job: ${m[1]}`);
      }
    }

    // Docker Compose: services
    let inServices = false;
    for (const line of lines) {
      if (/^services\s*:/.test(line)) { inServices = true; continue; }
      if (inServices && /^[a-z]/.test(line) && !line.startsWith('services')) inServices = false;
      if (inServices) {
        const m = line.match(/^  ([\w-]+)\s*:/);
        if (m) sigs.push(`service: ${m[1]}`);
      }
    }

    // OpenAPI paths
    let inPaths = false;
    for (const line of lines) {
      if (/^paths\s*:/.test(line)) { inPaths = true; continue; }
      if (inPaths && /^[a-z]/.test(line) && !line.startsWith('paths')) inPaths = false;
      if (inPaths) {
        const m = line.match(/^  (\/[\w/{}-]*)\s*:/);
        if (m) sigs.push(`path: ${m[1]}`);
      }
    }

    return sigs.slice(0, 25);
  }

  module.exports = { extract };
  
};

// ── ./src/format/benchmark-report ──
__factories["./src/format/benchmark-report"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');

  function escapeHtml(value) {
    return String(value == null ? '' : value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function formatInt(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 'n/a';
    return Math.round(n).toLocaleString('en-US');
  }

  function formatCompact(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 'n/a';
    if (Math.abs(n) >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
    if (Math.abs(n) >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
    return String(Math.round(n));
  }

  function formatPct(value, digits = 1) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 'n/a';
    return `${n.toFixed(digits)}%`;
  }

  function formatMaybePct(value, digits = 1) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 'n/a';
    return `${n.toFixed(digits)}%`;
  }

  function formatRatio(value, digits = 1) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 'n/a';
    return `${n.toFixed(digits)}x`;
  }

  function formatMoney(value) {
    const n = Number(value);
    if (!Number.isFinite(n)) return 'n/a';
    return `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
  }

  function durationLabel(ms) {
    const n = Number(ms);
    if (!Number.isFinite(n)) return 'n/a';
    const sec = n / 1000;
    if (sec < 60) return `${sec.toFixed(1)}s`;
    const min = Math.floor(sec / 60);
    const rem = sec - (min * 60);
    return `${min}m ${rem.toFixed(1)}s`;
  }

  function maxOrZero(values) {
    if (!Array.isArray(values) || values.length === 0) return 0;
    return Math.max(...values.map((v) => (Number.isFinite(v) ? v : 0)));
  }

  function readJson(filePath) {
    try {
      if (!fs.existsSync(filePath)) return null;
      return JSON.parse(fs.readFileSync(filePath, 'utf8'));
    } catch (_) {
      return null;
    }
  }

  function loadBenchmarkReports(cwd) {
    const reportsDir = path.join(cwd, 'benchmarks', 'reports');
    return {
      reportsDir,
      token: readJson(path.join(reportsDir, 'token-reduction.json')),
      retrieval: readJson(path.join(reportsDir, 'retrieval.json')),
      quality: readJson(path.join(reportsDir, 'quality.json')),
      task: readJson(path.join(reportsDir, 'task-benchmark.json')),
      matrix: readJson(path.join(reportsDir, 'benchmark-matrix.json')),
    };
  }

  function buildRetrievalSummary(retrieval) {
    if (!retrieval || !Array.isArray(retrieval.repos) || retrieval.repos.length === 0) return null;
    let totalTasks = 0;
    let weightedHit = 0;
    let weightedRand = 0;
    let correct = 0;
    let partial = 0;
    let wrong = 0;
    let repoCount = 0;

    for (const repo of retrieval.repos) {
      const tasks = Number(repo.tasks) || 0;
      repoCount++;
      totalTasks += tasks;
      weightedHit += (Number(repo.hitAt5) || 0) * tasks;
      weightedRand += (Number(repo.randomBaseline) || 0) * tasks;
      correct += Number(repo.tiers && repo.tiers.correct) || 0;
      partial += Number(repo.tiers && repo.tiers.partial) || 0;
      wrong += Number(repo.tiers && repo.tiers.wrong) || 0;
    }

    const hitAt5 = totalTasks > 0 ? (weightedHit / totalTasks) * 100 : null;
    const randomBaseline = totalTasks > 0 ? (weightedRand / totalTasks) * 100 : null;
    const lift = hitAt5 && randomBaseline ? hitAt5 / randomBaseline : null;

    return {
      repoCount,
      totalTasks,
      hitAt5,
      randomBaseline,
      lift,
      correct,
      partial,
      wrong,
    };
  }

  function buildBenchmarkSummary(reports, matrixSummary) {
    const missing = [];
    if (!reports.token) missing.push('token-reduction.json');
    if (!reports.retrieval) missing.push('retrieval.json');
    if (!reports.quality) missing.push('quality.json');
    if (!reports.task) missing.push('task-benchmark.json');

    const retrievalSummary = buildRetrievalSummary(reports.retrieval);
    const qualitySummary = reports.quality && reports.quality.summary ? reports.quality.summary : null;
    const tokenSummary = reports.token && reports.token.summary ? reports.token.summary : null;
    const taskSummary = reports.task && reports.task.summary ? reports.task.summary : null;
    const matrix = matrixSummary || reports.matrix || null;

    const generatedCandidates = [
      matrix && matrix.generated,
      reports.task && reports.task.generated,
      reports.retrieval && reports.retrieval.generated,
      reports.quality && reports.quality.timestamp,
      reports.token && reports.token.timestamp,
    ].filter(Boolean);
    const generatedAt = generatedCandidates
      .map((value) => ({ value, time: Date.parse(value) }))
      .filter((item) => Number.isFinite(item.time))
      .sort((a, b) => b.time - a.time)[0];

    return {
      generatedAt: (generatedAt && generatedAt.value) || generatedCandidates[0] || new Date().toISOString(),
      missing,
      tokenSummary,
      retrievalSummary,
      qualitySummary,
      taskSummary,
      matrix,
    };
  }

  function renderCard(label, value, hint, tone) {
    const toneClass = tone ? ` ${tone}` : '';
    return [
      `<article class="card${toneClass}">`,
      `<div class="label">${escapeHtml(label)}</div>`,
      `<div class="value">${escapeHtml(value)}</div>`,
      `<div class="hint">${escapeHtml(hint || '')}</div>`,
      '</article>',
    ].join('');
  }

  function renderProgress(label, value, max, suffix) {
    const safeValue = Number.isFinite(value) ? value : 0;
    const safeMax = Math.max(1, Number.isFinite(max) ? max : 1);
    const width = Math.max(2, Math.min(100, (safeValue / safeMax) * 100));
    return [
      '<div class="progress-row">',
      `<div class="progress-label">${escapeHtml(label)}</div>`,
      '<div class="progress-bar"><span style="width:',
      String(width.toFixed(1)),
      '%"></span></div>',
      `<div class="progress-value">${escapeHtml(`${safeValue}${suffix || ''}`)}</div>`,
      '</div>',
    ].join('');
  }

  function renderMatrixSection(matrix) {
    if (!matrix || !Array.isArray(matrix.steps) || matrix.steps.length === 0) return '';
    const rows = matrix.steps.map((step) => {
      const status = step.ok ? 'ok' : 'fail';
      return [
        '<tr>',
        `<td>${escapeHtml(step.name)}</td>`,
        `<td><span class="badge ${status}">${escapeHtml(step.ok ? 'ok' : `exit ${step.status}`)}</span></td>`,
        `<td>${escapeHtml(durationLabel(step.durationMs))}</td>`,
        `<td><code>${escapeHtml(['node', step.script].concat(step.args || []).join(' '))}</code></td>`,
        '</tr>',
      ].join('');
    }).join('');

    return [
      '<section>',
      '<h2>Run matrix</h2>',
      '<p class="section-copy">This shows which benchmark jobs ran, whether they succeeded, and how long each step took.</p>',
      '<table>',
      '<thead><tr><th>Step</th><th>Status</th><th>Duration</th><th>Command</th></tr></thead>',
      `<tbody>${rows}</tbody>`,
      '</table>',
      '</section>',
    ].join('');
  }

  function renderTokenSection(token) {
    if (!token || !Array.isArray(token.repos) || token.repos.length === 0) return '';
    const rows = token.repos
      .slice()
      .sort((a, b) => (b.reductionPct || 0) - (a.reductionPct || 0))
      .map((repo) => [
        '<tr>',
        `<td>${escapeHtml(repo.repo)}</td>`,
        `<td>${escapeHtml(repo.language || 'n/a')}</td>`,
        `<td>${escapeHtml(formatCompact(repo.rawTokens))}</td>`,
        `<td>${escapeHtml(formatCompact(repo.finalTokens))}</td>`,
        `<td>${escapeHtml(formatMaybePct(repo.reductionPct, 1))}</td>`,
        '</tr>',
      ].join(''))
      .join('');

    return [
      '<section>',
      '<h2>Token reduction</h2>',
      '<p class="section-copy">Raw repository tokens versus SigMap output size across the benchmark repos.</p>',
      '<table>',
      '<thead><tr><th>Repo</th><th>Language</th><th>Raw tokens</th><th>Final tokens</th><th>Reduction</th></tr></thead>',
      `<tbody>${rows}</tbody>`,
      '</table>',
      '</section>',
    ].join('');
  }

  function renderRetrievalSection(retrieval) {
    if (!retrieval || !Array.isArray(retrieval.repos) || retrieval.repos.length === 0) return '';
    const rows = retrieval.repos.map((repo) => {
      const lift = repo.randomBaseline > 0 ? (repo.hitAt5 / repo.randomBaseline) : null;
      return [
        '<tr>',
        `<td>${escapeHtml(repo.repo)}</td>`,
        `<td>${escapeHtml(formatMaybePct((repo.randomBaseline || 0) * 100, 1))}</td>`,
        `<td>${escapeHtml(formatMaybePct((repo.hitAt5 || 0) * 100, 1))}</td>`,
        `<td>${escapeHtml(formatRatio(lift, 1))}</td>`,
        `<td>${escapeHtml(String((repo.tiers && repo.tiers.correct) || 0))}</td>`,
        `<td>${escapeHtml(String((repo.tiers && repo.tiers.partial) || 0))}</td>`,
        `<td>${escapeHtml(String((repo.tiers && repo.tiers.wrong) || 0))}</td>`,
        '</tr>',
      ].join('');
    }).join('');

    return [
      '<section>',
      '<h2>Retrieval quality</h2>',
      '<p class="section-copy">Hit@5 performance against the random baseline, plus the quality-tier mix that drives the task benchmark.</p>',
      '<table>',
      '<thead><tr><th>Repo</th><th>Random hit@5</th><th>SigMap hit@5</th><th>Lift</th><th>Correct</th><th>Partial</th><th>Wrong</th></tr></thead>',
      `<tbody>${rows}</tbody>`,
      '</table>',
      '</section>',
    ].join('');
  }

  function renderQualitySection(quality) {
    if (!quality || !Array.isArray(quality.repos) || quality.repos.length === 0) return '';
    const rows = quality.repos.map((repo) => {
      const overflow = (repo.rawTokens || 0) > 128000 ? 'overflow' : 'fits';
      return [
        '<tr>',
        `<td>${escapeHtml(repo.repo)}</td>`,
        `<td>${escapeHtml(formatInt(repo.groundedSymbols))}</td>`,
        `<td>${escapeHtml(formatInt(repo.darkSymbols))}</td>`,
        `<td>${escapeHtml(formatMaybePct(repo.groundingPct, 0))}</td>`,
        `<td>${escapeHtml(String(repo.filesHiddenRaw || 0))}</td>`,
        `<td><span class="badge ${overflow === 'overflow' ? 'warn' : 'ok'}">${escapeHtml(overflow)}</span></td>`,
        '</tr>',
      ].join('');
    }).join('');

    return [
      '<section>',
      '<h2>Quality and hallucination surface</h2>',
      '<p class="section-copy">How much code stays visible to the model, plus the overflow and dark-symbol risk by repo.</p>',
      '<table>',
      '<thead><tr><th>Repo</th><th>Grounded symbols</th><th>Dark symbols</th><th>Grounding</th><th>Hidden files (raw)</th><th>GPT-4o 128K</th></tr></thead>',
      `<tbody>${rows}</tbody>`,
      '</table>',
      '</section>',
    ].join('');
  }

  function renderTaskSection(task) {
    if (!task || !Array.isArray(task.repos) || task.repos.length === 0 || !task.summary) return '';
    const summary = task.summary;
    const maxReduction = maxOrZero(task.repos.map((repo) => Number(repo.reductionPct) || 0));
    const repoBars = task.repos
      .slice()
      .sort((a, b) => (b.reductionPct || 0) - (a.reductionPct || 0))
      .slice(0, 10)
      .map((repo) => renderProgress(repo.repo, Number(repo.reductionPct) || 0, maxReduction, '%'))
      .join('');

    return [
      '<section>',
      '<h2>Task benchmark</h2>',
      '<p class="section-copy">A prompt-reduction proxy derived from retrieval quality tiers. Lower prompts means the right file surfaces sooner.</p>',
      '<div class="split">',
      '<div class="panel">',
      '<h3>Answer quality tiers</h3>',
      renderProgress('Correct', Number(summary.correctPct) || 0, 100, '%'),
      renderProgress('Partial', Number(summary.partialPct) || 0, 100, '%'),
      renderProgress('Wrong', Number(summary.wrongPct) || 0, 100, '%'),
      '</div>',
      '<div class="panel">',
      '<h3>Best prompt reduction by repo</h3>',
      repoBars,
      '</div>',
      '</div>',
      '</section>',
    ].join('');
  }

  function generateBenchmarkReportHtml(reports, opts = {}) {
    const summary = buildBenchmarkSummary(reports, opts.matrixSummary);
    const cards = [];
    cards.push(renderCard(
      'Token reduction',
      summary.tokenSummary ? formatPct(summary.tokenSummary.overallReductionPct, 1) : 'n/a',
      summary.tokenSummary ? `${formatInt(summary.tokenSummary.repoCount)} repos • ${formatCompact(summary.tokenSummary.totalRawTokens)} raw -> ${formatCompact(summary.tokenSummary.totalFinalTokens)} final` : 'token-reduction.json missing',
      'cool'
    ));
    cards.push(renderCard(
      'Retrieval hit@5',
      summary.retrievalSummary ? formatPct(summary.retrievalSummary.hitAt5, 1) : 'n/a',
      summary.retrievalSummary ? `${formatPct(summary.retrievalSummary.randomBaseline, 1)} random baseline • ${formatRatio(summary.retrievalSummary.lift, 1)} lift` : 'retrieval.json missing',
      'warm'
    ));
    cards.push(renderCard(
      'Prompt reduction',
      summary.taskSummary ? formatPct(summary.taskSummary.avgReductionPct, 0) : 'n/a',
      summary.taskSummary ? `${summary.taskSummary.avgPromptsWithout} -> ${summary.taskSummary.avgPromptsWith} prompts • ${formatInt(summary.taskSummary.totalTasks)} tasks` : 'task-benchmark.json missing',
      'neutral'
    ));
    cards.push(renderCard(
      'Overflow risk',
      summary.qualitySummary ? `${formatInt(summary.qualitySummary.overflowGPT4oCount)} repos` : 'n/a',
      summary.qualitySummary ? `${formatInt(summary.qualitySummary.totalHiddenFiles)} hidden raw files • ${formatMoney(summary.qualitySummary.gpt4oSavedPerMonth)}/month saved` : 'quality.json missing',
      summary.qualitySummary && summary.qualitySummary.overflowGPT4oCount > 0 ? 'warn' : 'ok'
    ));

    const missingHtml = summary.missing.length > 0
      ? `<div class="notice">Missing source reports: ${escapeHtml(summary.missing.join(', '))}. The page still renders whatever data is available.</div>`
      : '';

    return [
      '<!doctype html>',
      '<html lang="en">',
      '<head>',
      '<meta charset="utf-8" />',
      '<meta name="viewport" content="width=device-width, initial-scale=1" />',
      '<title>SigMap Benchmark Report</title>',
      '<style>',
      ':root { color-scheme: light; --bg:#f5f1e8; --panel:#fffaf2; --ink:#1f1b16; --muted:#6a6258; --line:#dccfbf; --gold:#c87f2a; --green:#2f6f52; --blue:#2f5f8f; --red:#9f4f43; --shadow:0 18px 40px rgba(54,38,14,.10);} ',
      '*{box-sizing:border-box} body{margin:0;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:linear-gradient(180deg,#f3ecdf 0%,#f7f3ed 100%);color:var(--ink)}',
      '.page{max-width:1240px;margin:0 auto;padding:28px 20px 56px}',
      'header{display:flex;justify-content:space-between;gap:24px;align-items:flex-end;margin-bottom:24px}',
      'h1{margin:0;font-size:clamp(2rem,4vw,3.6rem);line-height:1.02;letter-spacing:-.04em}',
      '.lede{max-width:760px;color:var(--muted);font-size:1rem;line-height:1.6;margin-top:10px}',
      '.stamp{font-size:.92rem;color:var(--muted);text-align:right}',
      '.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin:20px 0 24px}',
      '.card,.panel,.notice,section{background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:18px}',
      '.card{padding:18px 18px 16px}.card.cool{background:#f7f5ff}.card.warm{background:#fff4eb}.card.warn{background:#fff1eb}.card.ok{background:#eff8f1}',
      '.label{font-size:.84rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}',
      '.value{font-size:2rem;font-weight:700;letter-spacing:-.04em;margin-top:8px}',
      '.hint{font-size:.95rem;color:var(--muted);margin-top:8px;line-height:1.5}',
      '.notice{padding:14px 16px;margin-bottom:20px;color:var(--muted)}',
      'section{padding:20px;margin-top:18px}',
      'h2{margin:0 0 6px;font-size:1.4rem;letter-spacing:-.03em}',
      'h3{margin:0 0 14px;font-size:1rem}',
      '.section-copy{margin:0 0 16px;color:var(--muted);line-height:1.6}',
      'table{width:100%;border-collapse:collapse;font-size:.95rem}',
      'th,td{padding:10px 12px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}',
      'th{font-size:.82rem;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}',
      'tbody tr:hover{background:rgba(200,127,42,.06)}',
      '.badge{display:inline-flex;align-items:center;padding:4px 8px;border-radius:999px;font-size:.78rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em}',
      '.badge.ok{background:#e6f4ea;color:#21573f}.badge.warn{background:#fff0de;color:#8a4a17}.badge.fail{background:#fde8e5;color:#8a2e23}',
      '.split{display:grid;grid-template-columns:1fr 1fr;gap:16px}',
      '.panel{padding:16px}',
      '.progress-row{display:grid;grid-template-columns:140px 1fr 60px;gap:12px;align-items:center;margin:10px 0}',
      '.progress-label,.progress-value{font-size:.92rem}',
      '.progress-bar{height:10px;border-radius:999px;background:#efe4d5;overflow:hidden}',
      '.progress-bar span{display:block;height:100%;border-radius:999px;background:linear-gradient(90deg,var(--gold),#ebbb61)}',
      'code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.85rem}',
      '@media (max-width: 1020px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.split{grid-template-columns:1fr}header{flex-direction:column;align-items:flex-start}.stamp{text-align:left}}',
      '@media (max-width: 640px){.grid{grid-template-columns:1fr}.progress-row{grid-template-columns:110px 1fr 52px}th:nth-child(n+5),td:nth-child(n+5){display:none}}',
      '</style>',
      '</head>',
      '<body>',
      '<div class="page">',
      '<header>',
      '<div>',
      '<h1>SigMap Benchmark Report</h1>',
      '<p class="lede">A self-contained view of token reduction, retrieval quality, hallucination surface, and task-level prompt reduction. This page reads the saved JSON benchmark artifacts so it stays easy to regenerate locally.</p>',
      '</div>',
      `<div class="stamp">Generated: ${escapeHtml(summary.generatedAt)}<br />Source directory: <code>benchmarks/reports</code></div>`,
      '</header>',
      missingHtml,
      `<div class="grid">${cards.join('')}</div>`,
      renderMatrixSection(summary.matrix),
      renderTokenSection(reports.token),
      renderRetrievalSection(reports.retrieval),
      renderQualitySection(reports.quality),
      renderTaskSection(reports.task),
      '</div>',
      '</body>',
      '</html>',
    ].join('');
  }

  function writeBenchmarkReport(cwd, opts = {}) {
    const reports = loadBenchmarkReports(cwd);
    const html = generateBenchmarkReportHtml(reports, opts);
    const filePath = path.join(reports.reportsDir, opts.fileName || 'benchmark-report.html');
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, html, 'utf8');
    return {
      file: filePath,
      summary: buildBenchmarkSummary(reports, opts.matrixSummary),
    };
  }

  module.exports = {
    loadBenchmarkReports,
    buildBenchmarkSummary,
    generateBenchmarkReportHtml,
    writeBenchmarkReport,
  };
  
};

// ── ./src/format/cache ──
__factories["./src/format/cache"] = function(module, exports) {
  
  /**
   * Format context output for Anthropic prompt cache API.
   *
   * Usage:
   *   const { formatCache } = __require('./src/format/src/format/cache');
   *   const json = formatCache(markdownContent);
   *   // json is a ready-to-use Anthropic system block with cache_control
   *
   * Writes: .github/copilot-instructions.cache.json
   */

  /**
   * Wrap markdown context in an Anthropic cache-control system block.
   * @param {string} content - Markdown content from formatOutput()
   * @returns {string} - JSON string: a single Anthropic system content block
   */
  function formatCache(content) {
    if (!content || typeof content !== 'string') content = '';
    const block = {
      type: 'text',
      text: content,
      cache_control: { type: 'ephemeral' },
    };
    return JSON.stringify(block, null, 2);
  }

  /**
   * Wrap markdown context in a full Anthropic messages API payload.
   * Includes the system array with cache_control so it can be copy-pasted
   * directly into an API call.
   * @param {string} content - Markdown content from formatOutput()
   * @param {string} [model] - Anthropic model ID (default: claude-opus-4-5)
   * @returns {string} - JSON string: { model, system: [...] }
   */
  function formatCachePayload(content, model) {
    if (!content || typeof content !== 'string') content = '';
    const payload = {
      model: model || 'claude-opus-4-5',
      system: [
        {
          type: 'text',
          text: content,
          cache_control: { type: 'ephemeral' },
        },
      ],
      messages: [],
    };
    return JSON.stringify(payload, null, 2);
  }

  module.exports = { formatCache, formatCachePayload };
  
};

// ── ./src/format/dashboard ──
__factories["./src/format/dashboard"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');
  const { readLog } = __require('./src/tracking/logger');

  const LANGUAGE_KEYS = [
    'typescript', 'javascript', 'python', 'java', 'kotlin', 'go', 'rust',
    'csharp', 'cpp', 'ruby', 'php', 'swift', 'dart', 'scala', 'vue',
    'svelte', 'html', 'css', 'yaml', 'shell', 'dockerfile',
  ];

  function toNumber(v) {
    const n = Number(v);
    return Number.isFinite(n) ? n : null;
  }

  function percentile(values, p) {
    if (!Array.isArray(values) || values.length === 0) return 0;
    const sorted = values.filter(Number.isFinite).slice().sort((a, b) => a - b);
    if (sorted.length === 0) return 0;
    if (p <= 0) return sorted[0];
    if (p >= 100) return sorted[sorted.length - 1];
    const idx = (p / 100) * (sorted.length - 1);
    const lo = Math.floor(idx);
    const hi = Math.ceil(idx);
    if (lo === hi) return sorted[lo];
    const frac = idx - lo;
    return sorted[lo] + (sorted[hi] - sorted[lo]) * frac;
  }

  function overBudgetStreak(entries) {
    if (!Array.isArray(entries) || entries.length === 0) return 0;
    let streak = 0;
    for (let i = entries.length - 1; i >= 0; i--) {
      if (entries[i] && entries[i].overBudget) streak++;
      else break;
    }
    return streak;
  }

  function loadConfig(cwd) {
    try {
      const p = path.join(cwd, 'gen-context.config.json');
      if (!fs.existsSync(p)) return null;
      return JSON.parse(fs.readFileSync(p, 'utf8'));
    } catch (_) {
      return null;
    }
  }

  function shouldExclude(rel, excludeSet) {
    if (!rel) return true;
    const parts = rel.split('/');
    for (const part of parts) {
      if (excludeSet.has(part)) return true;
    }
    return false;
  }

  function detectLanguage(filePath) {
    const base = path.basename(filePath);
    const ext = path.extname(filePath).toLowerCase();
    if (base === 'Dockerfile' || /^Dockerfile\./.test(base)) return 'dockerfile';
    if (ext === '.ts' || ext === '.tsx') return 'typescript';
    if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') return 'javascript';
    if (ext === '.py' || ext === '.pyw') return 'python';
    if (ext === '.java') return 'java';
    if (ext === '.kt' || ext === '.kts') return 'kotlin';
    if (ext === '.go') return 'go';
    if (ext === '.rs') return 'rust';
    if (ext === '.cs') return 'csharp';
    if (ext === '.cpp' || ext === '.c' || ext === '.h' || ext === '.hpp' || ext === '.cc') return 'cpp';
    if (ext === '.rb' || ext === '.rake') return 'ruby';
    if (ext === '.php') return 'php';
    if (ext === '.swift') return 'swift';
    if (ext === '.dart') return 'dart';
    if (ext === '.scala' || ext === '.sc') return 'scala';
    if (ext === '.vue') return 'vue';
    if (ext === '.svelte') return 'svelte';
    if (ext === '.html' || ext === '.htm') return 'html';
    if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') return 'css';
    if (ext === '.yml' || ext === '.yaml') return 'yaml';
    if (ext === '.sh' || ext === '.bash' || ext === '.zsh' || ext === '.fish') return 'shell';
    return null;
  }

  function walkFiles(dir, maxDepth, depth, out, excludeSet) {
    if (depth > maxDepth) return;
    let entries = [];
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch (_) {
      return;
    }
    for (const entry of entries) {
      const abs = path.join(dir, entry.name);
      const rel = abs.replace(/\\/g, '/');
      if (shouldExclude(rel, excludeSet)) continue;
      if (entry.isDirectory()) {
        walkFiles(abs, maxDepth, depth + 1, out, excludeSet);
      } else if (entry.isFile()) {
        out.push(abs);
      }
    }
  }

  function computeExtractorCoverage(cwd) {
    const cfg = loadConfig(cwd) || {};
    const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0
      ? cfg.srcDirs
      : ['src', 'app', 'lib', 'packages', 'services', 'api'];
    const exclude = new Set([
      'node_modules', '.git', 'dist', 'build', 'out', '__pycache__', '.next',
      'coverage', 'target', 'vendor', '.context', 'jetbrains-plugin/build',
    ]);
    if (Array.isArray(cfg.exclude)) {
      for (const item of cfg.exclude) exclude.add(String(item));
    }

    const counts = {};
    for (const key of LANGUAGE_KEYS) counts[key] = 0;

    const files = [];
    for (const relDir of srcDirs) {
      const absDir = path.join(cwd, relDir);
      if (!fs.existsSync(absDir)) continue;
      walkFiles(absDir, 8, 0, files, exclude);
    }

    for (const f of files) {
      const lang = detectLanguage(f);
      if (lang) counts[lang]++;
    }

    const covered = LANGUAGE_KEYS.filter((k) => counts[k] > 0).length;
    const supported = LANGUAGE_KEYS.length;
    const pct = supported > 0 ? parseFloat(((covered / supported) * 100).toFixed(1)) : 0;
    return { supported, covered, pct, perLanguage: counts };
  }

  function readBenchmarkTrend(cwd) {
    // Prefer per-user history file written by benchmark scripts
    const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
    if (fs.existsSync(histPath)) {
      const values = [];
      try {
        const lines = fs.readFileSync(histPath, 'utf8').trim().split('\n').filter(Boolean);
        for (const line of lines) {
          try {
            const obj = JSON.parse(line);
            if (obj.type === 'retrieval') {
              const v = toNumber(obj.hitAt5Pct);
              if (v !== null) values.push(v);
            }
          } catch (_) {}
        }
      } catch (_) {}
      if (values.length > 0) return values.slice(-30);
    }

    // Fallback: legacy benchmarks/results directory (CI artifacts)
    const resultDir = path.join(cwd, 'benchmarks', 'results');
    if (!fs.existsSync(resultDir)) return [];

    const files = [];
    walkFiles(resultDir, 6, 0, files, new Set());

    const values = [];
    for (const filePath of files) {
      const base = path.basename(filePath).toLowerCase();
      if (!base.endsWith('.json') && !base.endsWith('.jsonl') && !base.endsWith('.ndjson')) continue;
      let raw = '';
      try {
        raw = fs.readFileSync(filePath, 'utf8');
      } catch (_) {
        continue;
      }

      if (base.endsWith('.json')) {
        try {
          const obj = JSON.parse(raw);
          const direct = toNumber(obj && obj.hitAt5);
          const nested = toNumber(obj && obj.metrics && obj.metrics.hitAt5);
          if (direct !== null) values.push(direct);
          else if (nested !== null) values.push(nested);
        } catch (_) {}
        continue;
      }

      const lines = raw.split('\n').filter(Boolean);
      for (const line of lines) {
        try {
          const obj = JSON.parse(line);
          const direct = toNumber(obj && obj.hitAt5);
          const nested = toNumber(obj && obj.metrics && obj.metrics.hitAt5);
          if (direct !== null) values.push(direct);
          else if (nested !== null) values.push(nested);
        } catch (_) {}
      }
    }

    return values.slice(-30);
  }

  function lineChartSvg(values, title, ySuffix) {
    const width = 760;
    const height = 210;
    const left = 38;
    const right = 18;
    const top = 22;
    const bottom = 30;
    const innerW = width - left - right;
    const innerH = height - top - bottom;
    const clean = values.filter((n) => Number.isFinite(n));

    if (clean.length === 0) {
      return [
        `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${title}">`,
        '<rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/>',
        `<text x="20" y="36" fill="#d7defa" font-size="14" font-family="monospace">${title}</text>`,
        '<text x="20" y="96" fill="#8ea0d9" font-size="13" font-family="monospace">No data yet. Run with --track and --benchmark.</text>',
        '</svg>',
      ].join('');
    }

    const min = Math.min(...clean);
    const max = Math.max(...clean);
    const span = max - min || 1;

    const points = clean.map((v, i) => {
      const x = left + ((clean.length === 1 ? 0 : i / (clean.length - 1)) * innerW);
      const y = top + (1 - ((v - min) / span)) * innerH;
      return `${x.toFixed(1)},${y.toFixed(1)}`;
    }).join(' ');

    const latest = clean[clean.length - 1];
    const yLabel = ySuffix || '';
    const grid = [];
    for (let i = 0; i <= 4; i++) {
      const gy = top + (i / 4) * innerH;
      grid.push(`<line x1="${left}" y1="${gy.toFixed(1)}" x2="${left + innerW}" y2="${gy.toFixed(1)}" stroke="#223056" stroke-width="1"/>`);
    }

    return [
      `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${title}">`,
      '<defs><linearGradient id="lineFill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#42d392" stop-opacity="0.25"/><stop offset="100%" stop-color="#42d392" stop-opacity="0"/></linearGradient></defs>',
      '<rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/>',
      `<text x="20" y="26" fill="#d7defa" font-size="13" font-family="monospace">${title}</text>`,
      grid.join(''),
      `<polyline fill="none" stroke="#42d392" stroke-width="2.5" points="${points}"/>`,
      `<text x="20" y="${height - 8}" fill="#8ea0d9" font-size="12" font-family="monospace">latest: ${latest.toFixed(2)}${yLabel}</text>`,
      '</svg>',
    ].join('');
  }

  function barChartSvg(perLanguage) {
    const width = 760;
    const height = 260;
    const left = 20;
    const top = 34;
    const usableW = width - left * 2;
    const keys = LANGUAGE_KEYS.slice();
    const max = Math.max(1, ...keys.map((k) => perLanguage[k] || 0));
    const barW = usableW / keys.length;

    const bars = [];
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const v = perLanguage[key] || 0;
      const h = (v / max) * 160;
      const x = left + i * barW + 2;
      const y = top + 160 - h;
      bars.push(`<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${Math.max(2, barW - 4).toFixed(1)}" height="${h.toFixed(1)}" fill="#7aa2ff" rx="2"/>`);
    }

    const labels = ['ts', 'js', 'py', 'java', 'kt', 'go', 'rs', 'cs', 'cpp', 'rb', 'php', 'swift', 'dart', 'scala', 'vue', 'sv', 'html', 'css', 'yaml', 'sh', 'df'];
    const xLabels = labels.map((lbl, i) => {
      const x = left + i * barW + barW / 2;
      return `<text x="${x.toFixed(1)}" y="222" fill="#8ea0d9" font-size="9" font-family="monospace" text-anchor="middle">${lbl}</text>`;
    });

    return [
      `<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Extractor coverage by language">`,
      '<rect x="0" y="0" width="100%" height="100%" fill="#0f1320" rx="12"/>',
      '<text x="20" y="24" fill="#d7defa" font-size="13" font-family="monospace">Per-language extractor coverage (file counts)</text>',
      '<line x1="20" y1="194" x2="740" y2="194" stroke="#223056" stroke-width="1"/>',
      bars.join(''),
      xLabels.join(''),
      '</svg>',
    ].join('');
  }

  function sparkline(values) {
    const clean = values.filter((n) => Number.isFinite(n));
    if (clean.length === 0) return 'n/a';
    const ticks = '▁▂▃▄▅▆▇█';
    const min = Math.min(...clean);
    const max = Math.max(...clean);
    const span = max - min || 1;
    return clean.map((v) => {
      const idx = Math.max(0, Math.min(ticks.length - 1, Math.round(((v - min) / span) * (ticks.length - 1))));
      return ticks[idx];
    }).join('');
  }

  function escapeAttr(s) {
    return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
      { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
    ));
  }

  // Surgical Context (v6.12.0): read the published token-reduction benchmark and
  // aggregate it for the dashboard panel. Numbers are never hand-typed — they come
  // straight from benchmarks/reports/token-reduction.json.
  function readTokenReduction(cwd) {
    const p = path.join(cwd, 'benchmarks', 'reports', 'token-reduction.json');
    let data;
    try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
    const repos = Array.isArray(data.repos) ? data.repos : [];
    if (repos.length === 0) return null;

    let baseline = 0, signatures = 0, surgical = 0, hasSurgical = false;
    for (const r of repos) {
      baseline += toNumber(r.rawTokens) || 0;
      signatures += toNumber(r.finalTokens) || 0;
      const s = toNumber(r.surgicalTokens);
      if (s !== null) { surgical += s; hasSurgical = true; }
    }

    const out = {
      version: data.version || null,
      repoCount: repos.length,
      baseline,
      signatures,
      savedPct: baseline > 0 ? Math.round((1 - signatures / baseline) * 1000) / 10 : 0,
      perRepo: repos.map((r) => ({
        repo: r.repo,
        language: r.language,
        rawTokens: toNumber(r.rawTokens) || 0,
        finalTokens: toNumber(r.finalTokens) || 0,
        reductionPct: toNumber(r.reductionPct) || 0,
      })),
    };
    if (hasSurgical) {
      out.surgical = surgical;
      out.surgicalSavedPct = baseline > 0 ? Math.round((1 - surgical / baseline) * 1000) / 10 : 0;
    }
    return out;
  }

  function tokenReductionPanelHtml(tr) {
    if (!tr) {
      return '<div class="panel"><div class="label">Token Reduction</div>' +
        '<div class="value" style="font-size:13px">No token-reduction benchmark found — run the token benchmark to populate this panel.</div></div>';
    }
    const fmt = (n) => Number(n).toLocaleString('en-US');
    const tiers = [
      { label: 'Whole-file baseline', value: fmt(tr.baseline) + ' tok' },
      { label: 'Ranked signatures (ask)', value: fmt(tr.signatures) + ' tok' },
    ];
    if (tr.surgical != null) {
      tiers.push({ label: 'Surgical (index + delta)', value: fmt(tr.surgical) + ' tok' });
      tiers.push({ label: 'Saved (surgical)', value: tr.surgicalSavedPct + '%' });
    } else {
      tiers.push({ label: 'Saved', value: tr.savedPct + '%' });
    }
    const tierHtml = tiers.map((t) =>
      `<div class="card"><div class="label">${escapeAttr(t.label)}</div><div class="value">${escapeAttr(t.value)}</div></div>`
    ).join('');

    // Proportional comparison bar (baseline = full width).
    const sigPct = tr.baseline > 0 ? Math.max(0.4, (tr.signatures / tr.baseline) * 100) : 0;
    const surgPct = (tr.surgical != null && tr.baseline > 0) ? Math.max(0.4, (tr.surgical / tr.baseline) * 100) : null;
    const barRow = (label, pct, color) =>
      `<div style="margin:4px 0;font-size:11px;color:#8ea0d9">${escapeAttr(label)}</div>` +
      `<div style="background:#0a0f1e;border:1px solid #223056;border-radius:6px;height:14px;overflow:hidden">` +
      `<div style="width:${pct.toFixed(1)}%;height:100%;background:${color}"></div></div>`;
    const bars = [
      barRow('Whole-file baseline (100%)', 100, '#3a4a78'),
      barRow(`Ranked signatures — ${tr.savedPct}% saved`, sigPct, '#2e7d6b'),
      surgPct != null ? barRow(`Surgical — ${tr.surgicalSavedPct}% saved`, surgPct, '#5ad1a8') : '',
    ].filter(Boolean).join('');

    const rows = tr.perRepo.slice(0, 8).map((r) =>
      `<tr><td>${escapeAttr(r.repo)}</td><td>${escapeAttr(r.language)}</td>` +
      `<td style="text-align:right">${fmt(r.rawTokens)}</td>` +
      `<td style="text-align:right">${fmt(r.finalTokens)}</td>` +
      `<td style="text-align:right">${r.reductionPct}%</td></tr>`
    ).join('');

    return [
      '<div class="panel">',
      `<div class="label">Token Reduction — ${tr.repoCount} benchmark repos${tr.version ? ' · v' + escapeAttr(tr.version) : ''}</div>`,
      `<div class="grid" style="margin:8px 0">${tierHtml}</div>`,
      bars,
      '<table style="width:100%;border-collapse:collapse;margin-top:10px;font-size:12px">',
      '<thead><tr style="color:#8ea0d9;text-align:left">',
      '<th>Repo</th><th>Lang</th><th style="text-align:right">Baseline</th><th style="text-align:right">Signatures</th><th style="text-align:right">Saved</th>',
      '</tr></thead>',
      `<tbody>${rows}</tbody>`,
      '</table>',
      '</div>',
    ].join('');
  }

  function buildDashboardData(cwd, health) {
    const entries = readLog(cwd);
    const recent = entries.slice(-30);
    const tokenReductionTrend = recent.map((e) => toNumber(e.reductionPct)).filter((n) => n !== null);
    const hitAt5Trend = readBenchmarkTrend(cwd);
    const coverage = computeExtractorCoverage(cwd);

    const finals = entries.map((e) => toNumber(e.finalTokens)).filter((n) => n !== null);
    const summary = {
      grade: health.grade,
      score: health.score,
      daysSinceRegen: health.daysSinceRegen,
      totalRuns: entries.length,
      overBudgetRate: entries.length > 0
        ? parseFloat(((entries.filter((e) => e.overBudget).length / entries.length) * 100).toFixed(1))
        : 0,
      p50TokenCount: Math.round(percentile(finals, 50)),
      p95TokenCount: Math.round(percentile(finals, 95)),
      overBudgetStreak: overBudgetStreak(entries),
      extractorCoverage: coverage.pct,
    };

    const tokenReduction = readTokenReduction(cwd);

    return {
      summary,
      tokenReductionTrend,
      hitAt5Trend,
      coverage,
      tokenReduction,
      charts: {
        tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
        hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
        coverageSvg: barChartSvg(coverage.perLanguage),
        tokenSavingsPanel: tokenReductionPanelHtml(tokenReduction),
      },
    };
  }

  function generateDashboardHtml(cwd, health) {
    const data = buildDashboardData(cwd, health);
    const cards = [
      { label: 'Current grade', value: `${data.summary.grade} (${data.summary.score}/100)` },
      { label: 'Days since regen', value: data.summary.daysSinceRegen === null ? 'n/a' : String(data.summary.daysSinceRegen) },
      { label: 'Total tracked runs', value: String(data.summary.totalRuns) },
      { label: 'Over-budget %', value: `${data.summary.overBudgetRate}%` },
      { label: 'p50 token count', value: String(data.summary.p50TokenCount) },
      { label: 'p95 token count', value: String(data.summary.p95TokenCount) },
      { label: 'Over-budget streak', value: String(data.summary.overBudgetStreak) },
      { label: 'Extractor coverage', value: `${data.summary.extractorCoverage}%` },
    ];

    const cardHtml = cards.map((c) => `<div class="card"><div class="label">${c.label}</div><div class="value">${c.value}</div></div>`).join('');

    const html = [
      '<!doctype html>',
      '<html lang="en">',
      '<head>',
      '<meta charset="utf-8"/>',
      '<meta name="viewport" content="width=device-width,initial-scale=1"/>',
      '<title>SigMap Dashboard</title>',
      '<style>',
      'body{margin:0;background:#0a0f1e;color:#e6ecff;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}',
      '.wrap{max-width:980px;margin:0 auto;padding:24px}',
      'h1{font-size:22px;margin:0 0 6px 0}',
      '.sub{color:#8ea0d9;font-size:12px;margin-bottom:20px}',
      '.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}',
      '.card{background:#111a33;border:1px solid #223056;border-radius:10px;padding:10px}',
      '.label{font-size:11px;color:#8ea0d9;margin-bottom:6px}',
      '.value{font-size:16px;color:#f5f7ff}',
      '.panel{background:#111a33;border:1px solid #223056;border-radius:12px;padding:10px;margin-top:12px}',
      '@media (max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr));}}',
      '</style>',
      '</head>',
      '<body>',
      '<div class="wrap">',
      '<h1>SigMap v2.10 dashboard</h1>',
      '<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
      `<div class="grid">${cardHtml}</div>`,
      data.charts.tokenSavingsPanel,
      `<div class="panel">${data.charts.tokenReductionSvg}</div>`,
      `<div class="panel">${data.charts.hitAt5Svg}</div>`,
      `<div class="panel">${data.charts.coverageSvg}</div>`,
      '</div>',
      '</body>',
      '</html>',
    ].join('');

    return { html, data };
  }

  function renderHistoryCharts(cwd, health) {
    const data = buildDashboardData(cwd, health);
    const lines = [
      '[sigmap] history charts:',
      `  token reduction trend : ${sparkline(data.tokenReductionTrend)}`,
      `  hit@5 trend           : ${sparkline(data.hitAt5Trend)}`,
      `  extractor coverage    : ${data.coverage.covered}/${data.coverage.supported} (${data.coverage.pct}%)`,
      '',
      '[sigmap] inline svg: token reduction',
      data.charts.tokenReductionSvg,
      '',
      '[sigmap] inline svg: hit@5',
      data.charts.hitAt5Svg,
      '',
      '[sigmap] inline svg: coverage',
      data.charts.coverageSvg,
    ];

    return {
      text: lines.join('\n'),
      tokenReductionSparkline: sparkline(data.tokenReductionTrend),
      hitAt5Sparkline: sparkline(data.hitAt5Trend),
      summary: data.summary,
      charts: data.charts,
    };
  }

  module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak };
  
};

// ── ./src/format/gain-terminal ──
__factories["./src/format/gain-terminal"] = function(module, exports) {
  
  /**
   * SigMap `gain` — terminal renderer.
   *
   * Zero-dependency ANSI dashboard for token savings. Honors NO_COLOR and
   * non-TTY output (plain text for pipes / --json consumers elsewhere).
   *
   * "saved" is a counterfactual estimate (whole-file baseline − actual context),
   * never a measured delta — the footer says so on every view.
   */

  const USE_COLOR = !process.env.NO_COLOR && process.stdout.isTTY;

  const C = {
    reset: USE_COLOR ? '\x1b[0m' : '',
    dim: USE_COLOR ? '\x1b[2m' : '',
    bold: USE_COLOR ? '\x1b[1m' : '',
    green: USE_COLOR ? '\x1b[32m' : '',
    yellow: USE_COLOR ? '\x1b[33m' : '',
    red: USE_COLOR ? '\x1b[31m' : '',
    cyan: USE_COLOR ? '\x1b[36m' : '',
    gray: USE_COLOR ? '\x1b[90m' : '',
  };

  // ── formatters ───────────────────────────────────────────────────────────
  function humanTokens(n) {
    n = Number(n) || 0;
    if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
    if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
    if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
    return String(Math.round(n));
  }

  function fmtInt(n) {
    return (Number(n) || 0).toLocaleString('en-US');
  }

  function fmtUSD(n) {
    n = Number(n) || 0;
    if (n >= 1000) return '$' + n.toLocaleString('en-US', { maximumFractionDigits: 0 });
    return '$' + n.toFixed(2);
  }

  function fmtDuration(ms) {
    ms = Number(ms) || 0;
    if (ms >= 3.6e6) return (ms / 3.6e6).toFixed(1) + 'h';
    if (ms >= 60000) return (ms / 60000).toFixed(1) + 'm';
    if (ms >= 1000) return (ms / 1000).toFixed(1) + 's';
    return Math.round(ms) + 'ms';
  }

  function fmtPct(p) {
    return (Number(p) || 0).toFixed(1) + '%';
  }

  function colorPct(p, text) {
    if (!USE_COLOR) return text;
    const c = p >= 85 ? C.green : p >= 60 ? C.yellow : C.red;
    return c + text + C.reset;
  }

  function pad(s, w, align) {
    s = String(s);
    const visible = s.replace(/\x1b\[[0-9;]*m/g, '');
    const gap = Math.max(0, w - visible.length);
    return align === 'right' ? ' '.repeat(gap) + s : s + ' '.repeat(gap);
  }

  /** Solid horizontal efficiency bar with a dotted remainder. */
  function bar(pct, width) {
    const filled = Math.round((clamp(pct, 0, 100) / 100) * width);
    const solid = '█'.repeat(filled);
    const rest = '░'.repeat(Math.max(0, width - filled));
    return (USE_COLOR ? C.green : '') + solid + (USE_COLOR ? C.gray : '') + rest + C.reset;
  }

  /** Proportional impact bar (share of total saved). */
  function impactBar(sharePct, width) {
    const n = Math.max(1, Math.round((clamp(sharePct, 0, 100) / 100) * width));
    return (USE_COLOR ? C.cyan : '') + '█'.repeat(n) + C.reset;
  }

  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
  const rule = (w) => C.gray + '─'.repeat(w) + C.reset;

  // ── views ──────────────────────────────────────────────────────────────
  /**
   * Render the global summary + by-operation table.
   * @param {object} agg     output of aggregate()
   * @param {object} [opts]  { version, scope }
   * @returns {string}
   */
  function renderSummary(agg, opts = {}) {
    const W = 74;
    const t = agg.totals;
    const L = [];
    const scope = opts.scope || 'this repo';
    const ver = opts.version ? `v${opts.version}` : '';

    L.push('');
    const heading = `⚡ SigMap — Token Savings (${scope})`;
    L.push('  ' + C.bold + C.cyan + heading + C.reset +
      pad(`${C.green}✓${C.reset} ${C.dim}${ver}${C.reset}`, W - heading.length, 'right'));
    L.push('  ' + rule(W));
    L.push('');

    if (t.count === 0) {
      L.push(`  ${C.yellow}No usage recorded yet.${C.reset}`);
      L.push(`  ${C.dim}Run a few queries (sigmap ask "...") or seed a demo:${C.reset}`);
      L.push(`  ${C.dim}  node scripts/gain.mjs --demo${C.reset}`);
      L.push('');
      return L.join('\n');
    }

    const label = (k) => C.dim + pad(k, 21) + C.reset;
    L.push('  ' + label('Total operations') + ': ' + C.bold + fmtInt(t.count) + C.reset);
    L.push('  ' + label('Whole-file baseline') + ': ' + humanTokens(t.baseline) + ' tok' +
      `   ${C.dim}← est. cost of feeding full files${C.reset}`);
    L.push('  ' + label('SigMap context') + ': ' + humanTokens(t.actual) + ' tok');
    L.push('  ' + label('Tokens saved') + ': ' + C.bold + humanTokens(t.saved) + C.reset +
      '  (' + colorPct(t.savedPct, fmtPct(t.savedPct)) + ')');
    L.push('  ' + label('Est. money saved') + ': ' + C.bold + C.green + fmtUSD(t.usdSaved) + C.reset +
      `   ${C.dim}(${agg.price.model} input @ $${agg.price.perMtok}/M · --model to change)${C.reset}`);
    L.push('  ' + label('Avg latency') + ': ' + fmtDuration(t.avgMs) + ' / op' +
      `   ${C.dim}(local, no API round-trip)${C.reset}`);
    L.push('');
    L.push('  ' + label('Efficiency') + ': ▕' + bar(t.savedPct, 30) + '▏  ' +
      colorPct(t.savedPct, C.bold + fmtPct(t.savedPct) + C.reset));
    L.push('');

    // By operation table
    L.push(`  ${C.bold}By operation${C.reset}`);
    L.push('  ' + rule(W));
    L.push('  ' + C.dim +
      pad('#', 3) + pad('Operation', 24) + pad('Count', 8, 'right') +
      pad('Saved', 9, 'right') + pad('Avg%', 8, 'right') + pad('Time', 8, 'right') +
      '  Impact' + C.reset);
    agg.byOp.forEach((o, i) => {
      L.push('  ' +
        pad(`${i + 1}.`, 3) +
        pad(o.op, 24) +
        pad(fmtInt(o.count), 8, 'right') +
        pad(humanTokens(o.saved), 9, 'right') +
        pad(colorPct(o.avgPct, fmtPct(o.avgPct)), 8, 'right') +
        pad(fmtDuration(o.avgMs), 8, 'right') +
        '  ' + impactBar(o.sharePct, 12));
    });
    L.push('  ' + rule(W));
    L.push(`  ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
    L.push('');
    return L.join('\n');
  }

  /**
   * Render daily / weekly / monthly trend tables.
   * @param {object} agg
   * @returns {string}
   */
  function renderBreakdown(agg) {
    const L = [];
    if (agg.totals.count === 0) return renderSummary(agg);

    const section = (icon, title, rows, keyHeader) => {
      L.push('');
      L.push(`  ${C.bold}${icon} ${title}${C.reset}`);
      L.push('  ' + C.dim +
        pad(keyHeader, 14) + pad('Ops', 7, 'right') + pad('Baseline', 11, 'right') +
        pad('Actual', 10, 'right') + pad('Saved', 10, 'right') + pad('Save%', 8, 'right') +
        pad('Time', 8, 'right') + C.reset);
      let TB = 0, TA = 0, TS = 0, TC = 0, TM = 0;
      for (const r of rows) {
        TB += r.baseline; TA += r.actual; TS += r.saved; TC += r.count; TM += r.ms;
        L.push('  ' +
          pad(r.key, 14) + pad(fmtInt(r.count), 7, 'right') +
          pad(humanTokens(r.baseline), 11, 'right') + pad(humanTokens(r.actual), 10, 'right') +
          pad(humanTokens(r.saved), 10, 'right') +
          pad(colorPct(r.savedPct, fmtPct(r.savedPct)), 8, 'right') +
          pad(fmtDuration(r.ms), 8, 'right'));
      }
      const tp = TB > 0 ? (TS / TB) * 100 : 0;
      L.push('  ' + C.bold +
        pad('TOTAL', 14) + pad(fmtInt(TC), 7, 'right') +
        pad(humanTokens(TB), 11, 'right') + pad(humanTokens(TA), 10, 'right') +
        pad(humanTokens(TS), 10, 'right') + pad(fmtPct(tp), 8, 'right') +
        pad(fmtDuration(TM), 8, 'right') + C.reset);
    };

    const daily = agg.buckets.daily.slice(-30);
    section('📅', `Daily (last ${daily.length})`, daily, 'Date');
    const weekly = agg.buckets.weekly.slice(-6);
    section('📆', `Weekly (last ${weekly.length})`, weekly, 'Week of');
    const monthly = agg.buckets.monthly.slice(-3);
    section('🗓️', `Monthly (last ${monthly.length})`, monthly, 'Month');
    L.push('');
    L.push(`  ${C.dim}saved = baseline − actual · estimated vs whole-file reads · local-only${C.reset}`);
    L.push('');
    return L.join('\n');
  }

  module.exports = {
    renderSummary,
    renderBreakdown,
    // exported for tests
    humanTokens, fmtUSD, fmtDuration, fmtPct,
  };
  
};

// ── ./src/format/llm-txt ──
__factories["./src/format/llm-txt"] = function(module, exports) {
  
  const path = require('path');
  module.exports = { format, outputPath };

  function outputPath(cwd) { return path.join(cwd, 'llm.txt'); }

  function format(context, cwd, version) {
    const name  = context.projectName || path.basename(cwd);
    const langs = [...new Set((context.fileEntries || []).map(f => f.language).filter(Boolean))];
    const mods  = context.srcDirs || [];

    return [
      `# Project: ${name}`,
      `Languages: ${langs.join(', ') || 'unknown'}`,
      `Root: ${mods[0] || 'src/'}`,
      '',
      '## Modules',
      ...mods.map(m => `- ${m}/`),
      '',
      '## Key flows',
      '- <!-- describe your main user flows here -->',
      '',
      '## Rules',
      '- <!-- describe your team conventions here -->',
      '',
      `Generated: ${new Date().toISOString()} | SigMap v${version}`,
    ].join('\n');
  }
  
};

// ── ./src/format/llms-txt ──
__factories["./src/format/llms-txt"] = function(module, exports) {
  
  const path = require('path');
  const fs   = require('fs');
  const { tryGit } = __require('./src/util/git');
  module.exports = { format, outputPath };

  function outputPath(cwd) { return path.join(cwd, 'llms.txt'); }

  function getShortCommit(cwd) {
    return tryGit(['rev-parse', '--short', 'HEAD'], { cwd, timeout: 2000 });
  }

  function detectVersion(cwd) {
    try {
      const pkg = path.join(cwd, 'package.json');
      if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf8')).version || '';
    } catch (_) {}
    return '';
  }

  function format(context, cwd, writtenFiles, sigmapVersion) {
    writtenFiles  = writtenFiles  || [];
    sigmapVersion = sigmapVersion || '';

    const name   = context.projectName || path.basename(cwd);
    const ver    = detectVersion(cwd);
    const commit = getShortCommit(cwd);
    const langs  = [...new Set((context.fileEntries || []).map(f => f.language).filter(Boolean))];

    const lines = [
      '# SigMap Context Index',
      `> Generated by SigMap v${sigmapVersion} — the deterministic, verifiable grounding layer for AI code work`,
      '',
      '## Project',
      `- Name: ${name}`,
    ];
    if (ver)          lines.push(`- Version: ${ver}`);
    if (langs.length) lines.push(`- Language: ${langs.join(', ')}`);
    if (commit)       lines.push(`- Commit: ${commit}`);

    if (writtenFiles.length) {
      lines.push('', '## Context Files');
      for (const f of writtenFiles) {
        const rel   = path.relative(cwd, f.path);
        const extra = f.tokens ? `: ${f.tokens} tokens` : '';
        lines.push(`- [${f.label || rel}](${rel})${extra}`);
      }
    }

    const mods = context.srcDirs || [];
    if (mods.length) {
      lines.push('', '## Source Modules');
      for (const m of mods) lines.push(`- [${m}/](${m}/)`);
    }

    const top5 = (context.fileEntries || [])
      .sort((a, b) => (b.sigs || []).length - (a.sigs || []).length)
      .slice(0, 5);
    if (top5.length) {
      lines.push('', '## Key Files');
      for (const f of top5) {
        const rel     = path.relative(cwd, f.filePath);
        const preview = (f.sigs || []).slice(0, 3).join(', ');
        lines.push(`- ${rel}: ${preview}`);
      }
    }

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

// ── ./src/format/usage-guidance ──
__factories["./src/format/usage-guidance"] = function(module, exports) {
  
  /**
   * Canonical "how to use SigMap" guidance block (v6.16/v7.0).
   *
   * Every adapter emits this one identical block so all generated context files
   * (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, GEMINI.md, .cursorrules,
   * …) carry the same, single usage section — instead of each adapter inventing
   * its own wording (and codex emitting a redundant second JSON block).
   */

  function usageBlock() {
    return [
      '## SigMap commands',
      '',
      '| When | Command |',
      '|------|---------|',
      '| Before answering a question about code | `sigmap ask "<your question>"` |',
      '| To rank files by topic | `sigmap --query "<topic>"` |',
      '| After changing config or source dirs | `sigmap validate` |',
      '| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
      '',
      'Always run `sigmap ask` (or `sigmap --query`) before searching for files relevant to a task.',
      '',
    ].join('\n');
  }

  module.exports = { usageBlock };
  
};

// ── ./src/format/verify-report ──
__factories["./src/format/verify-report"] = function(module, exports) {
  
  /**
   * Hallucination Guard report view (Surface A, v6.15.0).
   *
   * Turns the `verify-ai-output --json` result into a standalone, self-contained
   * HTML report — red/amber/green per issue, with closest-match suggestions
   * inline. The visual language deliberately mirrors the planned PR-comment
   * styling so a single screenshot is reusable across docs and CI (plan proof #5).
   *
   * Zero dependencies, inline CSS/SVG, no external assets. Also exports a compact
   * Markdown renderer for CI / PR comments that shares the same structure.
   */

  const TYPE_META = {
    'fake-file': { label: 'Fake file', tone: 'red', icon: '✕' },
    'fake-test-file': { label: 'Fake test file', tone: 'red', icon: '✕' },
    'fake-import': { label: 'Fake import', tone: 'red', icon: '✕' },
    'fake-npm-script': { label: 'Fake npm script', tone: 'red', icon: '✕' },
    'fake-symbol': { label: 'Fake symbol', tone: 'amber', icon: '!' },
  };

  function escapeHtml(value) {
    return String(value == null ? '' : value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function toneFor(issue) {
    const meta = TYPE_META[issue.type];
    if (meta) return meta.tone;
    return issue.confidence === 'high' ? 'red' : 'amber';
  }

  function labelFor(issue) {
    return (TYPE_META[issue.type] && TYPE_META[issue.type].label) || issue.type;
  }

  /**
   * Render the verify result to a full HTML document.
   * @param {{ file?: string, issues: object[], summary: object }} result
   * @param {object} [opts]
   * @param {string} [opts.title]
   * @returns {string} HTML
   */
  function renderReportHtml(result, opts = {}) {
    const issues = Array.isArray(result.issues) ? result.issues : [];
    const summary = result.summary || { total: issues.length, byType: {}, clean: issues.length === 0 };
    const file = result.file || opts.file || 'AI answer';
    const title = opts.title || 'SigMap — Hallucination Guard report';
    const clean = summary.clean || issues.length === 0;
    const byType = summary.byType || {};

    const chips = Object.keys(TYPE_META)
      .filter((t) => byType[t])
      .map((t) => `<span class="chip chip-${TYPE_META[t].tone}">${escapeHtml(TYPE_META[t].label)}: ${byType[t]}</span>`)
      .join('');

    const banner = clean
      ? `<div class="banner banner-green"><span class="dot"></span> No hallucinations detected — ${escapeHtml(String(summary.symbolsIndexed || 0))} symbols indexed</div>`
      : `<div class="banner banner-red"><span class="dot"></span> ${issues.length} issue${issues.length === 1 ? '' : 's'} found in <code>${escapeHtml(file)}</code></div>`;

    const rows = issues.map((issue) => {
      const tone = toneFor(issue);
      const sugg = issue.suggestion
        ? `<div class="suggestion">↳ ${escapeHtml(issue.suggestion)} <span class="conf">heuristic</span></div>`
        : '';
      return [
        `<li class="issue issue-${tone}">`,
        `  <div class="issue-head">`,
        `    <span class="badge badge-${tone}">${escapeHtml(labelFor(issue))}</span>`,
        `    <span class="loc">${escapeHtml(issue.location || ('L' + issue.line))}</span>`,
        `    <span class="conf conf-${escapeHtml(issue.confidence || 'high')}">${escapeHtml(issue.confidence || 'high')} confidence</span>`,
        `  </div>`,
        `  <div class="msg">${escapeHtml(issue.message || issue.value)}</div>`,
        `  ${sugg}`,
        `</li>`,
      ].join('\n');
    }).join('\n');

    const list = clean
      ? '<p class="empty">Nothing to report — every file, import, symbol, and script in the answer resolves against the repository.</p>'
      : `<ul class="issues">${rows}</ul>`;

    return `<!DOCTYPE html>
  <html lang="en">
  <head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${escapeHtml(title)}</title>
  <style>
    :root { color-scheme: light dark; }
    * { box-sizing: border-box; }
    body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; }
    .wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
    h1 { font-size: 20px; margin: 0 0 4px; }
    .sub { color: #8b949e; margin: 0 0 20px; font-size: 13px; }
    code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #161b22; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; }
    .banner { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-radius: 8px; font-weight: 600; margin-bottom: 16px; }
    .banner-green { background: rgba(46,160,67,.15); border: 1px solid rgba(46,160,67,.4); color: #3fb950; }
    .banner-red { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.4); color: #f85149; }
    .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
    .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
    .chip { font-size: 12px; padding: 3px 9px; border-radius: 999px; font-weight: 600; }
    .chip-red { background: rgba(248,81,73,.15); color: #f85149; }
    .chip-amber { background: rgba(210,153,34,.18); color: #d29922; }
    ul.issues { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
    .issue { border: 1px solid #30363d; border-left-width: 4px; border-radius: 8px; padding: 12px 14px; background: #0f141a; }
    .issue-red { border-left-color: #f85149; }
    .issue-amber { border-left-color: #d29922; }
    .issue-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
    .badge { font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; }
    .badge-red { background: rgba(248,81,73,.18); color: #f85149; }
    .badge-amber { background: rgba(210,153,34,.2); color: #d29922; }
    .loc { font-family: ui-monospace, monospace; color: #8b949e; font-size: 12px; }
    .conf { font-size: 11px; color: #8b949e; }
    .conf-high { color: #f85149; }
    .conf-medium { color: #d29922; }
    .msg { margin-top: 7px; font-family: ui-monospace, monospace; font-size: 12.5px; color: #e6edf3; }
    .suggestion { margin-top: 6px; font-size: 12.5px; color: #3fb950; }
    .suggestion .conf { margin-left: 6px; }
    .empty { color: #8b949e; }
    footer { margin-top: 28px; color: #6e7681; font-size: 12px; }
    a { color: #58a6ff; }
  </style>
  </head>
  <body>
  <div class="wrap">
    <h1>Hallucination Guard report</h1>
    <p class="sub">Deterministic verification of an AI answer against the real repository — <code>sigmap verify-ai-output</code></p>
    ${banner}
    ${chips ? `<div class="chips">${chips}</div>` : ''}
    ${list}
    <footer>Generated by <a href="https://github.com/manojmallick/sigmap">SigMap</a> · offline, no LLM · suggestions are heuristic (closest-match)</footer>
  </div>
  </body>
  </html>
  `;
  }

  /** Compact Markdown rendering of the same result (CI / PR comments). */
  function renderReportMarkdown(result) {
    const issues = Array.isArray(result.issues) ? result.issues : [];
    const summary = result.summary || {};
    const file = result.file || 'AI answer';
    if (summary.clean || issues.length === 0) {
      return `### ✅ Hallucination Guard — clean\n\nNo fabricated files, imports, symbols, or scripts in \`${file}\`.`;
    }
    const lines = [
      `### ❌ Hallucination Guard — ${issues.length} issue${issues.length === 1 ? '' : 's'} in \`${file}\``,
      '',
      '| Type | Location | Detail | Suggestion |',
      '| --- | --- | --- | --- |',
    ];
    for (const i of issues) {
      const sugg = i.suggestion ? i.suggestion.replace(/\|/g, '\\|') : '—';
      lines.push(`| ${labelFor(i)} | ${i.location || ('L' + i.line)} | \`${String(i.value).replace(/\|/g, '\\|')}\` | ${sugg} |`);
    }
    return lines.join('\n');
  }

  module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
  
};

// ── ./src/graph/builder ──
__factories["./src/graph/builder"] = function(module, exports) {
  
  /**
   * Dependency graph builder (v2.5).
   *
   * Builds a forward and reverse dependency graph by resolving import/require
   * statements across JS/TS, Python, Go, Rust, Java, Kotlin, and Ruby files.
   *
   * @module src/graph/builder
   */

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

  // Normalize paths for cross-platform consistency (Windows uses backslashes, Unix uses forward slashes)
  // Use lowercase to enable case-insensitive lookups on case-sensitive Windows filesystems
  function normalizePath(p) {
    return path.normalize(p).toLowerCase();
  }

  // ---------------------------------------------------------------------------
  // Language-specific import extractors
  // ---------------------------------------------------------------------------

  const JS_EXTS  = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
  const PY_EXTS  = new Set(['.py', '.pyw']);
  const GO_EXTS  = new Set(['.go']);
  const RS_EXTS  = new Set(['.rs']);
  const JVM_EXTS = new Set(['.java', '.kt', '.kts', '.scala', '.sc']);
  const RB_EXTS  = new Set(['.rb', '.rake']);
  const R_EXTS   = new Set(['.r', '.R']);

  /**
   * Resolve a JS/TS relative import string to an absolute path in fileSet.
   * @param {string} dir - directory of the importing file
   * @param {string} importStr - raw import string (e.g. './utils', '../store')
   * @param {Set<string>} fileSet
   * @returns {string|null}
   */
  function resolveJsPath(dir, importStr, fileSet) {
    const base = path.resolve(dir, importStr);
    const candidates = [
      base,
      base + '.ts', base + '.tsx',
      base + '.js', base + '.jsx', base + '.mjs', base + '.cjs',
      path.join(base, 'index.ts'),
      path.join(base, 'index.js'),
    ];
    for (const c of candidates) {
      const normC = normalizePath(c);
      if (fileSet.has(normC)) return normC;
    }
    return null;
  }

  /**
   * Resolve an R `source(...)` argument to an absolute path in fileSet.
   * Tries the dir-relative path first, then a cwd-relative path so that
   * `source("R/helpers.R")` resolves from the project root.
   */
  function escapeRegex(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function resolveRPath(dir, importStr, fileSet, cwd) {
    const tried = new Set();
    const bases = [path.resolve(dir, importStr)];
    if (cwd) bases.push(path.resolve(cwd, importStr));
    for (const base of bases) {
      for (const c of [base, base + '.R', base + '.r']) {
        const normC = normalizePath(c);
        if (tried.has(normC)) continue;
        tried.add(normC);
        // Check both original and normalized paths (tests may pass non-normalized fileSet)
        if (fileSet.has(c)) return c;
        if (fileSet.has(normC)) return normC;
      }
    }
    return null;
  }

  /**
   * Extract absolute dependency paths from a single file.
   * @param {string} filePath - absolute path to the file
   * @param {string} content  - file source content
   * @param {Set<string>} fileSet - set of all known absolute file paths
   * @param {string}  [cwd]   - project root, used to resolve R `source("R/...")` calls
   * @param {{ rPackage?: string, rLocalDefs?: Map<string,string> }} [ctx]
   *        Optional cross-file context. When present and the file is R, a
   *        `localPkg::fn` reference (where `localPkg` matches `rPackage`) is
   *        resolved to the file in `rLocalDefs` that defines `fn`.
   * @returns {string[]} resolved absolute paths this file imports
   */
  function extractFileDeps(filePath, content, fileSet, cwd, ctx) {
    const ext = path.extname(filePath).toLowerCase();
    const dir = path.dirname(filePath);
    const found = [];

    // ── JS / TS ───────────────────────────────────────────────────────────────
    if (JS_EXTS.has(ext)) {
      const stripped = content
        .replace(/\/\/.*$/gm, '')
        .replace(/\/\*[\s\S]*?\*\//g, '');

      // ES imports:   import ... from './foo'  or  import './side-effect'
      const reEs = /(?:^|[\r\n])\s*import\s+(?:[^'";\r\n]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
      let m;
      while ((m = reEs.exec(stripped)) !== null) {
        const r = resolveJsPath(dir, m[1], fileSet);
        if (r) found.push(r);
      }
      // CommonJS: __require('./src/graph/foo')
      const reCjs = /\brequire\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
      while ((m = reCjs.exec(stripped)) !== null) {
        const r = resolveJsPath(dir, m[1], fileSet);
        if (r) found.push(r);
      }
    }

    // ── Python ────────────────────────────────────────────────────────────────
    if (PY_EXTS.has(ext)) {
      // from .module import ...  /  from ..pkg import ...
      const re = /^[ \t]*from\s+(\.+[\w.]*)\s+import/gm;
      let m;
      while ((m = re.exec(content)) !== null) {
        const dotCount = (m[1].match(/^\.+/) || [''])[0].length;
        const modPart  = m[1].slice(dotCount).replace(/\./g, '/');
        let base = dir;
        for (let i = 1; i < dotCount; i++) base = path.dirname(base);
        const candidate = modPart
          ? path.join(base, modPart + '.py')
          : null;
        if (candidate) {
          const normC = normalizePath(candidate);
          if (fileSet.has(normC)) found.push(normC);
        }
      }

      // Absolute imports: from package.module import ... (infer from project structure)
      const reAbs = /^[ \t]*from\s+([\w.]+)\s+import/gm;
      while ((m = reAbs.exec(content)) !== null) {
        const modulePath = m[1].replace(/\./g, '/');
        const candidates = [
          path.join(dir, modulePath + '.py'),
          path.join(dir, modulePath, '__init__.py'),
          path.resolve(dir, '..', modulePath + '.py'),
          path.resolve(dir, '..', modulePath, '__init__.py'),
        ];
        for (const c of candidates) {
          const normC = normalizePath(c);
          if (fileSet.has(normC)) {
            found.push(normC);
            break;
          }
        }
      }
    }

    // ── Go ────────────────────────────────────────────────────────────────────
    // Go uses module paths, not relative file paths — we match same-module paths
    // by checking if any known file's relative path matches the imported suffix.
    if (GO_EXTS.has(ext)) {
      const re = /import\s*\(\s*([\s\S]*?)\s*\)/g;
      const reInline = /import\s+"([^"]+)"/g;
      const imports = [];
      let m;
      while ((m = re.exec(content)) !== null) {
        for (const imp of m[1].matchAll(/"([^"]+)"/g)) imports.push(imp[1]);
      }
      while ((m = reInline.exec(content)) !== null) imports.push(m[1]);

      for (const imp of imports) {
        const suffix = imp.split('/').pop();
        for (const f of fileSet) {
          const normF = normalizePath(f);
          if (normF.endsWith(path.sep + suffix + '.go') ||
              normF.includes(path.sep + suffix + path.sep)) {
            found.push(normF);
            break;
          }
        }
      }
    }

    // ── Rust ──────────────────────────────────────────────────────────────────
    // Match `mod foo;` and `use crate::foo::bar` — resolve to sibling .rs files
    if (RS_EXTS.has(ext)) {
      const reMod = /^\s*(?:pub\s+)?mod\s+(\w+)\s*;/gm;
      let m;
      while ((m = reMod.exec(content)) !== null) {
        const candidate = path.join(dir, m[1] + '.rs');
        const normC = normalizePath(candidate);
        if (fileSet.has(normC)) found.push(normC);
        // Also try mod/mod.rs
        const candidate2 = path.join(dir, m[1], 'mod.rs');
        const normC2 = normalizePath(candidate2);
        if (fileSet.has(normC2)) found.push(normC2);
      }
    }

    // ── Java / Kotlin / Scala ─────────────────────────────────────────────────
    // Match same-project import statements by matching package-relative paths
    if (JVM_EXTS.has(ext)) {
      const re = /^\s*import\s+([\w.]+)\s*;?/gm;
      let m;
      while ((m = re.exec(content)) !== null) {
        // Convert com.example.utils.StringHelper → com/example/utils/StringHelper.java
        const asPath = m[1].replace(/\./g, path.sep);
        for (const jvmExt of ['.java', '.kt', '.kts', '.scala', '.sc']) {
          for (const f of fileSet) {
            const normF = normalizePath(f);
            if (normF.endsWith(normalizePath(asPath + jvmExt))) { found.push(normF); break; }
          }
        }
      }
    }

    // ── Ruby ──────────────────────────────────────────────────────────────────
    if (RB_EXTS.has(ext)) {
      const re = /^\s*require_relative\s+['"]([^'"]+)['"]/gm;
      let m;
      while ((m = re.exec(content)) !== null) {
        const base = path.resolve(dir, m[1]);
        const candidate  = base.endsWith('.rb') ? base : base + '.rb';
        const normC = normalizePath(candidate);
        if (fileSet.has(normC)) found.push(normC);
      }
    }

    // ── R ─────────────────────────────────────────────────────────────────────
    // R doesn't have JS-style relative imports inside packages — files in R/ are
    // auto-sourced in alphabetical order. We emit edges for:
    //   1. Explicit `source("path/file.R")` calls (common in Shiny / scripts).
    //   2. `localPkg::fn` references where `localPkg` matches the project's
    //      own DESCRIPTION#Package — resolved via the symbol→file map in ctx.
    // `library(pkg)` / external `pkg::fn` calls are not graph edges.
    if (R_EXTS.has(ext)) {
      const stripped = content.replace(/#.*$/gm, '');
      const reSrc = /(?:^|[^\w.])source\s*\(\s*["']([^"']+)["']/g;
      let m;
      while ((m = reSrc.exec(stripped)) !== null) {
        const r = resolveRPath(dir, m[1], fileSet, cwd);
        if (r) found.push(r);
      }
      if (ctx && ctx.rPackage && ctx.rLocalDefs && ctx.rLocalDefs.size > 0) {
        const pkg = ctx.rPackage;
        // Match `pkg::fn` or `pkg:::fn`. The `::` form needs to be the local
        // package — references to other packages are external.
        const reNs = new RegExp(`\\b${escapeRegex(pkg)}:::?([A-Za-z][\\w.]*)`, 'g');
        while ((m = reNs.exec(stripped)) !== null) {
          const target = ctx.rLocalDefs.get(m[1]);
          if (!target) continue;
          const normTarget = normalizePath(target);
          const normFilePath = normalizePath(filePath);
          if (normTarget === normFilePath) continue;
          // Check both original and normalized paths (tests may pass non-normalized fileSet)
          if (fileSet.has(target)) {
            found.push(target);
          } else if (fileSet.has(normTarget)) {
            found.push(normTarget);
          }
        }
      }
    }

    return [...new Set(found)];
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  /**
   * Build a forward and reverse dependency graph for all given files.
   *
   * @param {string[]} files - absolute file paths to analyze
   * @param {string}   cwd   - project root (used only for error reporting)
   * @param {{ rPackage?: string, rLocalDefs?: Map<string,string> }} [ctx]
   *        Optional cross-file context for namespace-aware resolution. Built
   *        automatically by `buildFromCwd` when DESCRIPTION + NAMESPACE exist.
   * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
   */
  function build(files, cwd, ctx) {
    const fileSet = new Set(files.map((f) => path.resolve(f)));
    // Create a normalized version for cross-platform case-insensitive lookups
    const fileSetNormalized = new Set([...fileSet].map(normalizePath));
    const forward = new Map();
    const reverse = new Map();

    // Initialise every known file in both maps (ensures isolated files appear)
    // Store using normalized paths for Windows compatibility
    for (const f of fileSet) {
      const normF = normalizePath(f);
      if (!forward.has(normF)) forward.set(normF, []);
      if (!reverse.has(normF)) reverse.set(normF, []);
    }

    for (const filePath of fileSet) {
      let content;
      try {
        content = fs.readFileSync(filePath, 'utf8');
      } catch (_) {
        continue;
      }

      const normFilePath = normalizePath(filePath);
      const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd, ctx);
      if (deps.length > 0) {
        forward.set(normFilePath, deps);
        for (const dep of deps) {
          if (!reverse.has(dep)) reverse.set(dep, []);
          reverse.get(dep).push(normFilePath);
        }
      }
    }

    return { forward, reverse };
  }

  /**
   * Build a dependency graph scoped to a single cwd by walking all JS/TS/Py/Go
   * files under srcDirs. Useful for the MCP tool handler.
   *
   * @param {string} cwd
   * @param {object} [opts]
   * @param {string[]} [opts.srcDirs]
   * @param {string[]} [opts.exclude]
   * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
   */
  function buildFromCwd(cwd, opts) {
    // R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
    // The existence check below makes these no-ops in non-R projects.
    const { srcDirs = ['src', 'app', 'lib', 'R', 'inst'], exclude = ['node_modules', '.git', 'dist', 'build'] } = opts || {};
    const excludeSet = new Set(exclude);

    function walkDir(dir, depth) {
      if (depth > 8) return [];
      let entries;
      try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return []; }
      const out = [];
      for (const e of entries) {
        if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
        const full = path.join(dir, e.name);
        if (e.isDirectory()) {
          out.push(...walkDir(full, depth + 1));
        } else if (e.isFile()) {
          const ext = path.extname(e.name).toLowerCase();
          if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || GO_EXTS.has(ext) ||
              RS_EXTS.has(ext) || JVM_EXTS.has(ext) || RB_EXTS.has(ext) ||
              R_EXTS.has(ext)) {
            out.push(full);
          }
        }
      }
      return out;
    }

    const files = [];
    for (const sd of srcDirs) {
      const absDir = path.resolve(cwd, sd);
      if (fs.existsSync(absDir)) files.push(...walkDir(absDir, 0));
    }
    // Also include root-level entry files (R: app.R/server.R/ui.R/global.R for Shiny)
    for (const rootFile of ['gen-context.js', 'index.js', 'main.js', 'app.js',
                            'app.R', 'server.R', 'ui.R', 'global.R']) {
      const abs = path.resolve(cwd, rootFile);
      if (fs.existsSync(abs)) files.push(abs);
    }

    // Build R namespace context if this looks like an R package.
    let ctx;
    try {
      const { readDescription, collectLocalDefs } = __require('./src/discovery/r-manifest');
      const desc = readDescription(cwd);
      if (desc && desc.package) {
        const rFiles = files.filter((f) => R_EXTS.has(path.extname(f).toLowerCase()));
        if (rFiles.length > 0) {
          ctx = { rPackage: desc.package, rLocalDefs: collectLocalDefs(rFiles) };
        }
      }
    } catch (_) { /* manifest module missing or read failed — proceed without ctx */ }

    return build(files, cwd, ctx);
  }

  module.exports = { build, buildFromCwd, extractFileDeps, normalizePath };
  
};

// ── ./src/graph/call-graph ──
__factories["./src/graph/call-graph"] = function(module, exports) {
  
  /**
   * Method/caller-level call-graph (D4 v1).
   *
   * Builds symbol-level edges — which function calls which function — for JS/TS
   * and Python. Deterministic, zero-dependency, regex + brace/indent matching.
   * Call sites are resolved with high precision: a call resolves to a definition
   * of that name in the *same file* first, then in a *directly-imported* file
   * (via the existing file-level import graph). Names that resolve to no repo
   * definition produce no edge — over-approximation noise is avoided.
   *
   * Symbol IDs are `relPath#symbolName` (forward-slashed, relative to cwd).
   *
   * @module src/graph/call-graph
   */

  const fs = require('fs');
  const path = require('path');
  const { build } = __require('./src/graph/builder');

  const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
  const PY_EXTS = new Set(['.py', '.pyw']);

  // Tokens that look like `name(` calls or definition headers but are language
  // keywords, not user symbols — never treated as a call or a definition.
  const NON_CALL = new Set([
    'if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'typeof',
    'await', 'new', 'super', 'else', 'do', 'with', 'yield', 'void', 'delete',
    'in', 'of', 'case', 'throw', 'print', 'and', 'or', 'not', 'assert',
    'lambda', 'class', 'def', 'elif', 'except', 'finally', 'raise', 'import',
    'from', 'global', 'nonlocal', 'del', 'pass', 'async', 'require', 'constructor',
  ]);

  function normalizePath(p) { return path.normalize(p).toLowerCase(); }
  function toRel(cwd, f) { return path.relative(cwd, f).replace(/\\/g, '/'); }
  function symId(cwd, absFile, name) { return `${toRel(cwd, absFile)}#${name}`; }

  // ── Length- and newline-preserving maskers ──────────────────────────────────
  // Replace comment / string bodies with spaces so their braces, parens, and
  // call-looking tokens never confuse structure detection. Offsets stay aligned.

  function maskJs(src) {
    const out = src.split('');
    const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
    let i = 0; const n = src.length;
    while (i < n) {
      const c = src[i], d = src[i + 1];
      if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
      if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
      if (c === '"' || c === "'" || c === '`') {
        let j = i + 1;
        while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
        j = Math.min(n, j + 1); blank(i, j); i = j; continue;
      }
      i++;
    }
    return out.join('');
  }

  function maskPy(src) {
    const out = src.split('');
    const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
    let i = 0; const n = src.length;
    while (i < n) {
      const c = src[i];
      if (c === '#') { let j = i + 1; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
      if (c === '"' || c === "'") {
        if (src.substr(i, 3) === c + c + c) {
          let j = i + 3; while (j < n && src.substr(j, 3) !== c + c + c) j++; j = Math.min(n, j + 3); blank(i, j); i = j; continue;
        }
        let j = i + 1; while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c || src[j] === '\n') break; j++; }
        j = Math.min(n, j + 1); blank(i, j); i = j; continue;
      }
      i++;
    }
    return out.join('');
  }

  // ── Balanced-delimiter matchers (operate on masked source) ───────────────────
  function matchDelim(masked, openIdx, open, close) {
    let depth = 0;
    for (let i = openIdx; i < masked.length; i++) {
      if (masked[i] === open) depth++;
      else if (masked[i] === close) { depth--; if (depth === 0) return i; }
    }
    return masked.length - 1;
  }

  function lineAt(src, idx) {
    let line = 1;
    const end = Math.min(idx, src.length);
    for (let i = 0; i < end; i++) if (src.charCodeAt(i) === 10) line++;
    return line;
  }

  // ── Definition extraction ────────────────────────────────────────────────────
  // Each def: { name, line, bodyStart, bodyEnd } with char offsets into `masked`.

  function jsDefs(masked) {
    const defs = [];
    const seen = new Set();
    const push = (name, headerIdx, bodyStart, bodyEnd) => {
      const key = name + ':' + bodyStart;
      if (NON_CALL.has(name) || seen.has(key)) return;
      seen.add(key);
      defs.push({ name, line: lineAt(masked, headerIdx), bodyStart, bodyEnd });
    };

    // Locate the `{` body (or `=>` expression) that follows a param list `)`.
    const bodyAfterParams = (closeParen) => {
      let k = closeParen + 1;
      // skip a `=>`, return-type annotations, and whitespace up to `{` or a statement end
      while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n') k++;
      if (masked[k] === '{') { const end = matchDelim(masked, k, '{', '}'); return { bodyStart: k, bodyEnd: end }; }
      return null; // no braced body (interface/abstract/overload signature) — skip
    };

    // 1) function declarations:  (async) function name(...) { ... }
    const reFn = /\b(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)\s*\(/g;
    let m;
    while ((m = reFn.exec(masked)) !== null) {
      const paren = masked.indexOf('(', m.index + m[0].length - 1);
      const close = matchDelim(masked, paren, '(', ')');
      const body = bodyAfterParams(close);
      if (body) push(m[1], m.index, body.bodyStart, body.bodyEnd);
    }

    // 2) arrow / function expressions:  const name = (...) => { }  |  = function(...) { }
    const reArrow = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b\s*\*?\s*[A-Za-z_$]*\s*)?\(/g;
    while ((m = reArrow.exec(masked)) !== null) {
      const paren = masked.indexOf('(', m.index + m[0].length - 1);
      const close = matchDelim(masked, paren, '(', ')');
      let k = close + 1;
      while (k < masked.length && /\s/.test(masked[k])) k++;
      if (masked[k] === '{') { push(m[1], m.index, k, matchDelim(masked, k, '{', '}')); continue; }
      if (masked[k] === '=' && masked[k + 1] === '>') {
        let j = k + 2; while (j < masked.length && /\s/.test(masked[j])) j++;
        if (masked[j] === '{') push(m[1], m.index, j, matchDelim(masked, j, '{', '}'));
        else { // single-expression arrow body → to end of statement
          let e = j; let d = 0;
          while (e < masked.length) { const ch = masked[e]; if (ch === '(' || ch === '[') d++; else if (ch === ')' || ch === ']') d--; else if ((ch === ';' || ch === '\n') && d <= 0) break; e++; }
          push(m[1], m.index, j, e);
        }
      }
    }

    // 3) class methods:  class X { name(...) { } }
    const reClass = /\bclass\s+[A-Za-z_$][\w$]*/g;
    while ((m = reClass.exec(masked)) !== null) {
      const brace = masked.indexOf('{', m.index);
      if (brace === -1) continue;
      const classEnd = matchDelim(masked, brace, '{', '}');
      const reMethod = /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+|static\s+|readonly\s+|abstract\s+|async\s+|get\s+|set\s+|\*\s*)*([A-Za-z_$][\w$]*)\s*\(/g;
      reMethod.lastIndex = brace;
      let mm;
      while ((mm = reMethod.exec(masked)) !== null && mm.index < classEnd) {
        const paren = masked.indexOf('(', mm.index + mm[0].length - 1);
        const close = matchDelim(masked, paren, '(', ')');
        const body = bodyAfterParams(close);
        if (body && body.bodyEnd <= classEnd) push(mm[1], mm.index, body.bodyStart, body.bodyEnd);
      }
    }

    return defs;
  }

  function pyDefs(masked) {
    const defs = [];
    const lines = masked.split('\n');
    // precompute char offset of each line start
    const offsets = [0];
    for (let i = 0; i < lines.length; i++) offsets.push(offsets[i] + lines[i].length + 1);
    for (let i = 0; i < lines.length; i++) {
      const m = lines[i].match(/^([ \t]*)(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/);
      if (!m) continue;
      const indent = m[1].length;
      let j = i + 1;
      for (; j < lines.length; j++) {
        const ln = lines[j];
        if (!ln.trim()) continue;                       // blank
        const ind = ln.match(/^[ \t]*/)[0].length;
        if (ind <= indent) break;                        // dedent → block ends
      }
      defs.push({ name: m[2], line: i + 1, bodyStart: offsets[i], bodyEnd: offsets[j] || masked.length });
    }
    return defs;
  }

  function extractDefs(filePath, src) {
    const ext = path.extname(filePath).toLowerCase();
    if (JS_EXTS.has(ext)) return jsDefs(maskJs(src));
    if (PY_EXTS.has(ext)) return pyDefs(maskPy(src));
    return null; // unsupported language
  }

  // Collect `name(` call tokens within [start,end) of masked source.
  function callsInRange(masked, start, end) {
    const slice = masked.slice(start, end);
    const names = new Set();
    const re = /([A-Za-z_$][\w$]*)\s*\(/g;
    let m;
    while ((m = re.exec(slice)) !== null) {
      // skip a `.name(` method access (can't resolve the receiver deterministically)
      const before = slice[m.index - 1];
      if (before === '.') continue;
      if (!NON_CALL.has(m[1])) names.add(m[1]);
    }
    return names;
  }

  // ── Public API ───────────────────────────────────────────────────────────────

  function _walk(dir, excludeSet, out, depth) {
    if (depth > 8) return;
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
    for (const e of entries) {
      if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
      const full = path.join(dir, e.name);
      if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
      else if (e.isFile()) {
        const ext = path.extname(e.name).toLowerCase();
        if (JS_EXTS.has(ext) || PY_EXTS.has(ext)) out.push(full);
      }
    }
  }

  /**
   * Build the method-level call-graph for a project.
   *
   * @param {string} cwd
   * @param {object} [opts]
   * @param {string[]} [opts.srcDirs=['src','app','lib']]
   * @param {string[]} [opts.exclude]
   * @param {string[]} [opts.files]  explicit absolute file list (skips the walk)
   * @returns {{
   *   forward: Map<string,string[]>,   // callerId → calleeIds
   *   reverse: Map<string,string[]>,   // calleeId → callerIds
   *   defs:    Map<string,{file:string,name:string,line:number}>
   * }}
   */
  function buildCallGraph(cwd, opts = {}) {
    const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
    let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
    if (!opts.files) {
      for (const sd of (opts.srcDirs || ['src', 'app', 'lib'])) {
        const abs = path.resolve(cwd, sd);
        if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
      }
    }

    // File-level import graph (for precise call-site resolution). Keys normalized.
    let fileGraph;
    try { fileGraph = build(files, cwd); } catch (_) { fileGraph = { forward: new Map() }; }

    // Per-file definitions + name→file lookups.
    const perFileDefs = new Map();   // absFile → def[]
    const defsByName = new Map();     // absFile → Map<name, symbolId[]>
    const normToAbs = new Map();      // normalized abs → abs
    const defs = new Map();           // symbolId → {file,name,line}

    for (const f of files) {
      normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
      let src;
      try { src = fs.readFileSync(f, 'utf8'); } catch (_) { continue; }
      const fileDefs = extractDefs(f, src);
      if (!fileDefs) continue;
      perFileDefs.set(f, fileDefs);
      const byName = new Map();
      for (const d of fileDefs) {
        const id = symId(cwd, f, d.name);
        defs.set(id, { file: toRel(cwd, f), name: d.name, line: d.line });
        if (!byName.has(d.name)) byName.set(d.name, []);
        byName.get(d.name).push(id);
      }
      defsByName.set(f, byName);
    }

    const forward = new Map();
    const reverse = new Map();
    const addEdge = (from, to) => {
      if (from === to) return;
      if (!forward.has(from)) forward.set(from, new Set());
      forward.get(from).add(to);
      if (!reverse.has(to)) reverse.set(to, new Set());
      reverse.get(to).add(from);
    };

    for (const [f, fileDefs] of perFileDefs.entries()) {
      const masked = JS_EXTS.has(path.extname(f).toLowerCase()) ? maskJs(fs.readFileSync(f, 'utf8')) : maskPy(fs.readFileSync(f, 'utf8'));
      // resolution scope: this file's defs, then directly-imported files' defs
      const importedAbs = (fileGraph.forward.get(normalizePath(path.resolve(f))) || [])
        .map((nf) => normToAbs.get(nf)).filter(Boolean);
      for (const d of fileDefs) {
        const callerId = symId(cwd, f, d.name);
        if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
        const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
        for (const nm of callees) {
          const local = (defsByName.get(f) || new Map()).get(nm);
          if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
          for (const imp of importedAbs) {
            const ids = (defsByName.get(imp) || new Map()).get(nm);
            if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
          }
        }
      }
    }

    const toArr = (mapOfSets) => {
      const out = new Map();
      for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
      return out;
    };
    return { forward: toArr(forward), reverse: toArr(reverse), defs };
  }

  // Resolve a user-supplied symbol (bare name or full `file#name` id) to ids.
  function _resolveSymbol(symbol, defs) {
    if (defs.has(symbol)) return [symbol];
    const ids = [];
    for (const id of defs.keys()) if (id.slice(id.indexOf('#') + 1) === symbol) ids.push(id);
    return ids;
  }

  // BFS over a graph map from seed ids up to maxDepth (0 = unlimited).
  function _bfs(seedIds, graph, maxDepth) {
    const direct = new Set();
    const transitive = new Set();
    const visited = new Set(seedIds);
    let frontier = [];
    for (const s of seedIds) for (const nb of (graph.get(s) || [])) if (!visited.has(nb)) { direct.add(nb); visited.add(nb); frontier.push(nb); }
    let depth = 1;
    while (frontier.length && (maxDepth === 0 || depth < maxDepth)) {
      const next = [];
      for (const node of frontier) for (const nb of (graph.get(node) || [])) if (!visited.has(nb)) { transitive.add(nb); visited.add(nb); next.push(nb); }
      frontier = next; depth++;
    }
    return { direct: [...direct], transitive: [...transitive] };
  }

  /**
   * Method-level blast radius: everything that (transitively) calls `symbol`.
   *
   * @param {string} symbol  bare name or `file#name`
   * @param {string} cwd
   * @param {object} [opts]  { depth=0, ...buildCallGraph opts }
   * @returns {{ symbol:string, resolved:string[], direct:string[], transitive:string[], total:number, unresolved:boolean }}
   */
  function methodImpact(symbol, cwd, opts = {}) {
    const graph = opts.graph || buildCallGraph(cwd, opts);
    const ids = _resolveSymbol(symbol, graph.defs);
    if (ids.length === 0) return { symbol, resolved: [], direct: [], transitive: [], total: 0, unresolved: true };
    const { direct, transitive } = _bfs(ids, graph.reverse, opts.depth || 0);
    return { symbol, resolved: ids, direct, transitive, total: direct.length + transitive.length, unresolved: false };
  }

  /**
   * What `symbol` (transitively) calls.
   * @returns {{ symbol:string, resolved:string[], direct:string[], transitive:string[], total:number, unresolved:boolean }}
   */
  function methodCallees(symbol, cwd, opts = {}) {
    const graph = opts.graph || buildCallGraph(cwd, opts);
    const ids = _resolveSymbol(symbol, graph.defs);
    if (ids.length === 0) return { symbol, resolved: [], direct: [], transitive: [], total: 0, unresolved: true };
    const { direct, transitive } = _bfs(ids, graph.forward, opts.depth || 0);
    return { symbol, resolved: ids, direct, transitive, total: direct.length + transitive.length, unresolved: false };
  }

  // ── Formatters ───────────────────────────────────────────────────────────────
  function formatCallGraph(result, kind) {
    const verb = kind === 'callees' ? 'calls' : 'callers of';
    const lines = [`## ${kind === 'callees' ? 'Callees' : 'Callers'}: \`${result.symbol}\``, ''];
    if (result.unresolved) { lines.push('_symbol not found in the call-graph._'); return lines.join('\n'); }
    if (result.total === 0) {
      lines.push(kind === 'callees' ? '_calls no repo-defined symbols._' : '_no repo symbol calls this — zero method blast radius._');
      return lines.join('\n');
    }
    lines.push(`**Total ${verb}:** ${result.total}`, '');
    if (result.direct.length) { lines.push(`### Direct`); for (const id of result.direct) lines.push(`- \`${id}\``); lines.push(''); }
    if (result.transitive.length) { lines.push(`### Transitive`); for (const id of result.transitive) lines.push(`- \`${id}\``); lines.push(''); }
    return lines.join('\n');
  }

  function formatCallGraphJSON(result, kind) {
    return {
      symbol: result.symbol,
      kind: kind === 'callees' ? 'callees' : 'callers',
      resolved: result.resolved,
      direct: result.direct,
      transitive: result.transitive,
      total: result.total,
      unresolved: result.unresolved,
    };
  }

  module.exports = {
    buildCallGraph, methodImpact, methodCallees,
    formatCallGraph, formatCallGraphJSON,
    extractDefs, maskJs, maskPy,
  };
  
};

// ── ./src/graph/impact ──
__factories["./src/graph/impact"] = function(module, exports) {
  
  /**
   * Impact radius calculator (v2.5).
   *
   * Given a changed file and a dependency graph, finds every file that
   * transitively imports it using BFS. Handles circular dependencies safely.
   *
   * @module src/graph/impact
   */

  const path = require('path');
  const { buildFromCwd } = __require('./src/graph/builder');

  // Normalize paths for cross-platform consistency (same as in builder.js)
  function normalizePath(p) {
    return path.normalize(p).toLowerCase();
  }

  // ---------------------------------------------------------------------------
  // Core BFS traversal
  // ---------------------------------------------------------------------------

  /**
   * Walk the reverse graph from `startFile` using BFS up to `maxDepth` levels.
   * Returns separate sets for direct and transitive dependents.
   *
   * @param {string} startFile - absolute path of file that changed
   * @param {Map<string,string[]>} reverseGraph
   * @param {number} maxDepth  - 0 = unlimited
   * @returns {{ direct: Set<string>, transitive: Set<string> }}
   */
  function bfs(startFile, reverseGraph, maxDepth) {
    const direct     = new Set();
    const transitive = new Set();
    const visited    = new Set([startFile]);

    // Level 1 — direct importers
    const firstLevel = reverseGraph.get(startFile) || [];
    for (const f of firstLevel) {
      if (!visited.has(f)) {
        direct.add(f);
        visited.add(f);
      }
    }

    if (maxDepth === 1) return { direct, transitive };

    // BFS for deeper levels
    let frontier = [...direct];
    let depth = 1;

    while (frontier.length > 0 && (maxDepth === 0 || depth < maxDepth)) {
      const nextFrontier = [];
      for (const node of frontier) {
        const importers = reverseGraph.get(node) || [];
        for (const imp of importers) {
          if (!visited.has(imp)) {
            transitive.add(imp);
            visited.add(imp);
            nextFrontier.push(imp);
          }
        }
      }
      frontier = nextFrontier;
      depth++;
    }

    return { direct, transitive };
  }

  // ---------------------------------------------------------------------------
  // Helper: classify impacted files into tests / routes / other
  // ---------------------------------------------------------------------------

  const TEST_PATTERNS = [
    /[./\\](test|tests|spec|__tests__)[./\\]/,
    /\.(test|spec)\.[jt]sx?$/,
    /_test\.[jt]sx?$/,
    /_test\.py$/,
    /test_[^/\\]+\.py$/,
  ];

  const ROUTE_PATTERNS = [
    /router?\.[jt]sx?$/i,
    /routes?\.[jt]sx?$/i,
    /controller\.[jt]sx?$/i,
    /views?\.[jt]sx?$/i,
    /handlers?\.[jt]sx?$/i,
  ];

  function isTestFile(f)  { return TEST_PATTERNS.some((re) => re.test(f.replace(/\\/g, '/'))); }
  function isRouteFile(f) { return ROUTE_PATTERNS.some((re) => re.test(f.replace(/\\/g, '/'))); }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------

  /**
   * Compute the impact of changing `changedFile`.
   *
   * @param {string} changedFile - absolute or cwd-relative path
   * @param {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }} graph
   * @param {object} [opts]
   * @param {number} [opts.depth=0]   - BFS depth limit (0 = unlimited)
   * @param {string} [opts.cwd]       - project root for relative path display
   * @returns {{
   *   changed:     string,
   *   direct:      string[],
   *   transitive:  string[],
   *   tests:       string[],
   *   routes:      string[],
   *   totalImpact: number
   * }}
   */
  function getImpact(changedFile, graph, opts) {
    const { depth = 0, cwd = process.cwd() } = opts || {};

    const absChanged = normalizePath(path.resolve(cwd, changedFile));

    // Bail gracefully if file not in graph
    if (!graph || !graph.reverse) {
      return { changed: changedFile, direct: [], transitive: [], tests: [], routes: [], totalImpact: 0 };
    }

    const { direct, transitive } = bfs(absChanged, graph.reverse, depth);

    const allImpacted = [...direct, ...transitive];
    const tests  = allImpacted.filter(isTestFile);
    const routes = allImpacted.filter(isRouteFile);

    const toRel = (f) => path.relative(cwd, f).replace(/\\/g, '/');

    return {
      changed:     toRel(absChanged),
      direct:      [...direct].map(toRel),
      transitive:  [...transitive].map(toRel),
      tests:       tests.map(toRel),
      routes:      routes.map(toRel),
      totalImpact: direct.size + transitive.size,
    };
  }

  /**
   * Analyse the impact of one or more changed files, building the graph from cwd.
   * This is the high-level convenience function used by the CLI and MCP tool.
   *
   * @param {string|string[]} changedFiles
   * @param {string} cwd
   * @param {object} [opts]
   * @param {number} [opts.depth=3]
   * @param {string[]} [opts.srcDirs]
   * @param {string[]} [opts.exclude]
   * @returns {{ file: string, impact: object }[]}
   */
  function analyzeImpact(changedFiles, cwd, opts) {
    const { depth = 3 } = opts || {};
    const files = Array.isArray(changedFiles) ? changedFiles : [changedFiles];

    let graph;
    try {
      graph = buildFromCwd(cwd, opts);
    } catch (_) {
      graph = { forward: new Map(), reverse: new Map() };
    }

    return files.map((f) => ({
      file: f,
      impact: getImpact(f, graph, { depth, cwd }),
    }));
  }

  // ---------------------------------------------------------------------------
  // Formatting helpers
  // ---------------------------------------------------------------------------

  /**
   * Format an impact result as a readable markdown string.
   *
   * @param {object} result - return value of getImpact()
   * @returns {string}
   */
  function formatImpact(result) {
    const lines = [];
    lines.push(`## Impact: \`${result.changed}\``);
    lines.push('');

    if (result.direct.length === 0 && result.transitive.length === 0) {
      lines.push('_No files import this file — zero blast radius._');
      return lines.join('\n');
    }

    lines.push(`**Total impacted files:** ${result.totalImpact}`);
    lines.push('');

    if (result.direct.length > 0) {
      lines.push('### Direct importers');
      for (const f of result.direct) lines.push(`- \`${f}\``);
      lines.push('');
    }

    if (result.transitive.length > 0) {
      lines.push('### Transitive importers');
      for (const f of result.transitive) lines.push(`- \`${f}\``);
      lines.push('');
    }

    if (result.tests.length > 0) {
      lines.push('### Affected tests');
      for (const f of result.tests) lines.push(`- \`${f}\``);
      lines.push('');
    }

    if (result.routes.length > 0) {
      lines.push('### Affected routes / controllers');
      for (const f of result.routes) lines.push(`- \`${f}\``);
      lines.push('');
    }

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

  /**
   * Format an impact result as a JSON-serialisable object.
   *
   * @param {object} result - return value of getImpact()
   * @returns {object}
   */
  function formatImpactJSON(result) {
    return {
      changed:     result.changed,
      direct:      result.direct,
      transitive:  result.transitive,
      tests:       result.tests,
      routes:      result.routes,
      totalImpact: result.totalImpact,
    };
  }

  module.exports = { getImpact, analyzeImpact, formatImpact, formatImpactJSON };
  
};

// ── ./src/health/scorer ──
__factories["./src/health/scorer"] = function(module, exports) {
  
  /**
   * SigMap health scorer.
   *
   * Computes a composite 0-100 health score for the current project by combining:
   *   1. Days since context file was last regenerated  (staleness penalty ≤ 30 pts)
   *   2. Average token reduction percentage             (low-reduction penalty 20 pts)
   *   3. Over-budget run rate                           (budget penalty 20 pts)
   *
   * Strategy-aware: thresholds adjust based on the active strategy so that
   * hot-cold (90% reduction intentional) is not penalized as 'low reduction'.
   *
   * Grade scale:  A ≥ 90  |  B ≥ 75  |  C ≥ 60  |  D < 60
   *
   * Never throws — returns graceful result with nulls for unavailable metrics.
   *
   * @param {string} cwd - Working directory (root of the project)
   * @returns {{
   *   score: number,
   *   grade: 'A'|'B'|'C'|'D',
   *   strategy: string,
   *   tokenReductionPct: number|null,
   *   daysSinceRegen: number|null,
   *   strategyFreshnessDays: number|null,
   *   totalRuns: number,
   *   overBudgetRuns: number,
   * }}
   */
  function score(cwd) {
    const fs = require('fs');
    const path = require('path');

    let tokenReductionPct = null;
    let daysSinceRegen = null;
    let strategyFreshnessDays = null;
    let overBudgetRuns = 0;
    let totalRuns = 0;
    let p50TokenCount = 0;
    let p95TokenCount = 0;
    let overBudgetStreak = 0;
    let extractorCoverage = 0;

    // ── Detect active strategy ────────────────────────────────────────────────
    let strategy = 'full';
    try {
      const cfgPath = path.join(cwd, 'gen-context.config.json');
      if (fs.existsSync(cfgPath)) {
        const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
        strategy = cfg.strategy || 'full';
      }
    } catch (_) {}

    // ── Read usage log via tracking logger ──────────────────────────────────
    try {
      const { readLog, summarize } = __require('./src/tracking/logger');
      const { percentile, overBudgetStreak: calcOverBudgetStreak } = __require('./src/format/dashboard');
      const entries = readLog(cwd);
      const s = summarize(entries);
      // Only set tokenReductionPct when there is actual history; a brand-new/
      // untracked project should not be penalised for "0% reduction".
      if (s.totalRuns > 0) tokenReductionPct = s.avgReductionPct;
      overBudgetRuns = s.overBudgetRuns;
      totalRuns = s.totalRuns;
      const finals = entries.map((e) => Number(e.finalTokens)).filter(Number.isFinite);
      p50TokenCount = Math.round(percentile(finals, 50));
      p95TokenCount = Math.round(percentile(finals, 95));
      overBudgetStreak = calcOverBudgetStreak(entries);
    } catch (_) {
      // No usage log yet — proceed with nulls
    }

    try {
      const { computeExtractorCoverage } = __require('./src/format/dashboard');
      extractorCoverage = computeExtractorCoverage(cwd).pct;
    } catch (_) {
      extractorCoverage = 0;
    }

    // ── Days since primary context file was last regenerated ─────────────────
    try {
      const ctxFile = path.join(cwd, '.github', 'copilot-instructions.md');
      if (fs.existsSync(ctxFile)) {
        const mtime = fs.statSync(ctxFile).mtimeMs;
        daysSinceRegen = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
      }
    } catch (_) {}

    // ── Strategy freshness: context-cold.md age (hot-cold only) ─────────────
    if (strategy === 'hot-cold') {
      try {
        const coldFile = path.join(cwd, '.github', 'context-cold.md');
        if (fs.existsSync(coldFile)) {
          const mtime = fs.statSync(coldFile).mtimeMs;
          strategyFreshnessDays = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
        }
      } catch (_) {}
    }

    // ── Compute composite score ───────────────────────────────────────────────
    let points = 100;

    // Staleness penalty: -4 pts per day over the 7-day freshness window (max -30)
    if (daysSinceRegen !== null && daysSinceRegen > 7) {
      points -= Math.min(30, Math.floor((daysSinceRegen - 7) * 4));
    }

    // Low-reduction penalty — threshold depends on strategy:
    // - hot-cold: primary output is intentionally tiny; measure cold freshness instead
    // - per-module: per-file budgets; global reduction < 60% expected, no penalty
    // - full: standard 60% threshold
    const reductionThreshold = (strategy === 'full') ? 60 : 0; // disable for hot-cold/per-module
    if (tokenReductionPct !== null && tokenReductionPct < reductionThreshold) {
      points -= 20;
    }

    // hot-cold strategy freshness penalty: context-cold.md older than 1 day (-10 pts)
    if (strategy === 'hot-cold' && strategyFreshnessDays !== null && strategyFreshnessDays > 1) {
      points -= Math.min(10, Math.floor(strategyFreshnessDays - 1) * 3);
    }

    // Over-budget penalty: more than 20% of runs exceeded the token budget (-20)
    if (overBudgetRuns > 0 && totalRuns > 0) {
      const overBudgetRate = (overBudgetRuns / totalRuns) * 100;
      if (overBudgetRate > 20) points -= 20;
    }

    points = Math.max(0, Math.min(100, Math.round(points)));

    let grade;
    if (points >= 90) grade = 'A';
    else if (points >= 75) grade = 'B';
    else if (points >= 60) grade = 'C';
    else grade = 'D';

    return {
      score: points,
      grade,
      strategy,
      tokenReductionPct,
      daysSinceRegen,
      strategyFreshnessDays,
      totalRuns,
      overBudgetRuns,
      p50TokenCount,
      p95TokenCount,
      overBudgetStreak,
      extractorCoverage,
    };
  }

  module.exports = { score };
  
};

// ── ./src/init/creation-workflow ──
__factories["./src/init/creation-workflow"] = function(module, exports) {
  
  /**
   * Creation-workflow CLAUDE.md block (IMPL.md §6.2 — 2f).
   *
   * `sigmap --init` injects this block so an agent reading CLAUDE.md knows the
   * grounded-creation guard-rail workflow exists: scaffold → verify-plan →
   * verify-ai-output → review-pr, orchestrated by `sigmap create`. Idempotent and
   * marker-scoped — it never touches human content or the conventions /
   * auto-generated-signatures blocks. Pure string transforms; zero-dependency,
   * bundle-safe.
   */

  const START = '<!-- sigmap-creation-workflow:start -->';
  const END = '<!-- sigmap-creation-workflow:end -->';

  /** Render the Creation workflow block (including its start/end markers). */
  function renderCreationWorkflowBlock() {
    return [
      START,
      '## Creation workflow (SigMap)',
      '',
      'When creating or changing code, run the grounded-creation pipeline so each step is verified against the live index:',
      '',
      '1. **`sigmap scaffold "<name>"`** — propose a convention-matched file/structure (refuses if conventions are inconsistent).',
      '2. **`sigmap verify-plan <plan.md>`** — check the plan against the live index (files/symbols exist, blast radius, scope).',
      '3. **`sigmap verify-ai-output <answer.md>`** — flag fake files/symbols/imports in the generated output (offline).',
      '4. **`sigmap review-pr`** — audit the diff for scope drift, god-node edits, missing tests, and security files.',
      '',
      'Or run all four in one pass with **`sigmap create "<task>"`** (`1/4`…`4/4` numbering, single pass/fail).',
      '',
      '<sub>Generated by `sigmap --init` · refresh by re-running it.</sub>',
      END,
    ].join('\n');
  }

  /**
   * Inject (or replace) the Creation workflow block in existing CLAUDE.md content.
   * Replaces an existing marked block in place; appends one when absent.
   * Idempotent — never touches content outside the markers.
   * @param {string} existing current file content ('' if new)
   * @param {string} block the block from `renderCreationWorkflowBlock`
   * @returns {string}
   */
  function injectCreationWorkflow(existing, block) {
    const src = String(existing || '');
    const startIdx = src.indexOf(START);
    if (startIdx !== -1) {
      const endIdx = src.indexOf(END, startIdx);
      if (endIdx !== -1) {
        return src.slice(0, startIdx) + block + src.slice(endIdx + END.length);
      }
    }
    if (src.trim() === '') return block + '\n';
    const sep = src.endsWith('\n') ? '\n' : '\n\n';
    return src + sep + block + '\n';
  }

  module.exports = { renderCreationWorkflowBlock, injectCreationWorkflow, START, END };
  
};

// ── ./src/judge/judge-engine ──
__factories["./src/judge/judge-engine"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');
  const { boostFiles, normalizeFile, penalizeFiles } = __require('./src/learning/weights');

  const STOP = new Set([
    'the','a','an','in','on','at','to','of','for','and','or','but',
    'is','are','was','were','be','been','being','have','has','had',
    'do','does','did','will','would','could','should','may','might',
    'shall','can','not','with','from','by','as','this','that','it',
  ]);

  function tokenize(text) {
    return (text || '').toLowerCase().match(/\b[a-z][a-z0-9_]{2,}\b/g) || [];
  }

  function groundedness(response, context) {
    if (!response || !context) return 0;
    const ctxTokens = new Set(tokenize(context).filter((t) => !STOP.has(t)));
    if (ctxTokens.size === 0) return 0;
    const respTokens = tokenize(response).filter((t) => !STOP.has(t));
    if (respTokens.length === 0) return 0;
    const matched = respTokens.filter((t) => ctxTokens.has(t));
    return parseFloat((matched.length / respTokens.length).toFixed(3));
  }

  const GENERIC_MARKERS = [
    'however, based on my knowledge',
    'generally speaking',
    'in general',
    'typically,',
    'usually,',
    'as a general rule',
  ];

  function extractContextFiles(context, cwd) {
    if (!context || !cwd) return [];

    const seen = new Set();
    const files = [];
    const lines = context.split('\n');

    for (const line of lines) {
      const match = line.match(/^#{2,3}\s+(.+?)\s*$/);
      if (!match) continue;

      const normalized = normalizeFile(cwd, match[1]);
      if (!normalized) continue;

      const abs = path.join(cwd, normalized);
      if (!fs.existsSync(abs) || seen.has(normalized)) continue;

      seen.add(normalized);
      files.push(normalized);
    }

    return files;
  }

  function judge(response, context, opts = {}) {
    const score = groundedness(response, context);
    const threshold = opts.threshold !== undefined ? opts.threshold : 0.25;
    const reasons = [];

    if (score < threshold) {
      reasons.push(`score ${score} is below threshold ${threshold} — response may not be grounded in context`);
    }

    if (response) {
      const lower = response.toLowerCase();
      for (const m of GENERIC_MARKERS) {
        if (lower.includes(m)) {
          reasons.push(`response contains generic phrase: "${m}"`);
        }
      }
    }

    const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
    const result = { score, verdict, reasons };

    if (opts.learn) {
      const learning = {
        applied: false,
        action: 'none',
        files: [],
      };

      if (!opts.cwd) {
        learning.reason = 'cwd is required for learning';
        result.learning = learning;
        return result;
      }

      const contextFiles = extractContextFiles(context, opts.cwd);
      learning.files = contextFiles;

      if (contextFiles.length === 0) {
        learning.reason = 'no context files found in context headings';
        result.learning = learning;
        return result;
      }

      if (score > 0.75) {
        boostFiles(opts.cwd, contextFiles, 0.05);
        learning.applied = true;
        learning.action = 'boost';
      } else if (score < 0.40) {
        penalizeFiles(opts.cwd, contextFiles, 0.03);
        learning.applied = true;
        learning.action = 'penalize';
      } else {
        learning.reason = 'groundedness in no-op band (0.40-0.75)';
      }

      result.learning = learning;
    }

    return result;
  }

  module.exports = { groundedness, judge };
  
};

// ── ./src/learning/weights ──
__factories["./src/learning/weights"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');

  const DECAY = 0.95;
  const MAX_MULT = 3.0;
  const MIN_MULT = 0.30;
  const BASELINE = 1.0;

  function weightsPath(cwd) {
    return path.join(cwd, '.context', 'weights.json');
  }

  function clampMultiplier(value) {
    if (!Number.isFinite(value)) return BASELINE;
    if (value > MAX_MULT) return MAX_MULT;
    if (value < MIN_MULT) return MIN_MULT;
    return parseFloat(value.toFixed(6));
  }

  function normalizeFile(cwd, filePath) {
    if (!cwd || !filePath || typeof filePath !== 'string') return null;
    const cleaned = filePath.trim().replace(/\\/g, '/');
    if (!cleaned) return null;

    const abs = path.resolve(cwd, cleaned);
    const rel = path.relative(cwd, abs);
    if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
    return rel.split(path.sep).join('/');
  }

  function sanitizeWeights(cwd, weights) {
    const out = {};
    const entries = weights && typeof weights === 'object' ? Object.entries(weights) : [];

    for (const [filePath, raw] of entries) {
      const normalized = normalizeFile(cwd, filePath);
      if (!normalized) continue;
      const mult = clampMultiplier(Number(raw));
      if (Math.abs(mult - BASELINE) < 1e-9) continue;
      out[normalized] = mult;
    }

    return out;
  }

  function loadWeights(cwd) {
    try {
      const parsed = JSON.parse(fs.readFileSync(weightsPath(cwd), 'utf8'));
      return sanitizeWeights(cwd, parsed);
    } catch (_) {
      return {};
    }
  }

  function saveWeights(cwd, weights) {
    const cleaned = sanitizeWeights(cwd, weights);
    const outPath = weightsPath(cwd);

    if (Object.keys(cleaned).length === 0) {
      try {
        if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
      } catch (_) {}
      return;
    }

    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    const sorted = Object.keys(cleaned)
      .sort()
      .reduce((acc, key) => {
        acc[key] = cleaned[key];
        return acc;
      }, {});
    fs.writeFileSync(outPath, JSON.stringify(sorted, null, 2) + '\n', 'utf8');
  }

  function updateWeights(cwd, opts = {}) {
    const goodAmount = Number.isFinite(opts.goodAmount) ? opts.goodAmount : 0.15;
    const badAmount = Number.isFinite(opts.badAmount) ? opts.badAmount : 0.10;
    const goodFiles = Array.isArray(opts.goodFiles) ? opts.goodFiles : [];
    const badFiles = Array.isArray(opts.badFiles) ? opts.badFiles : [];

    const weights = loadWeights(cwd);

    for (const key of Object.keys(weights)) {
      weights[key] = clampMultiplier(weights[key] * DECAY);
    }

    const good = [];
    const bad = [];

    for (const filePath of goodFiles) {
      const normalized = normalizeFile(cwd, filePath);
      if (!normalized) continue;
      weights[normalized] = clampMultiplier((weights[normalized] || BASELINE) + goodAmount);
      good.push(normalized);
    }

    for (const filePath of badFiles) {
      const normalized = normalizeFile(cwd, filePath);
      if (!normalized) continue;
      weights[normalized] = clampMultiplier((weights[normalized] || BASELINE) - badAmount);
      bad.push(normalized);
    }

    saveWeights(cwd, weights);
    return { good, bad, weights: loadWeights(cwd) };
  }

  function boostFiles(cwd, files, amount = 0.15) {
    return updateWeights(cwd, { goodFiles: files, goodAmount: amount });
  }

  function penalizeFiles(cwd, files, amount = 0.10) {
    return updateWeights(cwd, { badFiles: files, badAmount: amount });
  }

  function resetWeights(cwd) {
    const outPath = weightsPath(cwd);
    if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
  }

  function exportWeights(cwd, outputPath) {
    const weights = loadWeights(cwd);
    const json = JSON.stringify(weights, null, 2) + '\n';
    if (outputPath) {
      fs.mkdirSync(path.dirname(path.resolve(outputPath)), { recursive: true });
      fs.writeFileSync(outputPath, json, 'utf8');
    } else {
      process.stdout.write(json);
    }
    return weights;
  }

  function importWeights(cwd, importPath, replace) {
    let incoming;
    try {
      incoming = JSON.parse(fs.readFileSync(importPath, 'utf8'));
    } catch (err) {
      throw new Error(`Cannot read weights file: ${err.message}`);
    }
    const sanitized = sanitizeWeights(cwd, incoming);
    if (replace) {
      saveWeights(cwd, sanitized);
      return sanitized;
    }
    const existing = loadWeights(cwd);
    const merged = Object.assign({}, existing, sanitized);
    saveWeights(cwd, merged);
    return merged;
  }

  module.exports = {
    BASELINE,
    DECAY,
    MAX_MULT,
    MIN_MULT,
    weightsPath,
    clampMultiplier,
    normalizeFile,
    loadWeights,
    saveWeights,
    updateWeights,
    boostFiles,
    penalizeFiles,
    resetWeights,
    exportWeights,
    importWeights,
  };
  
};

// ── ./src/map/build-ci ──
__factories["./src/map/build-ci"] = function(module, exports) {
  
  /**
   * Build & CI extractor (v8.5 C1).
   *
   * Surfaces how the project is built and validated: npm/pnpm/yarn scripts
   * (package.json), GitHub Actions workflows (.github/workflows/*.yml), and
   * Makefile targets. Pure, zero-dependency, deterministic.
   *
   * @param {string[]} files — absolute file paths (unused; roots are read directly)
   * @param {string}   cwd   — project root
   * @returns {string} formatted markdown table (empty string if none found)
   */

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

  const MAX_ROWS = 120;

  function readJson(p) {
    try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
  }

  function npmScripts(cwd, rows) {
    const pkg = readJson(path.join(cwd, 'package.json'));
    if (!pkg || !pkg.scripts || typeof pkg.scripts !== 'object') return;
    for (const name of Object.keys(pkg.scripts).sort()) {
      rows.push({ kind: 'script', name, detail: 'npm run ' + name });
    }
  }

  function ciWorkflows(cwd, rows) {
    const dir = path.join(cwd, '.github', 'workflows');
    let entries;
    try { entries = fs.readdirSync(dir); } catch (_) { return; }
    for (const file of entries.sort()) {
      if (!/\.ya?ml$/i.test(file)) continue;
      let content;
      try { content = fs.readFileSync(path.join(dir, file), 'utf8'); } catch (_) { continue; }
      const nameMatch = content.match(/^name:\s*(.+)$/m);
      const name = nameMatch ? nameMatch[1].trim().replace(/^['"]|['"]$/g, '') : file;
      // Trigger events from an `on:` mapping or inline form.
      const onMatch = content.match(/^on:\s*(.*)$/m);
      let triggers = '';
      if (onMatch) {
        if (onMatch[1].trim()) {
          triggers = onMatch[1].replace(/[[\]{}'"]/g, '').trim();
        } else {
          const block = content.slice(onMatch.index);
          const events = [...block.matchAll(/^\s{2,}([a-z_]+):/gm)].map((m) => m[1]);
          triggers = [...new Set(events)].slice(0, 6).join(', ');
        }
      }
      rows.push({ kind: 'ci', name, detail: `${file}${triggers ? ' — ' + triggers : ''}` });
    }
  }

  function makeTargets(cwd, rows) {
    let content;
    try { content = fs.readFileSync(path.join(cwd, 'Makefile'), 'utf8'); } catch (_) { return; }
    const targets = [];
    for (const line of content.split('\n')) {
      const m = line.match(/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*)\s*:(?!=)/);
      if (m && m[1] !== '.PHONY') targets.push(m[1]);
    }
    for (const t of [...new Set(targets)].sort()) {
      rows.push({ kind: 'make', name: t, detail: 'make ' + t });
    }
  }

  function analyze(files, cwd) {
    const rows = [];
    npmScripts(cwd, rows);
    ciWorkflows(cwd, rows);
    makeTargets(cwd, rows);
    if (rows.length === 0) return '';

    const lines = [
      '| Kind | Name | Detail |',
      '|------|------|--------|',
    ];
    for (const r of rows.slice(0, MAX_ROWS)) {
      lines.push(`| ${r.kind} | ${r.name} | ${r.detail} |`);
    }
    if (rows.length > MAX_ROWS) {
      lines.push(`| … | | +${rows.length - MAX_ROWS} more |`);
    }
    return lines.join('\n');
  }

  module.exports = { analyze };
  
};

// ── ./src/map/class-hierarchy ──
__factories["./src/map/class-hierarchy"] = function(module, exports) {
  
  /**
   * Class hierarchy analyzer.
   * Extracts class declarations with extends/implements across
   * TypeScript, JavaScript, Python, Java, Kotlin, C# files.
   *
   * @param {string[]} files — absolute file paths to analyze
   * @param {string}   cwd   — project root for relative path display
   * @returns {string} formatted section content (empty string if nothing found)
   */

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

  function analyze(files, cwd) {
    const entries = [];

    for (const filePath of files) {
      const ext = path.extname(filePath).toLowerCase();
      const rel = path.relative(cwd, filePath).replace(/\\/g, '/');
      let content;
      try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }

      // TS / JS
      if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
        const re = /^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+([\w<>.]+?))?(?:\s+implements\s+([\w<>.,\s]+?))?\s*\{/gm;
        let m;
        while ((m = re.exec(content)) !== null) {
          const parent = m[2] ? m[2].split('<')[0].trim() : null;
          const ifaces = m[3]
            ? m[3].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
            : [];
          entries.push({ name: m[1], parent, interfaces: ifaces, file: rel });
        }
      }

      // Python
      if (['.py', '.pyw'].includes(ext)) {
        const re = /^\s*class\s+(\w+)\s*\(([^)]*)\)\s*:/gm;
        let m;
        while ((m = re.exec(content)) !== null) {
          const parents = m[2]
            .split(',')
            .map((s) => s.trim())
            .filter((s) => s && s !== 'object');
          entries.push({
            name: m[1],
            parent: parents[0] || null,
            interfaces: parents.slice(1),
            file: rel,
          });
        }
      }

      // Java
      if (ext === '.java') {
        const re = /^\s*(?:(?:public|protected|private|static|abstract|final)\s+)*class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s<>]+?))?\s*\{/gm;
        let m;
        while ((m = re.exec(content)) !== null) {
          const ifaces = m[3]
            ? m[3].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
            : [];
          entries.push({ name: m[1], parent: m[2] || null, interfaces: ifaces, file: rel });
        }
      }

      // Kotlin
      if (['.kt', '.kts'].includes(ext)) {
        const re = /^\s*(?:(?:data|sealed|abstract|open|inner)\s+)?class\s+(\w+)(?:\s*[^:\r\n]*)?\s*:\s*([\w<>(),.\s]+?)(?:\s*\{|$)/gm;
        let m;
        while ((m = re.exec(content)) !== null) {
          const parents = m[2]
            .split(',')
            .map((s) => s.replace(/\(.*?\)/, '').split('<')[0].trim())
            .filter(Boolean);
          entries.push({
            name: m[1],
            parent: parents[0] || null,
            interfaces: parents.slice(1),
            file: rel,
          });
        }
      }

      // C#
      if (ext === '.cs') {
        const re = /^\s*(?:(?:public|internal|protected|private|static|abstract|sealed|partial)\s+)*class\s+(\w+)(?:\s*:\s*([\w<>.,\s]+?))?\s*\{/gm;
        let m;
        while ((m = re.exec(content)) !== null) {
          const parents = m[2]
            ? m[2].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
            : [];
          entries.push({
            name: m[1],
            parent: parents[0] || null,
            interfaces: parents.slice(1),
            file: rel,
          });
        }
      }
    }

    if (entries.length === 0) return '';

    return entries
      .map((e) => {
        let line = e.name;
        if (e.parent) line += ` extends ${e.parent}`;
        if (e.interfaces.length > 0) line += ` implements ${e.interfaces.join(', ')}`;
        line += ` (${e.file})`;
        return line;
      })
      .join('\n');
  }

  module.exports = { analyze };
  
};

// ── ./src/map/config-manifest ──
__factories["./src/map/config-manifest"] = function(module, exports) {
  
  /**
   * Config & package-manifest extractor (v8.5 C1).
   *
   * Surfaces the project's package manifests (name / version / dependency counts)
   * across ecosystems and the notable root config files present. Pure,
   * zero-dependency, deterministic.
   *
   * @param {string[]} files — absolute file paths (unused; roots are read directly)
   * @param {string}   cwd   — project root
   * @returns {string} formatted markdown table (empty string if none found)
   */

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

  const CONFIG_FILES = [
    'tsconfig.json', 'jsconfig.json', '.eslintrc', '.eslintrc.json', '.eslintrc.js',
    '.prettierrc', 'babel.config.js', 'jest.config.js', 'vitest.config.ts',
    'webpack.config.js', 'vite.config.ts', 'rollup.config.js', 'tailwind.config.js',
    'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile', '.editorconfig',
  ];

  function readText(p) { try { return fs.readFileSync(p, 'utf8'); } catch (_) { return null; } }
  function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
  function count(obj) { return obj && typeof obj === 'object' ? Object.keys(obj).length : 0; }

  function manifests(cwd, rows) {
    const pkg = readJson(path.join(cwd, 'package.json'));
    if (pkg) {
      const deps = count(pkg.dependencies);
      const dev = count(pkg.devDependencies);
      const id = [pkg.name, pkg.version].filter(Boolean).join('@') || 'package.json';
      rows.push({ manifest: 'package.json (npm)', detail: `${id} · ${deps} deps, ${dev} devDeps` });
    }

    const pyproject = readText(path.join(cwd, 'pyproject.toml'));
    if (pyproject) {
      const name = (pyproject.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
      const ver = (pyproject.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
      rows.push({ manifest: 'pyproject.toml (python)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
    } else if (readText(path.join(cwd, 'setup.py'))) {
      rows.push({ manifest: 'setup.py (python)', detail: 'present' });
    }
    if (readText(path.join(cwd, 'requirements.txt'))) {
      rows.push({ manifest: 'requirements.txt (python)', detail: 'present' });
    }

    const cargo = readText(path.join(cwd, 'Cargo.toml'));
    if (cargo) {
      const name = (cargo.match(/^\s*name\s*=\s*["']([^"']+)["']/m) || [])[1];
      const ver = (cargo.match(/^\s*version\s*=\s*["']([^"']+)["']/m) || [])[1];
      rows.push({ manifest: 'Cargo.toml (rust)', detail: [name, ver].filter(Boolean).join('@') || 'present' });
    }

    const gomod = readText(path.join(cwd, 'go.mod'));
    if (gomod) {
      const mod = (gomod.match(/^module\s+(\S+)/m) || [])[1];
      const go = (gomod.match(/^go\s+(\S+)/m) || [])[1];
      rows.push({ manifest: 'go.mod (go)', detail: [mod, go && 'go ' + go].filter(Boolean).join(' · ') || 'present' });
    }

    if (readText(path.join(cwd, 'pom.xml'))) rows.push({ manifest: 'pom.xml (maven)', detail: 'present' });
    if (readText(path.join(cwd, 'build.gradle')) || readText(path.join(cwd, 'build.gradle.kts'))) {
      rows.push({ manifest: 'build.gradle (gradle)', detail: 'present' });
    }
    if (readText(path.join(cwd, 'Gemfile'))) rows.push({ manifest: 'Gemfile (ruby)', detail: 'present' });
    const composer = readJson(path.join(cwd, 'composer.json'));
    if (composer) {
      rows.push({ manifest: 'composer.json (php)', detail: `${composer.name || 'present'} · ${count(composer.require)} deps` });
    }
  }

  function configFiles(cwd) {
    const present = [];
    for (const f of CONFIG_FILES) {
      if (fs.existsSync(path.join(cwd, f))) present.push(f);
    }
    return present;
  }

  function analyze(files, cwd) {
    const rows = [];
    manifests(cwd, rows);
    const configs = configFiles(cwd);
    if (rows.length === 0 && configs.length === 0) return '';

    const lines = [];
    if (rows.length) {
      lines.push('| Manifest | Detail |', '|----------|--------|');
      for (const r of rows) lines.push(`| ${r.manifest} | ${r.detail} |`);
    }
    if (configs.length) {
      if (lines.length) lines.push('');
      lines.push(`**Config files:** ${configs.map((c) => '`' + c + '`').join(', ')}`);
    }
    return lines.join('\n');
  }

  module.exports = { analyze };
  
};

// ── ./src/map/env-schema ──
__factories["./src/map/env-schema"] = function(module, exports) {
  
  /**
   * Environment-variable schema extractor (v8.5 C1).
   *
   * Surfaces the environment the project actually reads — from source across
   * JS/TS, Python, Ruby, and Go, plus keys declared in a committed `.env.example`
   * / `.env.sample` / `.env.template`. Pure, zero-dependency, deterministic.
   *
   * @param {string[]} files — absolute file paths to analyze (srcDirs-scoped)
   * @param {string}   cwd   — project root
   * @returns {string} formatted markdown table (empty string if none found)
   */

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

  const SCAN_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.go']);
  const EXAMPLE_FILES = ['.env.example', '.env.sample', '.env.template', '.env.dist'];

  // process.env.X / process.env['X'] / import.meta.env.X / Deno.env.get('X')
  const JS_RE = /(?:process\.env|import\.meta\.env)(?:\.([A-Z_][A-Z0-9_]*)|\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\])|Deno\.env\.get\(\s*['"]([A-Z_][A-Z0-9_]*)['"]/g;
  // os.environ['X'] / os.environ.get('X') / os.getenv('X') / getenv('X')
  const PY_RE = /(?:os\.)?(?:environ(?:\.get)?\[?\s*['"]([A-Z_][A-Z0-9_]*)['"]|getenv\(\s*['"]([A-Z_][A-Z0-9_]*)['"])/g;
  const RB_RE = /ENV\[\s*['"]([A-Z_][A-Z0-9_]*)['"]\s*\]/g;
  const GO_RE = /os\.(?:Getenv|LookupEnv)\(\s*["`']([A-Z_][A-Z0-9_]*)["`']/g;

  const MAX_ROWS = 200;

  function collectMatches(re, content, into) {
    let m;
    re.lastIndex = 0;
    while ((m = re.exec(content)) !== null) {
      const name = m[1] || m[2] || m[3];
      if (name) into.add(name);
    }
  }

  function readExampleKeys(cwd) {
    const keys = new Set();
    for (const name of EXAMPLE_FILES) {
      let content;
      try { content = fs.readFileSync(path.join(cwd, name), 'utf8'); } catch (_) { continue; }
      for (const line of content.split('\n')) {
        const t = line.trim();
        if (!t || t.startsWith('#')) continue;
        const eq = t.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
        if (eq) keys.add(eq[1]);
      }
    }
    return keys;
  }

  function analyze(files, cwd) {
    const fromCode = new Set();

    for (const filePath of files) {
      const ext = path.extname(filePath).toLowerCase();
      if (!SCAN_EXTS.has(ext)) continue;
      let content;
      try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }

      if (ext === '.py') collectMatches(PY_RE, content, fromCode);
      else if (ext === '.rb') collectMatches(RB_RE, content, fromCode);
      else if (ext === '.go') collectMatches(GO_RE, content, fromCode);
      else collectMatches(JS_RE, content, fromCode);
    }

    const fromExample = readExampleKeys(cwd);
    const all = new Set([...fromCode, ...fromExample]);
    if (all.size === 0) return '';

    const names = [...all].sort();
    const lines = [
      '| Variable | Source |',
      '|----------|--------|',
    ];
    for (const name of names.slice(0, MAX_ROWS)) {
      const src = [];
      if (fromCode.has(name)) src.push('code');
      if (fromExample.has(name)) src.push('.env.example');
      lines.push(`| ${name} | ${src.join(', ')} |`);
    }
    if (names.length > MAX_ROWS) {
      lines.push(`| … | +${names.length - MAX_ROWS} more |`);
    }
    return lines.join('\n');
  }

  module.exports = { analyze };
  
};

// ── ./src/map/import-graph ──
__factories["./src/map/import-graph"] = function(module, exports) {
  
  /**
   * Import graph analyzer.
   * Extracts relative import relationships from JS/TS/Python files
   * and detects circular dependencies.
   *
   * @param {string[]} files — absolute file paths to analyze
   * @param {string}   cwd   — project root for relative path display
   * @returns {string} formatted section content (empty string if nothing found)
   */

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

  const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
  const PY_EXTS = new Set(['.py', '.pyw']);

  // ---------------------------------------------------------------------------
  // Import extraction per language
  // ---------------------------------------------------------------------------
  function extractImports(filePath, content, fileSet) {
    const ext = path.extname(filePath).toLowerCase();
    const dir = path.dirname(filePath);
    const found = [];

    if (JS_EXTS.has(ext)) {
      // ES: import ... from './foo'  or  import './side-effect'
      const re1 = /(?:^|[\r\n])\s*import\s+(?:[^'";\r\n]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
      let m;
      while ((m = re1.exec(content)) !== null) {
        const resolved = resolveJsPath(dir, m[1], fileSet);
        if (resolved) found.push(resolved);
      }
      // CommonJS: __require('./src/map/foo')
      const re2 = /\brequire\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
      while ((m = re2.exec(content)) !== null) {
        const resolved = resolveJsPath(dir, m[1], fileSet);
        if (resolved) found.push(resolved);
      }
    }

    if (PY_EXTS.has(ext)) {
      // Relative imports: from .module import ...  /  from ..pkg import ...
      const reRel = /^[ \t]*from\s+(\.+[\w.]*)\s+import/gm;
      let m;
      while ((m = reRel.exec(content)) !== null) {
        const dotCount = (m[1].match(/^\.+/) || [''])[0].length;
        const modPart = m[1].slice(dotCount).replace(/\./g, '/');
        let base = dir;
        for (let i = 1; i < dotCount; i++) base = path.dirname(base);
        const candidate = modPart ? path.join(base, modPart + '.py') : null;
        if (candidate && fileSet.has(candidate)) found.push(candidate);
      }

      // Absolute imports: from package.module import ... (infer from project structure)
      const reAbs = /^[ \t]*from\s+([\w.]+)\s+import/gm;
      while ((m = reAbs.exec(content)) !== null) {
        const modulePath = m[1].replace(/\./g, '/');
        const candidates = [
          path.join(dir, modulePath + '.py'),
          path.join(dir, modulePath, '__init__.py'),
          path.resolve(dir, '..', modulePath + '.py'),
          path.resolve(dir, '..', modulePath, '__init__.py'),
        ];
        for (const c of candidates) {
          if (fileSet.has(c)) {
            found.push(c);
            break;
          }
        }
      }
    }

    return [...new Set(found)];
  }

  function resolveJsPath(dir, importStr, fileSet) {
    const base = path.resolve(dir, importStr);
    const candidates = [
      base,
      base + '.ts', base + '.tsx',
      base + '.js', base + '.jsx', base + '.mjs', base + '.cjs',
      base + '/index.ts', base + '/index.tsx',
      base + '/index.js', base + '/index.jsx', base + '/index.mjs',
    ];
    for (const c of candidates) {
      if (fileSet.has(c)) return c;
    }

    // Fallback: check if base itself is already a valid file (handles .ts/.js already in path)
    if (fileSet.has(base)) return base;

    return null;
  }

  // ---------------------------------------------------------------------------
  // Cycle detection (DFS with path tracking)
  // ---------------------------------------------------------------------------
  function detectCycles(graph) {
    const cycles = [];
    const visited = new Set();
    const onStack = new Set();
    const stackArr = [];

    function dfs(node) {
      if (onStack.has(node)) {
        const start = stackArr.indexOf(node);
        if (start !== -1) cycles.push([...stackArr.slice(start), node]);
        return;
      }
      if (visited.has(node)) return;

      onStack.add(node);
      stackArr.push(node);
      for (const dep of (graph.get(node) || [])) dfs(dep);
      stackArr.pop();
      onStack.delete(node);
      visited.add(node);
    }

    for (const node of graph.keys()) {
      if (!visited.has(node)) dfs(node);
    }
    return cycles;
  }

  // ---------------------------------------------------------------------------
  // Build reverse graph (for caller detection)
  // ---------------------------------------------------------------------------
  function buildReverseGraph(graph) {
    const reverse = new Map();
    for (const [file, deps] of graph.entries()) {
      for (const dep of deps) {
        if (!reverse.has(dep)) reverse.set(dep, []);
        reverse.get(dep).push(file);
      }
    }
    return reverse;
  }

  // ---------------------------------------------------------------------------
  // Public API
  // ---------------------------------------------------------------------------
  function analyze(files, cwd) {
    const fileSet = new Set(files.map((f) => path.resolve(f)));
    const graph = new Map();

    for (const filePath of files) {
      let content;
      try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
      const deps = extractImports(path.resolve(filePath), content, fileSet);
      if (deps.length > 0) graph.set(path.resolve(filePath), deps);
    }

    if (graph.size === 0) return '';

    const cycles = detectCycles(graph);
    const cycleNodeSet = new Set(cycles.flatMap((c) => c));

    const lines = [];
    const sorted = [...graph.entries()].sort((a, b) => a[0].localeCompare(b[0]));

    for (const [fp, deps] of sorted) {
      const rel = path.relative(cwd, fp).replace(/\\/g, '/');
      const depList = deps.map((d) => {
        const drel = path.relative(cwd, d).replace(/\\/g, '/');
        return cycleNodeSet.has(d) ? `${drel} ⚠` : drel;
      });
      lines.push(`${rel} → ${depList.join(', ')}`);
    }

    if (cycles.length > 0) {
      lines.push('');
      lines.push('Circular dependencies detected:');
      for (const cycle of cycles) {
        const relPath = cycle.map((n) => path.relative(cwd, n).replace(/\\/g, '/')).join(' → ');
        lines.push(`  ⚠ ${relPath}`);
      }
    }

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

  module.exports = { analyze, extractImports, buildReverseGraph, resolveJsPath, detectCycles };
  
};

// ── ./src/map/migrations ──
__factories["./src/map/migrations"] = function(module, exports) {
  
  /**
   * Database-migration extractor (v8.5 C1).
   *
   * Detects schema-migration files across the common frameworks — Rails
   * (db/migrate), Django/Alembic, Prisma, Flyway (`V1__name.sql`), knex/Sequelize,
   * and timestamped SQL — and surfaces them with a parsed version + name. Pure,
   * zero-dependency, deterministic.
   *
   * @param {string[]} files — absolute file paths (unused; the tree is walked)
   * @param {string}   cwd   — project root
   * @returns {string} formatted markdown table (empty string if none found)
   */

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

  const MAX_DEPTH = 6;
  const MAX_ROWS = 200;
  const SKIP_DIR = new Set(['.git', 'node_modules', 'vendor', 'dist', 'build', 'target', '.venv', 'venv', '__pycache__']);
  const MIG_EXT = new Set(['.sql', '.rb', '.py', '.js', '.ts']);

  // A directory whose path marks its children as migrations.
  const MIG_DIR_RE = /(^|\/)(db\/migrate|migrations?|alembic\/versions|prisma\/migrations)$/i;
  // A filename that is itself a migration regardless of directory.
  const FLYWAY_RE = /^V\d+(?:[._]\d+)*__(.+)\.(sql|java)$/;
  const TIMESTAMP_RE = /^(\d{8,})[_-](.+)\.(sql|rb|py|js|ts)$/;
  const NAMED_RE = /[._-]migrations?[._-]/i;

  function walk(dir, cwd, depth, out) {
    if (depth > MAX_DEPTH) return;
    let entries;
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
    entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));

    const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
    const dirIsMigration = MIG_DIR_RE.test(relDir);

    for (const e of entries) {
      if (e.isDirectory()) {
        if (SKIP_DIR.has(e.name)) continue;
        walk(path.join(dir, e.name), cwd, depth + 1, out);
        continue;
      }
      const ext = path.extname(e.name).toLowerCase();
      if (!MIG_EXT.has(ext)) continue;

      const rel = path.relative(cwd, path.join(dir, e.name)).replace(/\\/g, '/');
      let version = null;
      let name = null;

      let m;
      if ((m = e.name.match(FLYWAY_RE))) { version = e.name.split('__')[0]; name = m[1].replace(/_/g, ' '); }
      else if ((m = e.name.match(TIMESTAMP_RE))) { version = m[1]; name = m[2].replace(/[_-]/g, ' '); }
      else if (dirIsMigration) { version = '—'; name = e.name.replace(ext, ''); }
      else if (NAMED_RE.test(e.name)) { version = '—'; name = e.name.replace(ext, ''); }
      else continue;

      out.push({ version, name, file: rel });
    }
  }

  function analyze(files, cwd) {
    const found = [];
    walk(cwd, cwd, 0, found);
    if (found.length === 0) return '';

    found.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));

    const lines = [
      '| Version | Migration | File |',
      '|---------|-----------|------|',
    ];
    for (const r of found.slice(0, MAX_ROWS)) {
      lines.push(`| ${r.version} | ${r.name} | ${r.file} |`);
    }
    if (found.length > MAX_ROWS) {
      lines.push(`| … | +${found.length - MAX_ROWS} more | |`);
    }
    return lines.join('\n');
  }

  module.exports = { analyze };
  
};

// ── ./src/map/route-table ──
__factories["./src/map/route-table"] = function(module, exports) {
  
  /**
   * HTTP route table extractor.
   * Detects routes in Express, Fastify, NestJS, Flask, FastAPI, Gin, Spring.
   *
   * @param {string[]} files — absolute file paths to analyze
   * @param {string}   cwd   — project root for relative path display
   * @returns {string} formatted markdown table (empty string if no routes found)
   */

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

  const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
  const PY_EXTS = new Set(['.py', '.pyw']);

  function shouldSkipFile(rel) {
    const normalized = rel.replace(/\\/g, '/').toLowerCase();
    return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
  }

  function analyze(files, cwd) {
    const routes = [];

    for (const filePath of files) {
      const ext = path.extname(filePath).toLowerCase();
      const rel = path.relative(cwd, filePath).replace(/\\/g, '/');
      if (shouldSkipFile(rel)) continue;
      let content;
      try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }

      // -----------------------------------------------------------------------
      // Express / Fastify / Koa (JS/TS)
      // -----------------------------------------------------------------------
      if (JS_EXTS.has(ext)) {
        // app.get('/path', ...) / router.post('/path') / fastify.put('/path')
        const re1 = /\b(?:app|router|fastify|server|koa|instance)\.(get|post|put|patch|delete|head|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
        let m;
        while ((m = re1.exec(content)) !== null) {
          routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
        }

        // NestJS decorators: @Get('/path') @Post('/path')
        const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
        while ((m = re2.exec(content)) !== null) {
          routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
        }

        // NestJS: @Get() with no path
        const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
        while ((m = re3.exec(content)) !== null) {
          routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
        }
      }

      // -----------------------------------------------------------------------
      // Flask / FastAPI (Python)
      // -----------------------------------------------------------------------
      if (PY_EXTS.has(ext)) {
        // @app.route('/path', methods=['GET', 'POST'])
        const re1 = /@[\w.]+\.route\s*\(\s*['"]([^'"]+)['"]([\s\S]{0,150}?)\)/g;
        let m;
        while ((m = re1.exec(content)) !== null) {
          const routePath = m[1];
          const methodsMatch = m[2].match(/methods\s*=\s*\[([^\]]+)\]/);
          if (methodsMatch) {
            const methods = methodsMatch[1].match(/['"]([A-Z]+)['"]/g) || [];
            for (const meth of methods) {
              routes.push({ method: meth.replace(/['"]/g, ''), path: routePath, file: rel });
            }
          } else {
            routes.push({ method: 'GET', path: routePath, file: rel });
          }
        }

        // @app.get('/path')  @router.post('/path')  FastAPI style
        const re2 = /@[\w.]+\.(get|post|put|patch|delete|head|options)\s*\(\s*['"]([^'"]+)['"]/g;
        while ((m = re2.exec(content)) !== null) {
          routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
        }
      }

      // -----------------------------------------------------------------------
      // Go — Gin / Echo / chi / net/http
      // -----------------------------------------------------------------------
      if (ext === '.go') {
        // r.GET("/path", handler)
        const re1 = /\b\w+\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*["']([^"']+)["']/g;
        let m;
        while ((m = re1.exec(content)) !== null) {
          routes.push({ method: m[1], path: m[2], file: rel });
        }
        // http.HandleFunc("/path", handler)
        const re2 = /http\.HandleFunc\s*\(\s*["']([^"']+)["']/g;
        while ((m = re2.exec(content)) !== null) {
          routes.push({ method: 'ANY', path: m[1], file: rel });
        }
      }

      // -----------------------------------------------------------------------
      // Spring (Java)
      // -----------------------------------------------------------------------
      if (ext === '.java') {
        // @GetMapping("/path") @PostMapping @RequestMapping
        const re1 = /@(Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/g;
        let m;
        while ((m = re1.exec(content)) !== null) {
          const method = m[1] === 'Request' ? 'ANY' : m[1].toUpperCase();
          routes.push({ method, path: m[2], file: rel });
        }
      }
    }

    if (routes.length === 0) return '';

    const lines = [
      '| Method | Path | File |',
      '|--------|------|------|',
    ];
    for (const r of routes) {
      lines.push(`| ${r.method} | ${r.path} | ${r.file} |`);
    }
    return lines.join('\n');
  }

  module.exports = { analyze };
  
};

// ── ./src/mcp/handlers ──
__factories["./src/mcp/handlers"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');
  const { git } = __require('./src/util/git');

  const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
  const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');

  function _readContextFiles(cwd) {
    const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
    const chunks = [];
    for (const p of paths) {
      if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
    }
    return chunks.join('\n');
  }

  // Section header keywords in PROJECT_MAP.md
  const MAP_SECTIONS = {
    imports: '### Import graph',
    classes: '### Class hierarchy',
    routes: '### Route table',
    env: '### Environment variables',
    buildci: '### Build & CI',
    manifests: '### Config & manifests',
    migrations: '### Database migrations',
  };

  /**
   * read_context({ module? }) → string
   *
   * Returns the full context file, or just the sections whose file paths
   * contain the given module substring.
   */
  function readContext(args, cwd) {
    const content = _readContextFiles(cwd);
    if (!content) {
      return 'No context file found. Run: node gen-context.js';
    }

    if (!args || !args.module) return content;

    const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
    const lines = content.split('\n');
    const result = [];
    let capturing = false;

    for (const line of lines) {
      if (line.startsWith('### ')) {
        const filePath = line.slice(4).trim().replace(/\\/g, '/');
        // Match if file path starts with mod or contains /mod/ or /mod
        capturing =
          filePath === mod ||
          filePath.startsWith(mod + '/') ||
          filePath.includes('/' + mod + '/') ||
          filePath.includes('/' + mod);
        if (capturing) result.push(line);
        continue;
      }
      if (capturing) result.push(line);
    }

    if (result.length === 0) return `No signatures found for module: ${mod}`;
    return result.join('\n');
  }

  /**
   * search_signatures({ query }) → string
   *
   * Case-insensitive search through all signature lines.
   * Returns matching lines grouped by file path.
   */
  function searchSignatures(args, cwd) {
    if (!args || !args.query) return 'Missing required argument: query';

    const query = args.query.toLowerCase();
    try {
      try { __require('./src/cache/freshen').freshen(cwd); } catch (_) {}
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      const index = buildSigIndex(cwd);
      if (index.size === 0) {
        return 'No context file found. Run: node gen-context.js';
      }

      const result = [];
      for (const [file, sigs] of index.entries()) {
        const hits = sigs.filter((s) => s.toLowerCase().includes(query));
        if (hits.length === 0) continue;
        if (result.length > 0) result.push('');
        result.push(`### ${file}`);
        result.push(...hits);
      }

      if (result.length === 0) return `No signatures found matching: ${args.query}`;
      return result.join('\n');
    } catch (err) {
      return `_search_signatures failed: ${err.message}_`;
    }
  }

  /**
   * get_map({ type }) → string
   *
   * Returns a section from PROJECT_MAP.md.
   * type: 'imports' | 'classes' | 'routes'
   */
  function getMap(args, cwd) {
    if (!args || !args.type) return 'Missing required argument: type';

    const header = MAP_SECTIONS[args.type];
    if (!header) {
      return `Unknown map type: "${args.type}". Use: ${Object.keys(MAP_SECTIONS).join(', ')}`;
    }

    const mapPath = path.join(cwd, 'PROJECT_MAP.md');
    if (!fs.existsSync(mapPath)) {
      return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
    }

    const content = fs.readFileSync(mapPath, 'utf8');
    const idx = content.indexOf(header);
    if (idx === -1) {
      return `Section "${header}" not found in PROJECT_MAP.md`;
    }

    // Extract from this header to the next ### header
    const after = content.slice(idx);
    const nextMatch = after.slice(header.length).search(/\n###\s/);
    return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
  }

  /**
   * create_checkpoint({ note? }) → string
   *
   * Returns a markdown checkpoint summarising current project state:
   * - Timestamp and optional user note
   * - Active git branch + last 5 commit messages
   * - Token count of current context file
   * - List of modules present in the context
   * - Route count (if PROJECT_MAP.md exists)
   */
  function createCheckpoint(args, cwd) {
    const note = (args && args.note) ? args.note.trim() : '';
    const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
    const lines = [
      '# SigMap Checkpoint',
      `**Created:** ${now}`,
    ];

    if (note) lines.push(`**Note:** ${note}`);
    lines.push('');

    // ── Git info ────────────────────────────────────────────────────────────
    lines.push('## Git state');
    try {
      const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).trim();
      lines.push(`**Branch:** ${branch}`);
    } catch (_) {
      lines.push('**Branch:** (not a git repo)');
    }

    try {
      const log = git(['log', '--oneline', '-5', '--no-decorate'], { cwd }).trim();
      if (log) {
        lines.push('');
        lines.push('**Recent commits:**');
        for (const l of log.split('\n')) lines.push(`- ${l}`);
      }
    } catch (_) {} // ignore — not every project uses git
    lines.push('');

    // ── Context stats ────────────────────────────────────────────────────────
    lines.push('## Context snapshot');
    const contextPath = path.join(cwd, CONTEXT_FILE);
    if (fs.existsSync(contextPath)) {
      const content = fs.readFileSync(contextPath, 'utf8');
      const tokens = Math.ceil(content.length / 4);

      // Count modules (### headers are file paths)
      const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
      lines.push(`**Token count:** ~${tokens}`);
      lines.push(`**Modules in context:** ${modules.length}`);

      if (modules.length > 0) {
        lines.push('');
        lines.push('**Modules:**');
        for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
        if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
      }
    } else {
      lines.push('_No context file found. Run: node gen-context.js_');
    }
    lines.push('');

    // ── Route summary ────────────────────────────────────────────────────────
    const mapPath = path.join(cwd, 'PROJECT_MAP.md');
    if (fs.existsSync(mapPath)) {
      const mapContent = fs.readFileSync(mapPath, 'utf8');
      const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
      if (routeLines.length > 0) {
        lines.push('## Routes');
        lines.push(`**Total routes detected:** ${routeLines.length}`);
        lines.push('');
        for (const r of routeLines.slice(0, 10)) lines.push(r);
        if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
        lines.push('');
      }
    }

    lines.push('---');
    lines.push('_Generated by SigMap `create_checkpoint`_');

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

  /**
   * get_routing({}) → string
   *
   * Reads the current context file, classifies all indexed files by complexity,
   * and returns a formatted markdown routing guide showing which files belong
   * to the fast/balanced/powerful model tier.
   */
  function getRouting(args, cwd) {
    const contextPath = path.join(cwd, CONTEXT_FILE);
    if (!fs.existsSync(contextPath)) {
      return (
        '_No context file found. Run `node gen-context.js --routing` first._\n\n' +
        'This generates routing hints that map each file to a model tier:\n' +
        '- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
        '- **balanced** (sonnet/gpt-4o) — standard application code\n' +
        '- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
      );
    }

    // Parse file list from context (### headings are file paths)
    const content = fs.readFileSync(contextPath, 'utf8');
    const fileRels = content.split('\n')
      .filter((l) => l.startsWith('### '))
      .map((l) => l.slice(4).trim());

    // Build synthetic fileEntries for the classifier
    // We don't have live sig arrays here, so rebuild from the context blocks
    const entries = [];
    const blocks = content.split(/^### /m).slice(1); // slice past the header
    for (const block of blocks) {
      const firstLine = block.split('\n')[0].trim();
      const codeBlock = block.match(/```\n([\s\S]*?)```/);
      const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
      entries.push({ filePath: path.join(cwd, firstLine), sigs });
    }

    try {
      const { classifyAll } = __require('./src/routing/classifier');
      const { formatRoutingSection } = __require('./src/routing/hints');
      const groups = classifyAll(entries, cwd);
      return formatRoutingSection(groups);
    } catch (err) {
      return `_Routing classification failed: ${err.message}_`;
    }
  }

  /**
   * explain_file({ path }) → string
   *
   * Returns a file's signatures, its direct imports, and files that import it.
   * path: relative path from project root (e.g. 'src/services/auth.ts')
   */
  function explainFile(args, cwd) {
    if (!args || !args.path) return 'Missing required argument: path';

    const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
    const targetAbs = path.resolve(cwd, targetRel);
    const contextPath = path.join(cwd, CONTEXT_FILE);

    const lines = ['# explain_file: ' + targetRel, ''];

    // ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
    lines.push('## Signatures');
    let indexedFiles = [];

    try {
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      const index = buildSigIndex(cwd);
      let sigs = index.get(targetRel);
      if (!sigs) {
        for (const [file, fileSigs] of index.entries()) {
          if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
            sigs = fileSigs;
            break;
          }
        }
      }
      if (sigs && sigs.length > 0) {
        lines.push(...sigs);
      } else {
        lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
      }
      indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
    } catch (_) {
      lines.push('_No context file found. Run: node gen-context.js_');
    }

    if (!fs.existsSync(targetAbs)) {
      lines.push('');
      lines.push('> File not found on disk: ' + targetRel);
      return lines.join('\n');
    }

    lines.push('');

    // ── Direct imports ────────────────────────────────────────────────────────
    lines.push('## Imports (direct dependencies)');
    try {
      const { extractImports } = __require('./src/map/import-graph');
      const fileContent = fs.readFileSync(targetAbs, 'utf8');
      const fileSet = new Set(indexedFiles);
      fileSet.add(targetAbs);
      const imports = extractImports(targetAbs, fileContent, fileSet);
      if (imports.length > 0) {
        for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
      } else {
        lines.push('_No resolvable relative imports found._');
      }
    } catch (err) {
      lines.push('_Could not analyze imports: ' + err.message + '_');
    }

    lines.push('');

    // ── Callers (reverse-import lookup) ──────────────────────────────────────
    lines.push('## Callers (files that import this file)');
    try {
      const { extractImports } = __require('./src/map/import-graph');
      const fileSet = new Set(indexedFiles);
      fileSet.add(targetAbs);
      const callers = [];
      for (const f of indexedFiles) {
        if (f === targetAbs || !fs.existsSync(f)) continue;
        try {
          const fc = fs.readFileSync(f, 'utf8');
          const imps = extractImports(f, fc, fileSet);
          if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
        } catch (_) {}
      }
      if (callers.length > 0) {
        for (const c of callers) lines.push('- ' + c);
      } else {
        lines.push('_No indexed files import this file._');
      }
    } catch (err) {
      lines.push('_Could not analyze callers: ' + err.message + '_');
    }

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

  /**
   * list_modules({}) → string
   *
   * Lists all srcDir modules present in the context file, sorted by token count
   * descending. Helps agents decide which module to query with read_context.
   */
  function listModules(args, cwd) {
    try {
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      const index = buildSigIndex(cwd);
      if (index.size === 0) {
        return 'No context file found. Run: node gen-context.js';
      }

      const groups = {};
      for (const [rel, sigs] of index.entries()) {
        const parts = rel.replace(/\\/g, '/').split('/');
        const mod = parts.length > 1 ? parts[0] : '.';
        if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
        groups[mod].fileCount++;
        groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
      }

    const sorted = Object.entries(groups)
      .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
      .sort((a, b) => b.tokenCount - a.tokenCount);

    if (sorted.length === 0) return 'No modules found in context file.';

    const total = sorted.reduce((s, m) => s + m.tokenCount, 0);

    return [
      '# Modules',
      '',
      '| Module | Files | Tokens |',
      '|--------|-------|--------|',
      ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
      '',
      `**Total context tokens: ~${total}**`,
      '',
      '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
    ].join('\n');
    } catch (err) {
      return `_list_modules failed: ${err.message}_`;
    }
  }

  /**
   * query_context({ query, topK? }) → string
   *
   * Ranks context-file entries by relevance to the query and returns the
   * top-K most relevant files with their signatures and scores.
   */
  function queryContext(args, cwd) {
    if (!args || !args.query) return 'Missing required argument: query';

    try {
      const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
      const { buildFromCwd } = __require('./src/graph/builder');
      const index = buildSigIndex(cwd);
      if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';

      const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
      // Build dependency graph for neighbor boost — non-fatal if it fails
      let graph = null;
      try { graph = buildFromCwd(cwd); } catch (_) {}
      const results = rank(args.query, index, { topK, cwd, graph });
      return formatRankTable(results, args.query);
    } catch (err) {
      return `_query_context failed: ${err.message}_`;
    }
  }

  /**
   * get_impact({ file, depth? }) → string
   *
   * Returns a formatted markdown impact report for the given file:
   * direct importers, transitive importers, affected tests, affected routes.
   */
  function getImpact(args, cwd) {
    if (!args || !args.file) return 'Missing required argument: file';

    try {
      const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
      const depth = Math.max(0, parseInt(args.depth, 10) || 3);
      const results = analyzeImpact(args.file, cwd, { depth });
      if (results.length === 0) return `No impact data for: ${args.file}`;
      return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
    } catch (err) {
      return `_get_impact failed: ${err.message}_`;
    }
  }

  /**
   * get_lines({ file, start, end }) → string
   *
   * Surgical Context demand-driven fetch: returns an exact, clamped line range from a
   * source file. The path is resolved inside the project root (no traversal escape) and
   * the returned lines are secret-scanned via the same redactor used for signatures.
   */
  function getLines(args, cwd) {
    if (!args || !args.file) return 'Missing required argument: file';

    const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
    const abs = path.resolve(cwd, rel);

    // Sandbox: refuse paths that resolve outside the project root.
    const root = path.resolve(cwd);
    if (abs !== root && !abs.startsWith(root + path.sep)) {
      return `Refused: ${rel} resolves outside the project root`;
    }
    if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
      return `File not found: ${rel}`;
    }

    const start = parseInt(args.start, 10);
    const end = parseInt(args.end, 10);
    if (!Number.isFinite(start) || !Number.isFinite(end)) {
      return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
    }

    let lines;
    try {
      lines = fs.readFileSync(abs, 'utf8').split('\n');
    } catch (err) {
      return `Could not read ${rel}: ${err.message}`;
    }

    const total = lines.length;
    const from = Math.max(1, Math.min(start, end));
    const to = Math.min(total, Math.max(start, end));
    if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;

    const slice = lines.slice(from - 1, to);

    // Redaction scan: reuse the signature secret scanner line-by-line.
    let safeLines = slice;
    try {
      const { scan } = __require('./src/security/scanner');
      safeLines = scan(slice, rel).safe;
    } catch (_) {} // non-fatal: fall back to raw slice

    return [
      `# ${rel}:${from}-${to}`,
      '```',
      ...safeLines,
      '```',
    ].join('\n');
  }

  /**
   * read_memory({ limit? }) → string
   *
   * Recall the cross-session decision log (notes) plus the last ranking-session
   * focus, formatted for an agent to consume at the start of a task.
   */
  function readMemory(args, cwd) {
    let limit = parseInt(args && args.limit, 10);
    if (!Number.isFinite(limit) || limit <= 0) limit = 10;
    limit = Math.min(limit, 50);

    const out = ['# SigMap memory'];

    let notes = [];
    try {
      const { readNotes, formatNotes } = __require('./src/session/notes');
      notes = readNotes(cwd, limit);
      out.push('');
      out.push(`## Recent notes (${notes.length})`);
      // Most recent first for quick scanning.
      out.push(formatNotes(notes.slice().reverse()));
    } catch (_) {
      out.push('');
      out.push('_No notes available._');
    }

    // Last ranking-session focus (if any) — extends src/session/memory.js.
    try {
      const { loadSession } = __require('./src/session/memory');
      const s = loadSession(cwd);
      if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
        out.push('');
        out.push('## Last session');
        if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
        if (s.topFiles && s.topFiles.length) {
          out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
        }
      }
    } catch (_) { /* session optional */ }

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

  /**
   * get_callee_signatures — return the exact defining signature(s) of named
   * symbols from the index, so an agent never guesses a callee's parameter types.
   * Unknown names get a closest-match suggestion.
   * @param {{symbols:string[]}} args
   * @param {string} cwd
   */
  function getCalleeSignatures(args, cwd) {
    const symbols = args && Array.isArray(args.symbols)
      ? args.symbols.map((s) => String(s).trim()).filter(Boolean)
      : null;
    if (!symbols || symbols.length === 0) {
      return 'Missing required argument: symbols (non-empty string[])';
    }

    try {
      try { __require('./src/cache/freshen').freshen(cwd); } catch (_) {}
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      const { buildSymbolCandidates, closestMatch, formatSuggestion } = __require('./src/verify/closest-match');
      const index = buildSigIndex(cwd);
      if (index.size === 0) return 'No context file found. Run: node gen-context.js';

      // Extract the defining symbol name from a signature line (same rules as
      // buildSymbolCandidates) so we match definitions, not param occurrences.
      const defName = (sig) => {
        const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
        const m = cleaned.match(/\b(?:async\s+function|function|class|def|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/)
          || cleaned.match(/([A-Za-z_$][\w$]*)\s*\(/);
        return m ? m[1] : null;
      };

      const candidates = buildSymbolCandidates(index);
      const blocks = [];
      for (const symbol of symbols) {
        const matches = [];
        for (const [file, sigs] of index.entries()) {
          for (const sig of sigs) {
            if (defName(sig) === symbol) matches.push(`${sig}    (${file})`);
          }
        }
        if (matches.length === 0) {
          const cm = closestMatch(symbol, candidates);
          const hint = cm ? ' — ' + formatSuggestion(cm) : '';
          blocks.push(`### ${symbol}\n_not found in index${hint}_`);
        } else {
          blocks.push(`### ${symbol}\n${matches.join('\n')}`);
        }
      }
      return blocks.join('\n\n');
    } catch (err) {
      return `_get_callee_signatures failed: ${err.message}_`;
    }
  }

  // ── Layer 1: live-index write hooks ────────────────────────────────────────
  // Keep the sig-cache fresh while an agent creates/modifies/deletes files, so
  // new code is discoverable in the same session. buildSigIndex already merges
  // the cache (_buildSigIndexFromCache), so updates are live on the next read.

  function _pkgVersion(cwd) {
    try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
    catch (_) { return '0.0.0'; }
  }


  /** notify_file_created — extract a file's signatures and index it live. */
  function notifyFileCreated(args, cwd) {
    const rel = args && args.path;
    if (!rel) return 'Missing required argument: path';
    try {
      const { extractFile } = __require('./src/extractors/dispatch');
      const { loadCache, saveCache } = __require('./src/cache/sig-cache');
      const abs = path.resolve(cwd, rel);
      let content = args.content;
      if (typeof content !== 'string') {
        try { content = fs.readFileSync(abs, 'utf8'); } catch (_) { content = ''; }
      }
      const sigs = extractFile(abs, content);
      const version = _pkgVersion(cwd);
      const cache = loadCache(cwd, version);
      if (sigs.length > 0) {
        cache.set(abs, { mtime: Date.now(), sigs });
      }
      saveCache(cwd, version, cache);
      return `Indexed ${rel}: ${sigs.length} signature(s) now live.`;
    } catch (err) {
      return `_notify_file_created failed: ${err.message}_`;
    }
  }

  /** notify_symbol_added — append one signature to a file's live cache entry. */
  function notifySymbolAdded(args, cwd) {
    if (!args || !args.signature || !args.file) {
      return 'Missing required arguments: signature, file';
    }
    try {
      const { loadCache, saveCache } = __require('./src/cache/sig-cache');
      const abs = path.resolve(cwd, args.file);
      const version = _pkgVersion(cwd);
      const cache = loadCache(cwd, version);
      const entry = cache.get(abs) || { mtime: Date.now(), sigs: [] };
      const line = Number.isFinite(Number(args.line)) ? `  :${args.line}` : '';
      const sig = String(args.signature) + line;
      if (!entry.sigs.includes(sig)) entry.sigs.push(sig);
      entry.mtime = Date.now();
      cache.set(abs, entry);
      saveCache(cwd, version, cache);
      return `Added signature to ${args.file} (${entry.sigs.length} total).`;
    } catch (err) {
      return `_notify_symbol_added failed: ${err.message}_`;
    }
  }

  /** notify_file_deleted — drop a file's cache-overlay entry. */
  function notifyFileDeleted(args, cwd) {
    const rel = args && args.path;
    if (!rel) return 'Missing required argument: path';
    try {
      const { loadCache, saveCache } = __require('./src/cache/sig-cache');
      const abs = path.resolve(cwd, rel);
      const version = _pkgVersion(cwd);
      const cache = loadCache(cwd, version);
      const had = cache.delete(abs);
      saveCache(cwd, version, cache);
      return had ? `Removed ${rel} from the live index.` : `${rel} was not in the live cache.`;
    } catch (err) {
      return `_notify_file_deleted failed: ${err.message}_`;
    }
  }

  /**
   * List the files changed in the working tree, staged area, or vs a base ref.
   * Shell-free (routes through src/util/git.js). Returns relative paths.
   */
  function _changedFiles(cwd, args) {
    const { tryGit } = __require('./src/util/git');
    let out = '';
    if (args.base) {
      if (!/^[A-Za-z0-9._/\-~^]+$/.test(args.base)) return [];
      out = tryGit(['diff', `${args.base}..HEAD`, '--name-only'], { cwd });
    } else if (args.staged) {
      out = tryGit(['diff', '--cached', '--name-only'], { cwd });
    } else {
      out = tryGit(['diff', 'HEAD', '--name-only'], { cwd });
    }
    return out.split('\n').map((s) => s.trim()).filter(Boolean);
  }

  /**
   * get_diff_context({ base?, staged?, depth? }) → string
   *
   * For each changed file: its extracted signatures + blast radius (direct
   * importers, transitive count, affected tests/routes) + a risk label.
   * Hands an agent everything a review or a safe edit needs in one call.
   */
  function getDiffContext(args, cwd) {
    try {
      const a = args || {};
      const files = _changedFiles(cwd, a);
      if (files.length === 0) {
        return '_No changed files detected._ (Outside a git repo, or the working tree / selected range is clean.)';
      }

      const { extractFile, langFor } = __require('./src/extractors/dispatch');
      const { analyzeImpact } = __require('./src/graph/impact');
      let riskLabelFor;
      try { ({ riskLabelFor } = __require('./src/evidence/pack')); } catch (_) { riskLabelFor = () => 'source'; }

      const depth = Math.max(0, parseInt(a.depth, 10) || 2);
      const srcFiles = files.filter((f) => langFor(f));
      let impactByFile = new Map();
      try {
        const impacts = analyzeImpact(srcFiles, cwd, { depth });
        impactByFile = new Map(impacts.map((r) => [r.file, r.impact]));
      } catch (_) { /* graph optional */ }

      const scope = a.base ? `vs ${a.base}` : (a.staged ? 'staged' : 'working tree');
      const out = [
        `# Diff context (${scope})`,
        '',
        `**${files.length} changed file${files.length === 1 ? '' : 's'}** · ${srcFiles.length} with extractable signatures`,
        '',
      ];

      for (const rel of files) {
        out.push(`## \`${rel}\``);
        if (!langFor(rel)) { out.push('_non-source file (no signatures)_', ''); continue; }

        out.push(`_risk: ${riskLabelFor(rel)}_`);
        const impact = impactByFile.get(rel);
        if (impact) {
          out.push(
            `**Blast radius:** ${impact.totalImpact} file(s) impacted — ` +
            `${impact.direct.length} direct importer(s), ${impact.transitive.length} transitive` +
            (impact.tests.length ? `, ${impact.tests.length} test(s)` : '') +
            (impact.routes.length ? `, ${impact.routes.length} route(s)` : '')
          );
          if (impact.direct.length) {
            out.push(`Direct importers: ${impact.direct.slice(0, 8).map((f) => '`' + f + '`').join(', ')}` + (impact.direct.length > 8 ? ' …' : ''));
          }
          if (impact.tests.length) {
            out.push(`Tests to run: ${impact.tests.slice(0, 8).map((f) => '`' + f + '`').join(', ')}`);
          }
        } else {
          out.push('**Blast radius:** (not in dependency graph — new or leaf file)');
        }
        out.push('');

        let src = '';
        try { src = fs.readFileSync(path.resolve(cwd, rel), 'utf8'); } catch (_) {}
        const sigs = src ? extractFile(rel, src) : [];
        if (sigs.length) {
          out.push('```');
          for (const s of sigs.slice(0, 40)) out.push(s);
          if (sigs.length > 40) out.push(`… +${sigs.length - 40} more`);
          out.push('```');
        } else {
          out.push('_(no signatures extracted — file may be deleted or empty)_');
        }
        out.push('');
      }

      return out.join('\n');
    } catch (err) {
      return `_get_diff_context failed: ${err.message}_`;
    }
  }

  /**
   * get_architecture_overview({}) → string
   *
   * A high-level map of the codebase: module breakdown (files/tokens), the most
   * depended-on "hub" files, dependency-cycle count, and route totals. Extends
   * get_map — one call to orient in an unfamiliar repo.
   */
  function getArchitectureOverview(args, cwd) {
    try {
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      const index = buildSigIndex(cwd);
      const out = ['# Architecture overview', ''];

      if (index.size === 0) {
        out.push('_No context file found. Run: node gen-context.js_', '');
      } else {
        const groups = {};
        let totalTokens = 0;
        let totalFiles = 0;
        for (const [rel, sigs] of index.entries()) {
          const parts = rel.replace(/\\/g, '/').split('/');
          const mod = parts.length > 1 ? parts[0] : '.';
          const tok = Math.ceil(sigs.join('\n').length / 4);
          if (!groups[mod]) groups[mod] = { files: 0, tokens: 0 };
          groups[mod].files++;
          groups[mod].tokens += tok;
          totalTokens += tok;
          totalFiles++;
        }
        const sorted = Object.entries(groups)
          .map(([mod, d]) => ({ mod, files: d.files, tokens: d.tokens }))
          .sort((a, b) => b.tokens - a.tokens);

        out.push(`**${totalFiles} indexed files · ${sorted.length} modules · ~${totalTokens} tokens**`, '');
        out.push('## Modules', '| Module | Files | Tokens |', '|--------|-------|--------|');
        for (const m of sorted.slice(0, 20)) out.push(`| ${m.mod} | ${m.files} | ~${m.tokens} |`);
        out.push('');
      }

      // Hub files + cycle count from the dependency graph (optional).
      try {
        const { buildFromCwd } = __require('./src/graph/builder');
        const { detectCycles } = __require('./src/map/import-graph');
        const graph = buildFromCwd(cwd);
        if (graph && graph.reverse && graph.reverse.size) {
          const hubs = [...graph.reverse.entries()]
            .map(([f, importers]) => ({ file: path.relative(cwd, f).replace(/\\/g, '/'), in: importers.length }))
            .filter((h) => h.in > 0)
            .sort((a, b) => b.in - a.in)
            .slice(0, 10);
          if (hubs.length) {
            out.push('## Hub files (most depended-on)', '| File | Importers |', '|------|-----------|');
            for (const h of hubs) out.push(`| \`${h.file}\` | ${h.in} |`);
            out.push('');
          }
          let cycleCount = 0;
          try { cycleCount = detectCycles(graph.forward).length; } catch (_) {}
          out.push(`**Dependency cycles:** ${cycleCount}` + (cycleCount ? ' _(see import graph)_' : ' — none detected'), '');
        }
      } catch (_) { /* graph optional */ }

      // Routes from PROJECT_MAP.md if present.
      const mapPath = path.join(cwd, 'PROJECT_MAP.md');
      if (fs.existsSync(mapPath)) {
        const mc = fs.readFileSync(mapPath, 'utf8');
        const routeCount = mc.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---')).length;
        out.push('## Project map', `Routes detected: ${routeCount} _(use get_map for imports/classes/routes detail)_`, '');
      } else {
        out.push('_Run `node gen-project-map.js` for routes / class-hierarchy detail (get_map)._');
      }

      return out.join('\n');
    } catch (err) {
      return `_get_architecture_overview failed: ${err.message}_`;
    }
  }

  /**
   * verify_suggestion({ code }) → string
   *
   * Ground an AI code suggestion before it is written: run the Hallucination
   * Guard against the repo AND the installed-library symbol index (the moat), and
   * render a verdict + issues + the installed libraries verified against (pinned
   * versions, D8). Deterministic, offline.
   */
  function verifySuggestion(args, cwd) {
    const code = args && typeof args.code === 'string' ? args.code : '';
    if (!code.trim()) {
      return 'Usage: verify_suggestion({ code: "<AI-suggested code or answer>" }) — provide the snippet to verify against the repo + installed libraries.';
    }

    let result;
    try {
      const { verify } = __require('./src/verify/hallucination-guard');
      result = verify(code, cwd);
    } catch (err) {
      return `_verify_suggestion failed: ${err.message}_`;
    }

    const { issues, summary } = result;
    const out = [];
    if (summary.clean) {
      out.push('✓ Grounded — no fake files, imports, symbols, or scripts detected.');
    } else {
      out.push(`✗ ${summary.total} issue(s) found:`);
      for (const i of issues) {
        out.push(`  L${i.line}  [${i.type}]  ${i.message}`);
        if (i.suggestion) out.push(`         ↳ ${i.suggestion}`);
      }
    }

    // D8: report the installed libraries the suggestion was verified against.
    const pins = (summary.libraries || []).filter((l) => l.version).map((l) => `${l.name}@${l.version}`);
    const n = summary.librariesIndexed || 0;
    out.push('');
    out.push(
      `Grounded against ${summary.symbolsIndexed} repo + library symbol(s)` +
      (n ? ` · ${n} installed librar${n === 1 ? 'y' : 'ies'}${pins.length ? ': ' + pins.join(', ') : ''}` : '')
    );
    return out.join('\n');
  }

  module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion };
  
};

// ── ./src/mcp/install ──
__factories["./src/mcp/install"] = function(module, exports) {
  
  /**
   * Per-client MCP install (v8.0 E4).
   *
   * `sigmap --setup` wires *every* known editor at once and only touches config
   * files that already exist (to avoid creating clutter for editors the user does
   * not use). This module is the targeted counterpart: `sigmap mcp install <client>`
   * picks one client, and — because the user explicitly asked for it — CREATES the
   * config dir/file if it is missing. Idempotent: re-running never duplicates the
   * entry. Zero dependencies; only `fs`/`path`/`os`.
   */

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

  // Config shapes the supported clients use.
  //  - 'json'  → { mcpServers: { sigmap: { command, args } } }
  //  - 'zed'   → { context_servers: { sigmap: { command: { path, args } } } }
  //  - 'yaml'  → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
  const CLIENTS = {
    claude:   { label: 'Claude Code',  format: 'json', scope: 'project', project: ['.claude', 'settings.json'] },
    cursor:   { label: 'Cursor',       format: 'json', scope: 'project', project: ['.cursor', 'mcp.json'] },
    windsurf: { label: 'Windsurf',     format: 'json', scope: 'both',
                project: ['.windsurf', 'mcp.json'],
                global:  ['.codeium', 'windsurf', 'mcp_config.json'] },
    vscode:   { label: 'VS Code',      format: 'json', scope: 'project', project: ['.vscode', 'mcp.json'] },
    opencode: { label: 'OpenCode',     format: 'json', scope: 'both',
                project: ['opencode.json'],
                global:  ['.config', 'opencode', 'config.json'] },
    gemini:   { label: 'Gemini CLI',   format: 'json', scope: 'global', global: ['.gemini', 'settings.json'] },
    zed:      { label: 'Zed',          format: 'zed',  scope: 'global', global: ['.config', 'zed', 'settings.json'] },
    codex:    { label: 'Codex CLI',    format: 'yaml', scope: 'global', global: ['.codex', 'config.yaml'] },
    mcp:      { label: 'Portable (.mcp.json)', format: 'json', scope: 'project', project: ['.mcp.json'] },
  };

  /** Resolve the absolute config path for a client, honoring `global`. */
  function resolveTarget(spec, cwd, home, useGlobal) {
    const wantGlobal = useGlobal || spec.scope === 'global';
    if (wantGlobal && spec.global) return path.join(home, ...spec.global);
    if (spec.project) return path.join(cwd, ...spec.project);
    if (spec.global)  return path.join(home, ...spec.global);
    return null;
  }

  /** List supported clients with their resolved target paths. */
  function listClients(opts = {}) {
    const cwd  = opts.cwd  || process.cwd();
    const home = opts.home || os.homedir();
    return Object.keys(CLIENTS).map((key) => {
      const spec = CLIENTS[key];
      return {
        client: key,
        label:  spec.label,
        scope:  spec.scope,
        format: spec.format,
        target: resolveTarget(spec, cwd, home, false),
        globalTarget: spec.scope === 'both' ? resolveTarget(spec, cwd, home, true) : null,
      };
    });
  }

  function serverArgs(scriptPath) {
    return [path.resolve(scriptPath), '--mcp'];
  }

  /** Install into a JSON `mcpServers` config (create file/dir if absent). */
  function _installJson(filePath, scriptPath) {
    let settings = {};
    if (fs.existsSync(filePath)) {
      try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
      catch (_) { settings = {}; }
    }
    if (!settings.mcpServers) settings.mcpServers = {};
    if (settings.mcpServers.sigmap) return 'already';
    settings.mcpServers.sigmap = { command: 'node', args: serverArgs(scriptPath) };
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
    return 'installed';
  }

  /** Install into Zed's `context_servers` config (create file/dir if absent). */
  function _installZed(filePath, scriptPath) {
    let settings = {};
    if (fs.existsSync(filePath)) {
      try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
      catch (_) { settings = {}; }
    }
    if (!settings.context_servers) settings.context_servers = {};
    if (settings.context_servers.sigmap) return 'already';
    settings.context_servers.sigmap = { command: { path: 'node', args: serverArgs(scriptPath) } };
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
    return 'installed';
  }

  /** Install into Codex CLI YAML (append block; create file if absent). */
  function _installYaml(filePath, scriptPath) {
    let raw = '';
    if (fs.existsSync(filePath)) {
      raw = fs.readFileSync(filePath, 'utf8');
      if (raw.includes('sigmap')) return 'already';
    }
    const block = [
      'mcpServers:',
      '  sigmap:',
      '    command: node',
      '    args:',
      `      - ${path.resolve(scriptPath)}`,
      '      - --mcp',
    ].join('\n');
    const next = raw ? raw.trimEnd() + '\n\n' + block + '\n' : block + '\n';
    fs.mkdirSync(path.dirname(filePath), { recursive: true });
    fs.writeFileSync(filePath, next);
    return 'installed';
  }

  /**
   * Install the sigmap MCP server for a single client.
   * @returns { client, label, path, status } where status is
   *   'installed' | 'already' | 'unknown'.
   */
  function installClient(client, opts = {}) {
    const spec = CLIENTS[client];
    if (!spec) {
      return { client, status: 'unknown', valid: Object.keys(CLIENTS) };
    }
    const cwd        = opts.cwd  || process.cwd();
    const home       = opts.home || os.homedir();
    const scriptPath = opts.scriptPath || path.join(cwd, 'gen-context.js');
    const filePath   = resolveTarget(spec, cwd, home, opts.global);

    let status;
    if (spec.format === 'zed')       status = _installZed(filePath, scriptPath);
    else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
    else                             status = _installJson(filePath, scriptPath);

    return { client, label: spec.label, path: filePath, status };
  }

  module.exports = { CLIENTS, listClients, installClient, resolveTarget };
  
};

// ── ./src/mcp/server ──
__factories["./src/mcp/server"] = function(module, exports) {
  
  /**
   * SigMap MCP server — zero npm dependencies.
   *
   * Wire protocol: JSON-RPC 2.0 over stdio.
   * One JSON object per line on both stdin and stdout.
   *
   * Supported methods:
   *   initialize        → serverInfo + capabilities
   *   tools/list        → 18 tool definitions
   *   tools/call        → dispatch to handler, return result
   */

  const readline = require('readline');
  const { TOOLS } = __require('./src/mcp/tools');
  const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion } = __require('./src/mcp/handlers');

  const SERVER_INFO = {
    name: 'sigmap',
    version: '8.7.1',
    description: 'SigMap MCP server — code signatures on demand',
  };

  // ---------------------------------------------------------------------------
  // JSON-RPC helpers
  // ---------------------------------------------------------------------------
  function respond(id, result) {
    process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
  }

  function respondError(id, code, message) {
    process.stdout.write(
      JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
    );
  }

  // ---------------------------------------------------------------------------
  // Method dispatcher
  // ---------------------------------------------------------------------------
  function dispatch(msg, cwd) {
    const { method, id, params } = msg;

    // Notifications (no id) need no response
    if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
      return;
    }

    if (method === 'initialize') {
      respond(id, {
        protocolVersion: (params && params.protocolVersion) || '2024-11-05',
        serverInfo: SERVER_INFO,
        capabilities: { tools: {} },
      });
      return;
    }

    if (method === 'tools/list') {
      respond(id, { tools: TOOLS });
      return;
    }

    if (method === 'tools/call') {
      const name = params && params.name;
      const args = (params && params.arguments) || {};

      let text;
      try {
        if (name === 'read_context') text = readContext(args, cwd);
        else if (name === 'search_signatures') text = searchSignatures(args, cwd);
        else if (name === 'get_map') text = getMap(args, cwd);
        else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
        else if (name === 'get_routing') text = getRouting(args, cwd);
        else if (name === 'explain_file') text = explainFile(args, cwd);
        else if (name === 'list_modules') text = listModules(args, cwd);
        else if (name === 'query_context') text = queryContext(args, cwd);
        else if (name === 'get_impact') text = getImpact(args, cwd);
        else if (name === 'get_lines') text = getLines(args, cwd);
        else if (name === 'read_memory') text = readMemory(args, cwd);
        else if (name === 'get_callee_signatures') text = getCalleeSignatures(args, cwd);
        else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
        else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
        else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
        else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
        else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
        else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
        else {
          respondError(id, -32601, `Unknown tool: ${name}`);
          return;
        }
      } catch (err) {
        respondError(id, -32603, `Tool error: ${err.message}`);
        return;
      }

      respond(id, {
        content: [{ type: 'text', text: String(text) }],
      });
      return;
    }

    // Unknown method
    if (id !== undefined && id !== null) {
      respondError(id, -32601, `Method not found: ${method}`);
    }
  }

  // ---------------------------------------------------------------------------
  // Server entry point
  // ---------------------------------------------------------------------------
  function start(cwd) {
    const rl = readline.createInterface({ input: process.stdin, terminal: false });

    rl.on('line', (line) => {
      const trimmed = line.trim();
      if (!trimmed) return;

      let msg;
      try {
        msg = JSON.parse(trimmed);
      } catch (_) {
        // Cannot respond without a valid id — ignore malformed input
        return;
      }

      try {
        dispatch(msg, cwd);
      } catch (err) {
        const id = (msg && msg.id) != null ? msg.id : null;
        respondError(id, -32603, `Internal error: ${err.message}`);
      }
    });

    rl.on('close', () => {
      process.exit(0);
    });
  }

  module.exports = { start };
  
};

// ── ./src/mcp/tools ──
__factories["./src/mcp/tools"] = function(module, exports) {
  
  /**
   * MCP tool definitions for SigMap (17 tools).
   * read_context, search_signatures, get_map, create_checkpoint, get_routing,
   * explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
   * get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
   * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview.
   */

  const TOOLS = [
    {
      name: 'read_context',
      description:
        'Read extracted code signatures for the project or a specific module path. ' +
        'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
        'filtered subset when a module path is provided (~50–500 tokens).',
      inputSchema: {
        type: 'object',
        properties: {
          module: {
            type: 'string',
            description:
              'Optional subdirectory path to scope results (e.g. "src/services"). ' +
              'Omit to get the full codebase context.',
          },
        },
        required: [],
      },
    },
    {
      name: 'search_signatures',
      description:
        'Search extracted code signatures for a keyword, function name, or class name. ' +
        'Returns matching signature lines with their file paths.',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'Keyword to search for in signatures (case-insensitive).',
          },
        },
        required: ['query'],
      },
    },
    {
      name: 'get_map',
      description:
        'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
        'Requires gen-project-map.js to have been run first.',
      inputSchema: {
        type: 'object',
        properties: {
          type: {
            type: 'string',
            enum: ['imports', 'classes', 'routes'],
            description: 'Which section to retrieve: imports, classes, or routes.',
          },
        },
        required: ['type'],
      },
    },
    {
      name: 'create_checkpoint',
      description:
        'Create a session checkpoint summarising current project state. ' +
        'Returns recent git commits, active branch, token count, and a ' +
        'compact snapshot of the codebase context — ideal for session handoffs ' +
        'or periodic saves during long coding sessions.',
      inputSchema: {
        type: 'object',
        properties: {
          note: {
            type: 'string',
            description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
          },
        },
        required: [],
      },
    },
    {
      name: 'get_routing',
      description:
        'Get model routing hints for this project — which files belong to which complexity ' +
        'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
        'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
      inputSchema: {
        type: 'object',
        properties: {},
        required: [],
      },
    },
    {
      name: 'explain_file',
      description:
        'Explain a specific file: returns its extracted signatures, direct imports ' +
        '(files it depends on), and callers (files that import it). ' +
        'Ideal for understanding a file in isolation without reading raw source. ' +
        'Requires the context file to have been generated first.',
      inputSchema: {
        type: 'object',
        properties: {
          path: {
            type: 'string',
            description:
              'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
              'Use the paths shown in read_context output.',
          },
        },
        required: ['path'],
      },
    },
    {
      name: 'list_modules',
      description:
        'List all top-level modules (srcDirs) present in the context file, ' +
        'sorted by token count descending. Use this to decide which module to ' +
        'pass to read_context before querying a specific area of the codebase.',
      inputSchema: {
        type: 'object',
        properties: {},
        required: [],
      },
    },
    {
      name: 'query_context',
      description:
        'Rank and return the most relevant files for a specific task or question. ' +
        'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
        'to the query — much cheaper than reading all context. ' +
        'Returns ranked file list with signatures and relevance scores.',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description:
              'Natural language task description or keyword(s) to rank files against. ' +
              'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
          },
          topK: {
            type: 'number',
            description: 'Maximum number of files to return (default: 10, max: 25).',
          },
        },
        required: ['query'],
      },
    },
    {
      name: 'get_impact',
      description:
        'Show every file that is impacted when a given file changes — direct importers, ' +
        'transitive importers, affected tests, and affected routes/controllers. ' +
        'Gives agents instant blast-radius awareness before making a change. ' +
        'Handles circular dependencies safely (no infinite loops).',
      inputSchema: {
        type: 'object',
        properties: {
          file: {
            type: 'string',
            description:
              'Relative path from the project root of the file that changed ' +
              '(e.g. "src/extractors/python.js"). Use forward slashes.',
          },
          depth: {
            type: 'number',
            description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
          },
        },
        required: ['file'],
      },
    },
    {
      name: 'get_lines',
      description:
        'Fetch an exact line range from a source file on demand — the Surgical Context ' +
        'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
        'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
        'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
      inputSchema: {
        type: 'object',
        properties: {
          file: {
            type: 'string',
            description:
              'Relative path from the project root (e.g. "src/config/loader.js"). ' +
              'Use the path shown in a signature anchor. Use forward slashes.',
          },
          start: {
            type: 'number',
            description: '1-based start line (inclusive). Clamped to the file bounds.',
          },
          end: {
            type: 'number',
            description: '1-based end line (inclusive). Clamped to the file bounds.',
          },
        },
        required: ['file', 'start', 'end'],
      },
    },
    {
      name: 'read_memory',
      description:
        'Recall the project decision log — recent notes left by humans or agents ' +
        'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
        'Call this at the start of a task to kill cold-start: it answers ' +
        '"what were we doing and why" without re-reading the whole codebase.',
      inputSchema: {
        type: 'object',
        properties: {
          limit: {
            type: 'number',
            description: 'How many of the most recent notes to return (default: 10, max: 50).',
          },
        },
        required: [],
      },
    },
    {
      name: 'get_callee_signatures',
      description:
        'Return the EXACT current signature(s) of named symbols (functions, classes, ' +
        "methods) from the index — so an agent never guesses a callee's parameter types " +
        'from training memory. Call this before writing code that uses a symbol. ' +
        'Unknown names get a closest-match suggestion.',
      inputSchema: {
        type: 'object',
        properties: {
          symbols: {
            type: 'array',
            items: { type: 'string' },
            description: 'Symbol names to resolve (e.g. ["validateToken", "UserService"]).',
          },
        },
        required: ['symbols'],
      },
    },
    {
      name: 'sigmap_notify_file_created',
      description:
        'Tell SigMap a file was created or modified so its signatures are indexed ' +
        'live for the rest of the session. Call this after writing a file — the new ' +
        'symbols become resolvable by search_signatures / get_callee_signatures.',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'File path relative to the project root.' },
          content: { type: 'string', description: 'Optional file content; read from disk if omitted.' },
        },
        required: ['path'],
      },
    },
    {
      name: 'sigmap_notify_symbol_added',
      description:
        'Fast path: register a single new symbol signature directly in the live ' +
        'index without re-reading the whole file.',
      inputSchema: {
        type: 'object',
        properties: {
          signature: { type: 'string', description: 'The signature line (e.g. "function check(key)").' },
          file: { type: 'string', description: 'File path the symbol belongs to (relative to root).' },
          line: { type: 'number', description: 'Optional 1-based line number.' },
        },
        required: ['signature', 'file'],
      },
    },
    {
      name: 'sigmap_notify_file_deleted',
      description:
        'Tell SigMap a file was deleted so its symbols are dropped from the live index.',
      inputSchema: {
        type: 'object',
        properties: {
          path: { type: 'string', description: 'Deleted file path relative to the project root.' },
        },
        required: ['path'],
      },
    },
    {
      name: 'get_diff_context',
      description:
        'For every changed file in the working tree (or staged, or vs a base ref), return its ' +
        'current signatures plus blast radius — direct importers, transitive count, and affected ' +
        'tests/routes — with a risk label. One call gives an agent everything a code review or a ' +
        'safe edit needs. Lists changed files shell-free (git binary, never a shell).',
      inputSchema: {
        type: 'object',
        properties: {
          base: {
            type: 'string',
            description: 'Optional git ref to diff against (e.g. "main"). Returns files changed in `base..HEAD`. Omit for working-tree changes.',
          },
          staged: {
            type: 'boolean',
            description: 'When true (and no base), report only staged changes (`git diff --cached`).',
          },
          depth: {
            type: 'number',
            description: 'Blast-radius BFS depth limit (default: 2). Use 0 for unlimited.',
          },
        },
        required: [],
      },
    },
    {
      name: 'get_architecture_overview',
      description:
        'A high-level map of the codebase in one call: module breakdown (files/tokens), the most ' +
        'depended-on "hub" files, the dependency-cycle count, and route totals. Extends get_map — ' +
        'use it to orient in an unfamiliar repo before drilling in with read_context / query_context.',
      inputSchema: {
        type: 'object',
        properties: {},
        required: [],
      },
    },
    {
      name: 'verify_suggestion',
      description:
        'Ground an AI code suggestion before writing it: verify a snippet or answer against the ' +
        'repository AND the libraries actually installed in node_modules (the grounding moat). ' +
        'Flags fake file paths, unresolvable imports, symbols absent from both the repo index and ' +
        'the installed libraries, and non-existent npm scripts — deterministic, offline, no LLM. ' +
        'Reports the installed libraries it verified against with pinned versions.',
      inputSchema: {
        type: 'object',
        properties: {
          code: {
            type: 'string',
            description:
              'The AI-suggested code snippet or answer text to verify against the repo + installed libraries.',
          },
        },
        required: ['code'],
      },
    },
  ];

  module.exports = { TOOLS };
  
};

// ── ./src/nudge ──
__factories["./src/nudge"] = function(module, exports) {
  
  /**
   * Star nudge + usage tracking (v7.0.0).
   *
   * Records run counts in `.context/usage.json` and shows a one-time GitHub-star
   * message after the tool has been genuinely useful (≥10 runs, ≥8 successes).
   * Shown exactly once per machine — even under concurrent runs (an `wx` lock
   * file makes the show race-safe). Wired into `ask` (and the `squeeze` path).
   */

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

  const RUN_THRESHOLD = 10;
  const SUCCESS_THRESHOLD = 8;

  function usagePath(cwd) { return path.join(cwd, '.context', 'usage.json'); }

  function defaultUsage() {
    return {
      totalRuns: 0, successfulRuns: 0, squeezeOffered: 0, squeezeAccepted: 0,
      starNudgeShown: false, firstRunDate: null, lastRunDate: null,
    };
  }

  function readUsage(cwd) {
    try { return { ...defaultUsage(), ...JSON.parse(fs.readFileSync(usagePath(cwd), 'utf8')) }; }
    catch (_) { return defaultUsage(); }
  }

  function writeUsageAtomic(cwd, usage) {
    const p = usagePath(cwd);
    fs.mkdirSync(path.dirname(p), { recursive: true });
    const tmp = `${p}.${process.pid}.tmp`;
    fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
    fs.renameSync(tmp, p); // atomic on POSIX
  }

  const STAR_MESSAGE = [
    '─────────────────────────────────────────────────────────',
    '  SigMap has helped you 10 times now.',
    '',
    "  If it's been useful, a GitHub star takes 5 seconds and",
    '  helps other developers find it:',
    '  → github.com/manojmallick/sigmap',
    '',
    "  (Won't ask again. Press Enter to continue.)",
    '─────────────────────────────────────────────────────────',
  ].join('\n');

  function showStarNudge(write) {
    (write || ((s) => process.stderr.write(s)))('\n' + STAR_MESSAGE + '\n');
  }

  /**
   * Record one run and, when the thresholds are first met, show the star nudge.
   * @param {string} cwd
   * @param {boolean} runSuccess
   * @param {object} [opts]
   * @param {boolean} [opts.silent]   record only — never print
   * @param {function} [opts.write]   sink for the message (default stderr)
   * @param {string} [opts.today]     override date (testing)
   * @param {object} [opts.bump]      counter deltas to merge (e.g. { squeezeAccepted: 1 })
   * @returns {{ usage, nudged }}
   */
  function checkStarNudge(cwd, runSuccess, opts = {}) {
    const usage = readUsage(cwd);
    usage.totalRuns += 1;
    if (runSuccess) usage.successfulRuns += 1;
    if (opts.bump) for (const k of Object.keys(opts.bump)) usage[k] = (usage[k] || 0) + opts.bump[k];

    const today = opts.today || new Date().toISOString().slice(0, 10);
    if (!usage.firstRunDate) usage.firstRunDate = today;
    usage.lastRunDate = today;

    let nudged = false;
    if (!usage.starNudgeShown && usage.totalRuns >= RUN_THRESHOLD && usage.successfulRuns >= SUCCESS_THRESHOLD) {
      // Race-safe single-show: only the process that creates the lock prints.
      let won = false;
      try { fs.closeSync(fs.openSync(usagePath(cwd) + '.nudge.lock', 'wx')); won = true; }
      catch (_) { won = false; }
      if (won && !opts.silent) showStarNudge(opts.write);
      nudged = won;
      usage.starNudgeShown = true;
    }

    writeUsageAtomic(cwd, usage);
    return { usage, nudged };
  }

  module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRESHOLD, SUCCESS_THRESHOLD };
  
};

// ── ./src/plan/planner ──
__factories["./src/plan/planner"] = function(module, exports) {
  
  const path = require('path');
  const fs = require('fs');
  const { buildFromCwd } = __require('./src/graph/builder');
  const { getImpact } = __require('./src/graph/impact');
  const { buildSigIndex, rank, detectIntent } = __require('./src/retrieval/ranker');
  const { buildTestIndex, isTested } = __require('./src/extractors/coverage');

  module.exports = { createPlan };

  function createPlan(goal, cwd, config) {
    // Step 1: Detect intent and rank files for the goal
    const intent = detectIntent(goal);
    const sigIndex = buildSigIndex(cwd);
    if (sigIndex.size === 0) {
      return { error: 'no context found' };
    }

    const ranked = rank(goal, sigIndex, { topK: 15, cwd });

    // Step 2: Separate into confidence levels
    const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
    const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);

    // Step 3: Compute impact radius for highest-confidence file
    let impact = null;
    if (highConf.length > 0) {
      const entryFile = highConf[0].file;
      try {
        const graph = buildFromCwd(cwd);
        impact = getImpact(entryFile, graph, { maxDepth: 3, cwd });
      } catch (_) {
        // Graph build failed, continue without impact
      }
    }

    // Step 4: Identify likely-affected tests
    let testedFiles = [];
    try {
      const testIndex = buildTestIndex(cwd, config.testDirs || ['test', 'tests', '__tests__', 'spec']);
      testedFiles = highConf.filter(r => {
        const sigs = r.sigs || [];
        const fnNames = sigs.map(s => {
          const m = s.match(/(?:function|def|fn)\s+(\w+)/);
          return m ? m[1] : null;
        }).filter(Boolean);
        return fnNames.some(fn => isTested(fn, testIndex));
      });
    } catch (_) {
      // Coverage index failed, continue without test info
    }

    return {
      goal,
      intent,
      inspectFirst: highConf.map(r => r.file),
      likelyToChange: medConf.map(r => r.file),
      impactRadius: impact ? {
        direct: [...(impact.direct || [])],
        transitive: [...(impact.transitive || [])],
      } : null,
      testsAffected: testedFiles.map(r => r.file),
    };
  }
  
};

// ── ./src/plan/verify-plan ──
__factories["./src/plan/verify-plan"] = function(module, exports) {
  
  /**
   * verify-plan (IMPL.md §6.1 — Gap 2, step 2 of the `create` pipeline).
   *
   * Checks a plan (markdown) against the LIVE index before execution: do the
   * referenced files and symbols exist, is the blast radius acceptable, is the
   * scope in bounds? Catches Cause 1+2 at plan time — cheaper than after the
   * code is written. Reuses the verify primitives + the impact graph.
   * Zero-dependency, bundle-safe.
   */

  const fs = require('fs');
  const path = require('path');
  const { extractFilePaths, extractSymbols } = __require('./src/verify/parsers');
  const { buildSymbolSet } = __require('./src/verify/hallucination-guard');
  const { closestMatch } = __require('./src/verify/closest-match');
  const { analyzeImpact } = __require('./src/graph/impact');

  const DEFAULT_BLAST_THRESHOLD = 20; // transitive+direct dependents → "high blast radius"
  const DEFAULT_SCOPE_THRESHOLD = 10; // distinct referenced files → "broad scope"

  /** Resolve a referenced path against cwd (handles a leading "./"). */
  function _fileExists(cwd, ref) {
    const clean = ref.replace(/^\.\//, '');
    for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
      try { if (fs.existsSync(c)) return true; } catch (_) {}
    }
    return false;
  }

  /**
   * Verify a plan against the live index.
   * @param {string} planText the plan as markdown
   * @param {string} cwd repo root
   * @param {object} [opts]
   * @param {number} [opts.blastThreshold=20]
   * @param {number} [opts.scopeThreshold=10]
   * @param {(ref:string)=>boolean} [opts.fileExists] override for testing
   * @returns {{ issues: object[], blast: object[], scope: object, summary: object }}
   */
  function verifyPlan(planText, cwd, opts = {}) {
    const blastThreshold = opts.blastThreshold != null ? opts.blastThreshold : DEFAULT_BLAST_THRESHOLD;
    const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : DEFAULT_SCOPE_THRESHOLD;
    const fileExists = opts.fileExists || ((ref) => _fileExists(cwd, ref));

    const text = String(planText || '');
    const filesRef = extractFilePaths(text);   // [{ path, line }]
    const symbolsRef = extractSymbols(text);   // [{ name, line }]
    const { set: symbolSet, symbolCandidates } = buildSymbolSet(cwd);

    const issues = [];

    // 1. Referenced files must exist.
    const existingFiles = [];
    for (const f of filesRef) {
      if (fileExists(f.path)) existingFiles.push(f.path);
      else issues.push({ type: 'missing-file', ref: f.path, line: f.line, severity: 'error' });
    }

    // 2. Referenced symbols must exist in the live index (suggest a near match).
    for (const s of symbolsRef) {
      if (symbolSet.has(s.name)) continue;
      const match = closestMatch(s.name, symbolCandidates);
      issues.push({
        type: 'unknown-symbol', ref: s.name, line: s.line, severity: 'error',
        suggestion: match ? match.name : null,
      });
    }

    // 3. Blast radius for each existing referenced file (one graph build).
    const blast = [];
    if (existingFiles.length) {
      let impacts = [];
      try { impacts = analyzeImpact(existingFiles, cwd, {}); } catch (_) { impacts = []; }
      for (const { file, impact } of impacts) {
        const entry = { file, totalImpact: impact.totalImpact, tests: impact.tests.length };
        blast.push(entry);
        if (impact.totalImpact > blastThreshold) {
          issues.push({ type: 'high-blast-radius', ref: file, count: impact.totalImpact, severity: 'warn' });
        }
      }
      blast.sort((a, b) => b.totalImpact - a.totalImpact);
    }

    // 4. Scope.
    const scope = { files: filesRef.length, symbols: symbolsRef.length, threshold: scopeThreshold };
    if (filesRef.length > scopeThreshold) {
      issues.push({ type: 'broad-scope', count: filesRef.length, threshold: scopeThreshold, severity: 'warn' });
    }

    const errors = issues.filter((i) => i.severity === 'error').length;
    const warnings = issues.filter((i) => i.severity === 'warn').length;
    return {
      issues,
      blast,
      scope,
      summary: {
        filesReferenced: filesRef.length,
        symbolsReferenced: symbolsRef.length,
        errors,
        warnings,
        ok: errors === 0,
      },
    };
  }

  module.exports = { verifyPlan, DEFAULT_BLAST_THRESHOLD, DEFAULT_SCOPE_THRESHOLD };
  
};

// ── ./src/retrieval/bm25 ──
__factories["./src/retrieval/bm25"] = function(module, exports) {
  
  /**
   * SigMap identifier-aware BM25 re-ranker (zero dependencies, deterministic).
   *
   * Plain exact-token TF-IDF misses queries whose terms live *inside* code
   * identifiers — e.g. `component emit` never surfaces `componentEmits.ts`,
   * because "componentEmits" is one token that shares no exact term with the
   * query. This module fixes that with four small additions:
   *
   *   1. Identifier-aware tokenization — split camelCase and snake_case.
   *   2. Light stemming — plurals / common suffixes (`emits` → `emit`).
   *   3. Path-token boost — file path / basename tokens weigh PATH_BOOST× more.
   *   4. BM25 scoring instead of raw TF-IDF (length-normalized).
   *
   * On 85 curated tasks across 17 repos this lifted hit@5 from 75.3% → 82.4%
   * (MRR +16% relative). See issue #395.
   */

  // Stop words: common English + low-signal code verbs/nouns that appear in
  // nearly every signature and so carry little retrieval signal.
  const STOP = new Set(
    ('a an the of to in on for and or is are be by with as at from that this it its ' +
     'into get set add new return value test')
      .split(' ')
  );

  /**
   * Light suffix stemmer — conservative, tuned for code identifiers rather than
   * prose. Words of 3 chars or fewer pass through unchanged; a result shorter
   * than 3 chars reverts to the original token.
   *
   * @param {string} w
   * @returns {string}
   */
  function stem(w) {
    if (w.length <= 3) return w;
    let s = w;
    s = s.replace(/ies$/, 'y');
    s = s.replace(/(sses|shes|ches|xes|zes)$/, (m) => m.slice(0, -2));
    s = s.replace(/([^s])s$/, '$1');
    s = s.replace(/(ization|izations)$/, 'ize');
    s = s.replace(/(ing|edly|ed|er|ers|ation|ations|ment|ness|ity|ive|able|ible|ize|ise|al)$/, '');
    return s.length >= 3 ? s : w;
  }

  /**
   * Split on non-alphanumeric characters AND camelCase / snake_case boundaries,
   * lowercase, drop stop words and single characters, then stem.
   *
   * @param {string} text
   * @returns {string[]}
   */
  function tokenize(text) {
    if (!text || typeof text !== 'string') return [];
    return text
      .replace(/[^A-Za-z0-9]+/g, ' ')
      .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
      .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
      .toLowerCase()
      .split(/\s+/)
      .filter((t) => t.length > 1 && !STOP.has(t))
      .map(stem)
      .filter(Boolean);
  }

  // The file path / basename is highly indicative of relevance, so its tokens
  // are counted PATH_BOOST times when building the document term-frequency map.
  const PATH_BOOST = 3;

  // Curated, high-precision code-domain synonym / abbreviation expansions. A query
  // for `authentication` should still surface a file whose signatures only say
  // `auth`. Kept deliberately tight — over-broad synonyms hurt precision. Groups
  // are expanded bidirectionally (every member maps to the others). Values are
  // tokenized+stemmed at load, so entries are written in natural form.
  const EXPANSION_GROUPS = [
    ['auth', 'authenticate', 'authentication', 'login', 'signin', 'credential'],
    ['authorize', 'authorization', 'permission', 'access'],
    ['config', 'configuration', 'settings', 'options'],
    ['db', 'database'],
    ['ctx', 'context'],
    ['req', 'request'],
    ['res', 'response'],
    ['err', 'error'],
    ['msg', 'message'],
    ['init', 'initialize', 'initialization', 'setup'],
    ['async', 'asynchronous'],
    ['sync', 'synchronize', 'synchronous'],
    ['repo', 'repository'],
    ['impl', 'implementation'],
    ['util', 'utility', 'helper'],
    ['param', 'parameter', 'argument'],
    ['fn', 'func', 'function'],
    ['btn', 'button'],
    ['calc', 'calculate', 'calculation'],
    ['gen', 'generate', 'generator'],
    ['val', 'validate', 'validation'],
    ['del', 'delete', 'remove'],
    ['dir', 'directory', 'folder'],
    ['env', 'environment'],
    ['doc', 'document', 'documentation'],
    ['id', 'identifier'],
    ['num', 'number'],
    ['str', 'string'],
  ];

  // The weight applied to an expanded (synonym) query term, so an exact match on
  // the literal query token always outranks a synonym-only match.
  const EXPANSION_WEIGHT = 0.15;

  // Build a stemmed lookup: stem(member) → Set of the group's other stemmed members.
  const EXPANSIONS = (() => {
    const map = new Map();
    for (const group of EXPANSION_GROUPS) {
      const stemmed = [...new Set(group.map((w) => tokenize(w).join('')).filter(Boolean))];
      for (const s of stemmed) {
        if (!map.has(s)) map.set(s, new Set());
        for (const other of stemmed) if (other !== s) map.get(s).add(other);
      }
    }
    return map;
  })();

  /**
   * Expand stemmed query tokens with curated synonyms. Returns a Map of
   * token → weight (1 for the original query tokens, EXPANSION_WEIGHT for
   * synonyms). Original tokens always keep full weight even if also a synonym.
   *
   * @param {string[]} qToks  stemmed, de-duplicated query tokens
   * @returns {Map<string, number>}
   */
  function expandQuery(qToks) {
    const weights = new Map();
    for (const t of qToks) weights.set(t, 1);
    for (const t of qToks) {
      const syns = EXPANSIONS.get(t);
      if (!syns) continue;
      for (const s of syns) if (!weights.has(s)) weights.set(s, EXPANSION_WEIGHT);
    }
    return weights;
  }

  /**
   * BM25 re-rank of candidates against a query. Each candidate is
   * `{ file, sigs }`; the returned objects preserve all original candidate
   * fields and add a numeric `score` (higher = more relevant), sorted best-first
   * with a deterministic path tie-break. A `score` of 0 means no query token
   * matched — callers typically drop those.
   *
   * @param {string} query
   * @param {{ file: string, sigs: string[] }[]} candidates
   * @returns {Array<object & { score: number }>}
   */
  function bm25rank(query, candidates) {
    if (!Array.isArray(candidates) || candidates.length === 0) return [];

    const k1 = 1.5;
    const b = 0.75;

    const docs = candidates.map((c) => {
      const pathToks = tokenize(c.file || '');
      const toks = tokenize((c.sigs || []).join(' '));
      for (let i = 0; i < PATH_BOOST; i++) toks.push(...pathToks);
      const tf = new Map();
      for (const t of toks) tf.set(t, (tf.get(t) || 0) + 1);
      return { cand: c, tf, len: toks.length };
    });

    const N = docs.length || 1;
    const avgdl = docs.reduce((s, d) => s + d.len, 0) / N || 1;

    const df = new Map();
    for (const d of docs) {
      for (const t of d.tf.keys()) df.set(t, (df.get(t) || 0) + 1);
    }

    const qToks = [...new Set(tokenize(query))];
    const qWeights = expandQuery(qToks); // token → weight (1 exact, <1 synonym)

    return docs
      .map((d) => {
        let score = 0;
        for (const [t, w] of qWeights) {
          const f = d.tf.get(t);
          if (!f) continue;
          const dfT = df.get(t);
          const idf = Math.log(1 + (N - dfT + 0.5) / (dfT + 0.5));
          score += w * ((idf * (f * (k1 + 1))) / (f + k1 * (1 - b + (b * d.len) / avgdl)));
        }
        return Object.assign({}, d.cand, { score });
      })
      .sort((a, c) => c.score - a.score || String(a.file).localeCompare(String(c.file)));
  }

  module.exports = { tokenize, stem, bm25rank, PATH_BOOST, STOP, expandQuery, EXPANSIONS, EXPANSION_WEIGHT };
  
};

// ── ./src/retrieval/ranker ──
__factories["./src/retrieval/ranker"] = function(module, exports) {
  
  /**
   * SigMap zero-dependency relevance ranker.
   *
   * Ranks all files in a signature index against a natural-language query.
   * Scoring weights:
   *   - keyword overlap (exact token match against sigs)
   *   - symbol match (token appears in a top-level identifier / function name)
   *   - partial prefix match (token is prefix of a sig token, length ≥ 4)
   *   - path relevance (query token appears in the file path)
   *   - recency boost (applied externally via recency map)
   *
   * Usage:
   *   const { rank } = __require('./src/retrieval/src/retrieval/ranker');
   *   const results = rank(query, sigIndex, { topK: 10 });
   *   // results: [{ file, score, sigs, tokens }]
   */

  const { loadWeights } = __require('./src/learning/weights');
  const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
  const { bm25rank } = __require('./src/retrieval/bm25');

  // ---------------------------------------------------------------------------
  // Default weights
  // ---------------------------------------------------------------------------
  const DEFAULT_WEIGHTS = {
    exactToken: 1.0,       // query token exactly in sig tokens
    symbolMatch: 0.5,      // bonus if token appears in a function/class name line
    prefixMatch: 0.3,      // partial prefix hit (query token ≥ 4 chars)
    pathMatch: 0.8,        // query token appears in the file path
    recencyBoost: 1.5,     // multiplier applied when file is in recencySet
    graphBoost: 0.4,       // additive bonus for 1-hop import neighbors of matching files
  };

  // Graph boost amounts for 2-hop traversal with decay (v6.7)
  const GRAPH_BOOST_AMOUNTS = {
    hop1: 0.40,   // direct import neighbor of a file with score > 0
    hop2: 0.15,   // 2 hops away (transitive), with decay
  };

  // Intent-specific weight adjustments
  const INTENT_WEIGHTS = {
    search:     DEFAULT_WEIGHTS,
    debug:      { ...DEFAULT_WEIGHTS, exactToken: 1.2, pathMatch: 0.6 },
    explain:    { ...DEFAULT_WEIGHTS, symbolMatch: 0.8, pathMatch: 0.9 },
    refactor:   { ...DEFAULT_WEIGHTS, symbolMatch: 0.9, exactToken: 0.8 },
    review:     { ...DEFAULT_WEIGHTS, pathMatch: 1.0, exactToken: 0.9 },
    test:       { ...DEFAULT_WEIGHTS, exactToken: 0.7, symbolMatch: 0.4 },
    integrate:  { ...DEFAULT_WEIGHTS, graphBoost: 0.7, pathMatch: 1.1 },
    navigate:   { ...DEFAULT_WEIGHTS, pathMatch: 1.2, exactToken: 0.9 },
  };

  // Penalty multipliers for negative signals
  const PENALTY_SIGNALS = {
    testFile:      0.4,    // test/spec/__tests__ in path
    generatedCode: 0.3,    // dist/build/.next in path
    docsFile:      0.2,    // docs/doc/README in path
    nodeModules:   0.0,    // node_modules (zero score)
  };

  function _computePenalty(filePath) {
    const pathLower = filePath.toLowerCase();
    if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
    if (/(^|\/)(test|tests|spec|__tests__|e2e)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.testFile;
    if (/(^|\/)(dist|build|\.next|\.nuxt|out|\.venv|venv)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.generatedCode;
    if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.docsFile;
    return 1.0;
  }

  // Detect hub files: those with fanout > 20% of all files in the graph
  function _computeHubs(graph) {
    if (!graph || !graph.reverse) return new Set();
    const fileCount = Math.max(1, graph.reverse.size);
    const threshold = Math.ceil(fileCount * 0.2);
    const hubs = new Set();
    for (const [file, deps] of graph.reverse) {
      if ((deps && deps.size >= threshold) || (Array.isArray(deps) && deps.length >= threshold)) {
        hubs.add(file);
      }
    }
    return hubs;
  }

  // Common utility paths that should be treated as hubs regardless of fanout
  function _isHub(filePath) {
    return /\/(utils|helpers|shared|common|constants|types|interfaces|index|zzz|globals)\.(ts|tsx|js|jsx|r|R)$/.test(filePath)
        || filePath.endsWith('/index.ts') || filePath.endsWith('/index.js')
        || filePath.endsWith('/R/utils.R') || filePath.endsWith('/R/zzz.R') || filePath.endsWith('/R/globals.R');
  }

  /**
   * Score a single file against a query, returning detailed signal breakdown.
   *
   * @param {string}   filePath   - relative file path (e.g. 'src/extractors/python.js')
   * @param {string[]} sigs       - signature strings for this file
   * @param {string[]} queryTokens - pre-tokenized query
   * @param {object}   weights
   * @returns {{ score: number, signals: { exactToken: number, symbolMatch: number, prefixMatch: number, pathMatch: number, penalty: number } }}
   */
  function scoreFile(filePath, sigs, queryTokens, weights) {
    if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };

    const w = weights || DEFAULT_WEIGHTS;
    const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath) };

    // Build token set from all signatures
    const sigText = sigs.join(' ');
    const sigTokenSet = new Set(tokenize(sigText));

    // Build token set from the file path
    const pathTokenSet = new Set(tokenize(filePath));

    let score = 0;

    for (const qt of queryTokens) {
      if (STOP_WORDS.has(qt)) continue;

      // Exact token match in sigs
      if (sigTokenSet.has(qt)) {
        const bonus = w.exactToken;
        score += bonus;
        signals.exactToken += bonus;

        // Bonus: appears directly in a function/class/method name line
        const nameLineMatch = sigs.some((sig) => {
          const nt = tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' '));
          return nt.includes(qt);
        });
        if (nameLineMatch) {
          score += w.symbolMatch;
          signals.symbolMatch += w.symbolMatch;
        }
      }

      // Prefix match (e.g. query "python" matches "pythonDeps")
      if (qt.length >= 4) {
        for (const st of sigTokenSet) {
          if (st !== qt && st.startsWith(qt)) {
            score += w.prefixMatch;
            signals.prefixMatch += w.prefixMatch;
            break; // one bonus per query token
          }
        }
      }

      // Path token match
      if (pathTokenSet.has(qt)) {
        score += w.pathMatch;
        signals.pathMatch += w.pathMatch;
      }
    }

    // Apply penalty multiplier
    score *= signals.penalty;

    return { score, signals };
  }

  /**
   * Rank all files in a signature index against a query.
   *
   * @param {string}              query     - natural language query
   * @param {Map<string,string[]>} sigIndex - Map<file, sigs[]>
   * @param {object}  [opts]
   * @param {number}  [opts.topK=10]               - max results to return
   * @param {number}  [opts.recencyBoost=1.5]       - multiplier for recent files
   * @param {Set<string>} [opts.recencySet]         - set of recently-changed file paths
   * @param {object}  [opts.weights]               - override scoring weights
   * @param {string}  [opts.cwd]                   - project root for learned ranking weights
   * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
   * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
   */
  function rank(query, sigIndex, opts) {
    if (!query || typeof query !== 'string') return [];
    if (!sigIndex || !(sigIndex instanceof Map) || sigIndex.size === 0) return [];

    const topK = (opts && opts.topK) || 10;
    const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
    const recencySet = (opts && opts.recencySet) || null;
    const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
    const cwd = (opts && opts.cwd) || null;

    // Detect query intent and get appropriate weights
    const intent = detectIntent(query);
    const intentWeights = INTENT_WEIGHTS[intent] || DEFAULT_WEIGHTS;
    const weights = (opts && opts.weights) ? Object.assign({}, intentWeights, opts.weights) : intentWeights;
    const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;

    const queryTokens = tokenize(query);
    if (queryTokens.length === 0) {
      // Empty query: return top-K by file count (most signatures = most useful)
      const all = [];
      for (const [file, sigs] of sigIndex.entries()) {
        all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4), intent, signals: {} });
      }
      all.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
      return all.slice(0, topK);
    }

    // Identifier-aware BM25 base relevance over the whole index (#395). BM25
    // splits camelCase/snake_case, stems, and boosts path tokens, so queries
    // whose terms live inside identifiers (e.g. "component emit" → componentEmits)
    // are matched. The existing negative-signal penalty and recency/graph/learned
    // boosts are layered on top; the per-token signals stay for the explain table.
    const bm25Scores = new Map();
    for (const c of bm25rank(query, [...sigIndex.entries()].map(([file, sigs]) => ({ file, sigs })))) {
      bm25Scores.set(c.file, c.score);
    }

    const scored = [];
    for (const [file, sigs] of sigIndex.entries()) {
      const result = scoreFile(file, sigs, queryTokens, weights);
      const penalty = result.signals.penalty;
      const base = bm25Scores.get(file) || 0;
      let score = base * penalty;
      const signals = result.signals;
      signals.bm25 = base;

      // Recency boost
      if (recencySet && recencySet.has(file) && score > 0) {
        score *= recencyMultiplier;
        signals.recencyBoost = recencyMultiplier;
      }

      if (learnedWeights && score > 0) {
        const multiplier = learnedWeights[file] || 1.0;
        score *= multiplier;
        signals.learnedWeights = multiplier;
      }

      scored.push({
        file,
        score,
        sigs,
        tokens: Math.ceil(sigs.join('\n').length / 4),
        intent,
        signals,
      });
    }

    // Graph neighbor boost: 2-hop traversal with decay (v6.7)
    // Hop 1: add hop1 amount to direct import neighbors (score > 0)
    // Hop 2: add hop2 amount to neighbors of hop1 files (with decay)
    // Hub suppression: files with high fanout (>20%) are not boosted
    if (graph && cwd) {
      const path = require('path');
      // Build maps for relative ↔ absolute path conversion and index lookup
      const relToIdx = new Map();
      const absToRel = new Map();
      for (let i = 0; i < scored.length; i++) {
        relToIdx.set(scored[i].file, i);
        const abs = path.resolve(cwd, scored[i].file);
        absToRel.set(abs, scored[i].file);
      }

      const hubs = _computeHubs(graph);
      const hop1Files = new Set(); // track which files received hop1 boost

      // Hop 1: direct neighbors of scored files
      for (const entry of scored) {
        if (entry.score <= 0) continue;
        const abs = path.resolve(cwd, entry.file);
        const neighbors = graph.forward.get(abs) || [];
        for (const neighborAbs of neighbors) {
          if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
          const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
          const idx = relToIdx.get(neighborRel);
          if (idx !== undefined) {
            scored[idx].score += GRAPH_BOOST_AMOUNTS.hop1;
            scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop1;
            hop1Files.add(neighborAbs);
          }
        }
      }

      // Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
      for (const hop1File of hop1Files) {
        if (!absToRel.has(hop1File)) continue; // skip files not in index
        const neighbors = graph.forward.get(hop1File) || [];
        for (const neighborAbs of neighbors) {
          if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
          if (hop1Files.has(neighborAbs)) continue; // skip already hop1-boosted
          const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
          const idx = relToIdx.get(neighborRel);
          if (idx !== undefined && scored[idx].score > 0) {
            // Only boost files that have some baseline score (not noise)
            scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
            scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
          }
        }
      }
    }

    // Compute confidence levels based on score distribution
    if (scored.length > 0) {
      const scores = scored.map(s => s.score);
      const maxScore = Math.max(...scores);
      const minScore = Math.min(...scores);
      const scoreRange = maxScore - minScore || 1;

      // Confidence tiers: top 33% = high, next 33% = medium, rest = low
      for (const entry of scored) {
        if (entry.score <= 0) {
          entry.confidence = 'low';
        } else {
          const normalized = (entry.score - minScore) / scoreRange;
          entry.confidence = normalized > 0.66 ? 'high' : normalized > 0.33 ? 'medium' : 'low';
        }
      }
    }

    scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
    return scored.slice(0, topK);
  }

  /**
   * All paths where sigmap adapters write their context files, in probe order.
   * The first existing file with a non-empty index wins when no explicit path
   * is supplied.
   */
  const ADAPTER_OUTPUT_PATHS = [
    ['.github', 'copilot-instructions.md'], // copilot (default)
    ['CLAUDE.md'],                           // claude
    ['AGENTS.md'],                           // codex
    ['.cursorrules'],                        // cursor
    ['.windsurfrules'],                      // windsurf
    ['.github', 'openai-context.md'],        // openai
    ['.github', 'gemini-context.md'],        // gemini
    ['llm-full.txt'],                        // llm-full
    ['llm.txt'],                             // llm
  ];

  /**
   * Parse a single context file into a Map<filePath, string[]>.
   *
   * Files that contain human-written content before an
   * "## Auto-generated signatures" marker (e.g. CLAUDE.md) are handled
   * by skipping everything above the marker before scanning for ### headers.
   *
   * @param {string} contextPath  - absolute path to the context file
   * @returns {Map<string, string[]>}
   */
  function _parseContextFile(contextPath) {
    const fs = require('fs');
    const index = new Map();

    if (!fs.existsSync(contextPath)) return index;

    let content = fs.readFileSync(contextPath, 'utf8');

    // Skip any human-written preamble that sits above the auto-generated block.
    const markerIdx = content.indexOf('## Auto-generated signatures');
    if (markerIdx !== -1) content = content.slice(markerIdx);

    const lines = content.split('\n');
    let currentFile = null;
    let inBlock = false;
    let sigs = [];

    for (const line of lines) {
      const headerMatch = line.match(/^###\s+(\S+)\s*$/);
      if (headerMatch) {
        if (currentFile !== null) index.set(currentFile, sigs);
        currentFile = headerMatch[1];
        sigs = [];
        inBlock = false;
        continue;
      }
      if (line.startsWith('```')) { inBlock = !inBlock; continue; }
      if (inBlock && currentFile && line.trim()) sigs.push(line.trim());
    }
    if (currentFile !== null) index.set(currentFile, sigs);

    return index;
  }

  /** Merge source index into target; prefer non-empty sig lists. */
  function _mergeSigIndex(target, source) {
    for (const [file, sigs] of source.entries()) {
      if (!sigs || sigs.length === 0) continue;
      if (!target.has(file) || target.get(file).length < sigs.length) {
        target.set(file, sigs);
      }
    }
    return target;
  }

  /**
   * Load signatures from .sigmap-cache.json (absolute paths → repo-relative keys).
   * @param {string} cwd
   * @returns {Map<string, string[]>}
   */
  function _buildSigIndexFromCache(cwd) {
    const fs = require('fs');
    const path = require('path');
    const index = new Map();
    try {
      const { loadCache } = __require('./src/cache/sig-cache');
      const pkgPath = path.join(cwd, 'package.json');
      let version = '0.0.0';
      if (fs.existsSync(pkgPath)) {
        version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || version;
      }
      const cache = loadCache(cwd, version);
      for (const [absPath, entry] of cache.entries()) {
        if (!entry || !entry.sigs || entry.sigs.length === 0) continue;
        const rel = path.relative(cwd, absPath).replace(/\\/g, '/');
        if (!rel || rel.startsWith('..')) continue;
        index.set(rel, entry.sigs);
      }
    } catch (_) {}
    return index;
  }

  /**
   * Hot-cold and per-module strategies store most signatures outside the primary
   * copilot-instructions.md file. MCP tools must merge all sources.
   * @param {string} cwd
   * @returns {Map<string, string[]>}
   */
  function _enrichSigIndexFromStrategy(cwd, index) {
    const path = require('path');
    const coldPath = path.join(cwd, '.github', 'context-cold.md');
    _mergeSigIndex(index, _parseContextFile(coldPath));
    _mergeSigIndex(index, _buildSigIndexFromCache(cwd));
    return index;
  }

  /**
   * Build a signature index from the generated context file.
   * Returns Map<filePath, string[]> where filePath is the relative path
   * as it appears in the ### headers of the context file.
   *
   * Resolution priority:
   *  1. `opts.contextPath` — explicit path from --output or --adapter flag
   *  2. `customOutput` key in gen-context.config.json — persisted from a
   *     previous `--output <file>` generation run
   *  3. All known adapter output paths probed in order (first non-empty wins)
   *
   * @param {string} cwd
   * @param {{ contextPath?: string }} [opts]
   * @returns {Map<string, string[]>}
   */
  function buildSigIndex(cwd, opts) {
    const fs   = require('fs');
    const path = require('path');

    // 1. Caller supplied an explicit path — use it directly.
    if (opts && opts.contextPath) {
      const index = _parseContextFile(opts.contextPath);
      return _enrichSigIndexFromStrategy(cwd, index);
    }

    // 2. Check gen-context.config.json for a persisted customOutput path.
    try {
      const cfgPath = path.join(cwd, 'gen-context.config.json');
      if (fs.existsSync(cfgPath)) {
        const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
        if (cfg.customOutput) {
          const customPath = path.resolve(cwd, cfg.customOutput);
          const index = _parseContextFile(customPath);
          if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
        }
      }
    } catch (_) {}

    // 3. Probe all known adapter output paths; return first non-empty index.
    for (const parts of ADAPTER_OUTPUT_PATHS) {
      const contextPath = path.join(cwd, ...parts);
      const index = _parseContextFile(contextPath);
      if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
    }

    // 4. Primary file empty/missing (hot-cold) — still serve cold + cache.
    const fallback = new Map();
    return _enrichSigIndexFromStrategy(cwd, fallback);
  }

  /**
   * Format ranked results as a markdown table string.
   *
   * @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
   * @param {string} query
   * @returns {string}
   */
  function formatRankTable(results, query) {
    if (!results || results.length === 0) {
      return `No matching files found for query: "${query}"\n`;
    }

    const intent = (results[0] && results[0].intent) || 'search';
    const lines = [
      `## Query: ${query}`,
      `Intent: ${intent}`,
      '',
      '| Rank | File | Score | Sigs | Penalty |',
      '|------|------|-------|------|---------|',
      ...results.map((r, i) => {
        const penalty = r.signals && r.signals.penalty ? r.signals.penalty.toFixed(2) : '1.00';
        return `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${penalty} |`;
      }),
      '',
    ];

    // Add signature details for top results
    for (const r of results.slice(0, 3)) {
      if (r.sigs.length > 0) {
        lines.push(`### ${r.file}`);
        if (r.signals) {
          const sig = r.signals;
          lines.push(`Signals: exactToken=${(sig.exactToken || 0).toFixed(2)} symbolMatch=${(sig.symbolMatch || 0).toFixed(2)} prefixMatch=${(sig.prefixMatch || 0).toFixed(2)} pathMatch=${(sig.pathMatch || 0).toFixed(2)} penalty=${(sig.penalty || 1).toFixed(2)}`);
        }
        lines.push('```');
        lines.push(...r.sigs.slice(0, 10));
        if (r.sigs.length > 10) lines.push(`... (${r.sigs.length - 10} more)`);
        lines.push('```');
        lines.push('');
      }
    }

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

  /**
   * Format ranked results as a structured JSON-serialisable object.
   *
   * @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
   * @param {string} query
   * @returns {object}
   */
  function formatRankJSON(results, query) {
    const intent = (results && results[0] && results[0].intent) || 'search';
    return {
      query,
      intent,
      results: (results || []).map((r, i) => ({
        rank: i + 1,
        file: r.file,
        score: r.score,
        sigs: r.sigs,
        tokens: r.tokens,
        signals: r.signals || {},
      })),
      totalResults: (results || []).length,
    };
  }

  // ---------------------------------------------------------------------------
  // Intent detection — 7 intents
  // ---------------------------------------------------------------------------
  const INTENT_PATTERNS = {
    debug:    /\b(bug|fix|error|crash|exception|broken|failing|issue|problem|regression)\b/i,
    explain:  /\b(explain|how does|what is|understand|overview|architecture|describe|walk me|teach)\b/i,
    refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|optimize)\b/i,
    review:   /\b(review|check|audit|security|pr|pull request|assess|validate)\b/i,
    test:     /\b(test|unit test|integration test|testing|spec|assert|mock)\b/i,
    integrate:/\b(import|integrate|connect|wire|bind|require|export|depend|graph)\b|require[ds]\b/i,
    navigate: /\b(find|locate|where|search|look for|show me|navigate|browse|list)\b/i,
  };

  function detectIntent(query) {
    if (!query || typeof query !== 'string') return 'search';
    for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
      if (re.test(query)) return intent;
    }
    return 'search';
  }

  module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
  
};

// ── ./src/retrieval/tokenizer ──
__factories["./src/retrieval/tokenizer"] = function(module, exports) {
  
  /**
   * SigMap zero-dependency tokenizer.
   * Splits code identifiers: camelCase, snake_case, kebab-case, PascalCase,
   * removes stop words, and returns lower-case tokens.
   */

  const STOP_WORDS = new Set([
    'the', 'a', 'an', 'in', 'of', 'to', 'for', 'and', 'or', 'is', 'are',
    'that', 'this', 'it', 'with', 'from', 'by', 'be', 'as', 'on', 'at',
    'do', 'not', 'use', 'get', 'set', 'up', 'if', 'no', 'so', 'we',
  ]);

  /**
   * Tokenize any text (query or code signature) into unique lower-case tokens.
   * Handles:
   *   - camelCase  → ['camel', 'case']
   *   - PascalCase → ['pascal', 'case']
   *   - snake_case → ['snake', 'case']
   *   - kebab-case → ['kebab', 'case']
   *   - dot.notation → ['dot', 'notation']
   *   - File paths  → individual path components (no extension)
   *
   * @param {string} text
   * @param {object} [opts]
   * @param {boolean} [opts.removeStopWords=true]
   * @param {number}  [opts.minLength=2]
   * @returns {string[]}
   */
  function tokenize(text, opts) {
    if (!text || typeof text !== 'string') return [];
    const removeStop = opts && opts.removeStopWords === false ? false : true;
    const minLen = (opts && opts.minLength) || 2;

    const tokens = text
      // strip file extension (e.g. .js, .ts, .py)
      .replace(/\.\w{1,6}(?=\s|\/|$)/g, ' ')
      // camelCase / PascalCase split
      .replace(/([a-z])([A-Z])/g, '$1 $2')
      .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
      // snake_case / kebab-case / dot.notation
      .replace(/[_\-\.\/]/g, ' ')
      // drop remaining non-word characters
      .replace(/[^\w\s]/g, ' ')
      .toLowerCase()
      .split(/\s+/)
      .filter((t) => t.length >= minLen);

    if (!removeStop) return [...new Set(tokens)];
    return [...new Set(tokens.filter((t) => !STOP_WORDS.has(t)))];
  }

  module.exports = { tokenize, STOP_WORDS };
  
};

// ── ./src/review/pr-evidence ──
__factories["./src/review/pr-evidence"] = function(module, exports) {
  
  /**
   * PR Evidence Report (v9.0 G3).
   *
   * A single, branded, deterministic Markdown artifact for code review: for each
   * changed file it folds together the signature context, blast radius (direct /
   * transitive importers, impacted tests + routes), cross-language related tests,
   * a risk label, and the `review-pr` findings (scope drift, god-node edits,
   * missing tests, security-sensitive files). Posted as a PR comment, it answers
   * "what changed, what it touches, and what to test" — without an LLM.
   *
   * Built entirely from shipped zero-dep modules (reviewPr, graph/impact,
   * evidence/pack, extractors/dispatch). Carries NO wall-clock timestamp, so the
   * report is byte-stable given a fixed tree — diff-friendly as a comment.
   */

  const fs = require('fs');
  const path = require('path');
  const { reviewPr } = __require('./src/review/review-pr');

  /**
   * Build the structured PR evidence for a changed-file list.
   * @param {Array<{path:string,status?:string}>|string[]} changedFiles
   * @param {string} cwd
   * @param {object} [opts]
   * @param {number} [opts.depth=2]   blast-radius BFS depth
   * @param {string} [opts.scope]     label for the diff scope (e.g. "vs main")
   * @returns {{ scope:string, files:object[], review:object }}
   */
  function buildPrEvidence(changedFiles, cwd, opts = {}) {
    const files = (changedFiles || []).map((f) =>
      typeof f === 'string' ? { path: f, status: 'M' } : { path: f.path, status: f.status || 'M' });

    const review = reviewPr(files, cwd, opts);

    let riskLabelFor = () => 'source';
    let findRelatedTests = () => [];
    try { ({ riskLabelFor, findRelatedTests } = __require('./src/evidence/pack')); } catch (_) { /* defaults */ }
    const { extractFile, langFor } = __require('./src/extractors/dispatch');

    let allFiles = [];
    try { const { buildSigIndex } = __require('./src/retrieval/ranker'); allFiles = [...buildSigIndex(cwd).keys()]; } catch (_) { /* no index */ }

    const depth = Number.isFinite(opts.depth) ? opts.depth : 2;
    const srcPaths = files.filter((f) => f.status !== 'D' && langFor(f.path)).map((f) => f.path);
    let impactByFile = new Map();
    try {
      const { analyzeImpact } = __require('./src/graph/impact');
      impactByFile = new Map(analyzeImpact(srcPaths, cwd, { depth }).map((r) => [r.file, r.impact]));
    } catch (_) { /* graph optional */ }

    const fileReports = files.map((f) => {
      const deleted = f.status === 'D';
      let signatures = [];
      if (!deleted && langFor(f.path)) {
        try { signatures = extractFile(f.path, fs.readFileSync(path.resolve(cwd, f.path), 'utf8')); } catch (_) { /* unreadable */ }
      }
      const impact = impactByFile.get(f.path) || null;
      return {
        path: f.path,
        status: f.status,
        riskLabel: riskLabelFor(f.path),
        signatures,
        blast: impact ? {
          total: impact.totalImpact,
          direct: impact.direct || [],
          transitive: (impact.transitive || []).length,
          tests: impact.tests || [],
          routes: impact.routes || [],
        } : null,
        relatedTests: deleted ? [] : findRelatedTests(f.path, allFiles),
      };
    });

    return { scope: opts.scope || 'diff', files: fileReports, review };
  }

  const STATUS_LABEL = { M: 'modified', A: 'added', D: 'deleted', R: 'renamed', C: 'copied' };

  /** Render the branded, deterministic "PR Evidence Report" Markdown. */
  function formatPrEvidenceMarkdown(evidence, opts = {}) {
    const L = [];
    const s = evidence.review.summary;
    const maxSigs = Number.isFinite(opts.maxSignatures) ? opts.maxSignatures : 30;

    L.push('## 🔍 PR Evidence Report');
    L.push('');
    L.push(
      `**${s.filesChanged} file(s) changed** — ${s.sourceChanged} source, ${s.testsChanged} test · ` +
      (s.ok ? '✅ no review findings' : `⚠️ ${s.findings} finding(s)`) +
      ` · scope: ${evidence.scope}`
    );
    L.push('');

    if (!s.ok) {
      L.push('### Review findings');
      for (const f of evidence.review.findings) {
        if (f.type === 'missing-tests') L.push(`- ⚠️ **missing tests** — \`${f.file}\` changed with no matching test`);
        else if (f.type === 'security-file') L.push(`- ⚠️ **security-sensitive file** — \`${f.file}\``);
        else if (f.type === 'god-node') L.push(`- ⚠️ **god node** — \`${f.file}\` → ${f.count} dependents (high blast radius)`);
        else if (f.type === 'scope-drift') L.push(`- ⚠️ **scope drift** — ${f.count} top-level dirs touched (${f.dirs.join(', ')})`);
      }
      L.push('');
    }

    L.push('### Changed files');
    for (const f of evidence.files) {
      const st = STATUS_LABEL[f.status] || f.status;
      L.push(`#### \`${f.path}\`  _(${st} · risk: ${f.riskLabel})_`);
      if (f.status === 'D') { L.push('_deleted_', ''); continue; }

      if (f.blast) {
        L.push(
          `**Blast radius:** ${f.blast.total} file(s) impacted — ${f.blast.direct.length} direct, ${f.blast.transitive} transitive` +
          (f.blast.tests.length ? `, ${f.blast.tests.length} test(s)` : '') +
          (f.blast.routes.length ? `, ${f.blast.routes.length} route(s)` : '')
        );
        if (f.blast.tests.length) L.push(`Tests to run: ${f.blast.tests.slice(0, 8).map((t) => '`' + t + '`').join(', ')}`);
      } else {
        L.push('**Blast radius:** _(not in dependency graph — new or leaf file)_');
      }
      if (f.relatedTests.length) L.push(`Related tests: ${f.relatedTests.slice(0, 8).map((t) => '`' + t + '`').join(', ')}`);

      if (f.signatures.length) {
        L.push('```');
        for (const sig of f.signatures.slice(0, maxSigs)) L.push(sig);
        if (f.signatures.length > maxSigs) L.push(`… +${f.signatures.length - maxSigs} more`);
        L.push('```');
      }
      L.push('');
    }

    L.push('---');
    L.push('_Deterministic PR Evidence Report — generated by [SigMap](https://sigmap.io). No LLM; byte-stable given a fixed tree._');
    return L.join('\n');
  }

  module.exports = { buildPrEvidence, formatPrEvidenceMarkdown };
  
};

// ── ./src/review/review-pr ──
__factories["./src/review/review-pr"] = function(module, exports) {
  
  /**
   * review-pr (IMPL.md §6 step 4 — last guard stage of the `create` pipeline).
   *
   * Audits a diff for drift and side effects after a PR is opened: scope drift,
   * edits to high-fan-in "god-node" files, source changes without matching tests,
   * and changes to security-sensitive files. Pure (takes a changed-file list),
   * zero-dependency, bundle-safe; reuses the impact graph for blast radius.
   */

  const path = require('path');
  const { analyzeImpact } = __require('./src/graph/impact');

  const SECURITY_PATTERNS = [
    /(^|\/)\.env(\.|$)/i,
    /(^|\/)(secrets?|credentials?)(\/|\.|$)/i,
    /(^|\/)auth(\/|\.|-)/i,
    /(^|\/)package(-lock)?\.json$/,
    /(^|\/)(yarn\.lock|pnpm-lock\.yaml)$/,
    /(^|\/)\.github\/workflows\//,
    /(^|\/)Dockerfile/i,
    /\.(pem|key|crt|p12)$/i,
  ];

  const SRC_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java', '.rb', '.php']);
  const GOD_NODE_THRESHOLD = 15; // transitive dependents → high-fan-in "god node"
  const SCOPE_DIR_THRESHOLD = 5; // distinct top-level dirs → scope drift

  function isTestFile(p) {
    return /\.(test|spec)\.[jt]sx?$|(^|\/)test_|_test\.(py|go)$|(^|\/)(tests?|__tests__|spec)\//.test(p);
  }
  function isSource(p) {
    return SRC_EXTS.has(path.extname(p).toLowerCase()) && !isTestFile(p);
  }

  /**
   * Audit a changed-file list.
   * @param {Array<{path:string,status:string}>|string[]} changedFiles
   * @param {string} cwd
   * @param {object} [opts]
   * @param {number} [opts.godNodeThreshold=15]
   * @param {number} [opts.scopeThreshold=5]
   * @returns {{ findings: object[], blast: object[], summary: object }}
   */
  function reviewPr(changedFiles, cwd, opts = {}) {
    const godThreshold = opts.godNodeThreshold != null ? opts.godNodeThreshold : GOD_NODE_THRESHOLD;
    const scopeThreshold = opts.scopeThreshold != null ? opts.scopeThreshold : SCOPE_DIR_THRESHOLD;
    const files = (changedFiles || []).map((f) => (typeof f === 'string' ? { path: f, status: 'M' } : f));

    const findings = [];
    const paths = files.map((f) => f.path);
    const live = files.filter((f) => f.status !== 'D');
    const srcChanged = live.filter((f) => isSource(f.path)).map((f) => f.path);
    const testChanged = paths.filter(isTestFile);

    // 1. Missing tests: a changed source file with no matching changed test file.
    for (const s of srcChanged) {
      const stem = path.basename(s).replace(/\.[^.]+$/, '');
      const covered = testChanged.some((t) => path.basename(t).includes(stem));
      if (!covered) findings.push({ type: 'missing-tests', file: s, severity: 'warn' });
    }

    // 2. Security-sensitive files.
    for (const f of live) {
      if (SECURITY_PATTERNS.some((re) => re.test(f.path))) {
        findings.push({ type: 'security-file', file: f.path, severity: 'warn' });
      }
    }

    // 3. God-node edits (high blast radius).
    const blast = [];
    if (srcChanged.length) {
      let impacts = [];
      try { impacts = analyzeImpact(srcChanged, cwd, {}); } catch (_) { impacts = []; }
      for (const { file, impact } of impacts) {
        blast.push({ file, totalImpact: impact.totalImpact });
        if (impact.totalImpact > godThreshold) {
          findings.push({ type: 'god-node', file, count: impact.totalImpact, severity: 'warn' });
        }
      }
      blast.sort((a, b) => b.totalImpact - a.totalImpact);
    }

    // 4. Scope drift: distinct top-level directories touched.
    const dirs = [...new Set(paths.map((p) => (p.includes('/') ? p.split('/')[0] : '.')))];
    if (dirs.length > scopeThreshold) {
      findings.push({ type: 'scope-drift', dirs, count: dirs.length, threshold: scopeThreshold, severity: 'warn' });
    }

    const byType = findings.reduce((a, f) => { a[f.type] = (a[f.type] || 0) + 1; return a; }, {});
    return {
      findings,
      blast,
      summary: {
        filesChanged: files.length,
        sourceChanged: srcChanged.length,
        testsChanged: testChanged.length,
        findings: findings.length,
        byType,
        ok: findings.length === 0,
      },
    };
  }

  module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD };
  
};

// ── ./src/routing/classifier ──
__factories["./src/routing/classifier"] = function(module, exports) {
  
  /**
   * Classify files by complexity tier for model routing hints.
   *
   * Tiers:
   *   'fast'     — simple files: config, markup, templates, trivial utilities
   *   'balanced' — standard application code: moderate functions and classes
   *   'powerful' — complex files: large classes, many exports, security-critical paths
   *
   * @param {string} filePath - absolute path to the file
   * @param {string[]} sigs   - extracted signatures for the file
   * @returns {'fast'|'balanced'|'powerful'}
   */
  function classify(filePath, sigs) {
    const lower = filePath.toLowerCase();
    const sigCount = sigs.length;

    // ── Fast tier heuristics ────────────────────────────────────────────────
    // Configuration, markup, templates, and trivial utilities are small tasks
    if (
      lower.endsWith('.json') ||
      lower.endsWith('.yml') ||
      lower.endsWith('.yaml') ||
      lower.endsWith('.toml') ||
      lower.endsWith('.env') ||
      lower.endsWith('.html') ||
      lower.endsWith('.htm') ||
      lower.endsWith('.css') ||
      lower.endsWith('.scss') ||
      lower.endsWith('.sass') ||
      lower.endsWith('.less') ||
      /dockerfile/i.test(lower) ||
      lower.endsWith('.sh') ||
      lower.endsWith('.bash') ||
      lower.endsWith('.zsh') ||
      lower.endsWith('.fish')
    ) {
      return 'fast';
    }

    // Config-like directories
    if (
      lower.includes('/config/') ||
      lower.includes('/configs/') ||
      lower.includes('/fixtures/') ||
      lower.includes('/migrations/') ||
      lower.includes('/seeds/')
    ) {
      return sigCount > 4 ? 'balanced' : 'fast';
    }

    // Test files — usually standard complexity
    if (/\.(test|spec)\.[a-z]+$/.test(lower) || /_test\.[a-z]+$/.test(lower)) {
      return 'balanced';
    }

    // ── Powerful tier heuristics — high-signal keywords in the path ─────────
    if (
      lower.includes('/security/') ||
      lower.includes('/auth/') ||
      lower.includes('/crypto/') ||
      lower.includes('/core/') ||
      lower.includes('/engine/') ||
      lower.includes('/compiler/') ||
      lower.includes('/parser/') ||
      lower.includes('/scheduler/') ||
      lower.includes('/orchestrat')
    ) {
      return 'powerful';
    }

    // Many exports → complex file
    if (sigCount >= 12) return 'powerful';

    // Large number of class-level methods (indented 2-space sigs)
    const methodCount = sigs.filter((s) => s.startsWith('  ')).length;
    if (methodCount >= 8) return 'powerful';

    // ── Balanced covers everything else ────────────────────────────────────
    if (sigCount <= 2) return 'fast';
    return 'balanced';
  }

  /**
   * Classify all file entries and group them by tier.
   *
   * @param {Array<{filePath: string, sigs: string[]}>} fileEntries
   * @returns {{ fast: string[], balanced: string[], powerful: string[] }}
   *   Each array contains relative paths (relative to cwd).
   */
  function classifyAll(fileEntries, cwd) {
    const path = require('path');
    const result = { fast: [], balanced: [], powerful: [] };
    for (const { filePath, sigs } of fileEntries) {
      const tier = classify(filePath, sigs);
      result[tier].push(path.relative(cwd, filePath));
    }
    return result;
  }

  module.exports = { classify, classifyAll };
  
};

// ── ./src/routing/hints ──
__factories["./src/routing/hints"] = function(module, exports) {
  
  /**
   * Model routing hint definitions for SigMap.
   *
   * Maps complexity tiers to:
   *   - example model names (vendor-agnostic labels used as hints)
   *   - task types that belong to each tier
   *   - estimated per-1K-token API cost (USD, illustrative — update as needed)
   *
   * These are embedded in the generated context file when `routing: true`
   * so that AI agents and developers know which model tier to invoke.
   */

  const TIERS = {
    fast: {
      label: 'Fast (low-cost)',
      examples: 'claude-haiku-4-5, gpt-5-1-codex-mini, gemini-3-flash',
      tasks: [
        'Autocomplete and inline suggestions',
        'Edit config or markup files',
        'Fix typos and rename symbols',
        'Format, lint, and trivial style changes',
        'Explain a short utility function',
        'Generate simple shell scripts or Dockerfiles',
      ],
      costHint: '~$0.0008 / 1K tokens',
    },

    balanced: {
      label: 'Balanced (mid-tier)',
      examples: 'claude-sonnet-4-6, gpt-5-2, gemini-3-1-pro',
      tasks: [
        'Write unit or integration tests',
        'Implement a well-scoped feature function',
        'Debug a runtime error with stack trace',
        'Refactor a module (< 200 lines)',
        'Generate a PR description',
        'Explain a multi-function module',
      ],
      costHint: '~$0.003 / 1K tokens',
    },

    powerful: {
      label: 'Powerful (high-cost)',
      examples: 'claude-opus-4-6, gpt-5-4, gemini-2-5-pro',
      tasks: [
        'Cross-cutting architecture decisions',
        'Multi-file refactor spanning 5+ files',
        'Security audit (OWASP Top 10)',
        'Complex debugging across async boundaries',
        'Migration plan for a library/framework upgrade',
        'Designing a new module from requirements',
      ],
      costHint: '~$0.015 / 1K tokens',
    },
  };

  /**
   * Format the routing section as markdown to append to the context file.
   *
   * @param {{ fast: string[], balanced: string[], powerful: string[] }} groups
   *   Relative file paths grouped by tier (from classifier.classifyAll).
   * @returns {string} Markdown block to embed in the context output.
   */
  function formatRoutingSection(groups) {
    const lines = [
      '',
      '---',
      '',
      '## Model routing hints',
      '<!-- Generated by SigMap routing module — update gen-context.config.json to disable -->',
      '',
      'Select the model tier based on the task complexity and the files involved.',
      '',
    ];

    for (const [tier, info] of Object.entries(TIERS)) {
      const files = groups[tier] || [];
      lines.push(`### ${info.label}`);
      lines.push(`**Examples:** ${info.examples}  `);
      lines.push(`**Cost:** ${info.costHint}`);
      lines.push('');
      lines.push('**Use for tasks like:**');
      for (const task of info.tasks) lines.push(`- ${task}`);
      lines.push('');
      if (files.length > 0) {
        lines.push('**Files in this tier:**');
        for (const f of files.slice(0, 15)) lines.push(`- \`${f}\``);
        if (files.length > 15) lines.push(`- … and ${files.length - 15} more`);
      } else {
        lines.push('**Files in this tier:** _(none detected)_');
      }
      lines.push('');
    }

    lines.push('> **Tip:** Run `node gen-context.js --routing` to regenerate routing hints.');
    lines.push('> See `docs/MODEL_ROUTING.md` for full routing guide and cost optimisation tips.');

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

  module.exports = { TIERS, formatRoutingSection };
  
};

// ── ./src/scaffold/persist ──
__factories["./src/scaffold/persist"] = function(module, exports) {
  
  /**
   * Scaffold persistence (IMPL.md §6.2 — 2d).
   *
   * Renders an accepted scaffold proposal to markdown and locates the on-disk
   * record (`.context/scaffold/latest.md`) so the `create` pipeline and agents can
   * read back the convention-matched proposal. Pure render; zero-dependency,
   * bundle-safe.
   */

  const path = require('path');

  /** Path to the persisted scaffold record. */
  function scaffoldPath(cwd) {
    return path.join(cwd, '.context', 'scaffold', 'latest.md');
  }

  /**
   * Render an accepted scaffold decision to a markdown record.
   * @param {object} decision a `proposeScaffold` result with `ok: true`
   * @param {object} [opts]
   * @param {string} [opts.timestamp] ISO timestamp (caller supplies — keeps this pure)
   * @returns {string}
   */
  function renderScaffoldMarkdown(decision, opts = {}) {
    const d = decision || {};
    const p = d.proposal || {};
    const pct = (n) => `${Math.round((n || 0) * 100)}%`;
    const lines = [
      `# Scaffold — ${d.name || '(unnamed)'}`,
      '',
      `- **Conventions:** ${d.tier || 'unknown'} (${pct(d.confidence)})`,
      `- **File:** \`${p.filename || ''}\` (${p.namingStyle || ''})`,
      `- **Export style:** ${p.exportStyle || ''}`,
      `- **Test file:** \`${p.testFile || ''}\`${p.testFramework ? ` (${p.testFramework})` : ''}`,
    ];
    if (d.warning) lines.push(`- **Warning:** ${d.warning}`);
    lines.push('');
    lines.push(`<sub>Generated by \`sigmap scaffold\`${opts.timestamp ? ` · ${opts.timestamp}` : ''}.</sub>`);
    lines.push('');
    return lines.join('\n');
  }

  module.exports = { scaffoldPath, renderScaffoldMarkdown };
  
};

// ── ./src/scaffold/propose ──
__factories["./src/scaffold/propose"] = function(module, exports) {
  
  /**
   * Scaffold proposal with a confidence floor (IMPL.md §5 — Cause 3).
   *
   * Proposes a convention-matched structure for a new module — filename in the
   * repo's dominant naming style, the export style to use, and a matching test
   * file — but only when the conventions are consistent enough. Below a hard
   * floor it refuses and surfaces the conflict, because a wrong proposal
   * systematizes bad code. Pure, zero-dependency, bundle-safe; reuses the
   * conventions primitives.
   */

  const { toNamingStyle, analyzeConflicts } = __require('./src/conventions/conflicts');

  // Soft threshold is configurable; the hard floor is not (IMPL.md §5.1).
  const DEFAULT_THRESHOLD = 0.7;
  const HARD_FLOOR = 0.5;

  /** Tier for a consistency score (matches the conventions tiers). */
  function _tier(pct) {
    if (pct >= 0.9) return 'consistent';
    if (pct >= 0.7) return 'mostly';
    return 'inconsistent';
  }

  /** Strip any extension/compound suffix from a requested name → bare stem. */
  function _stem(name) {
    const s = String(name || '').trim();
    const slash = s.lastIndexOf('/');
    const base = slash >= 0 ? s.slice(slash + 1) : s;
    const dot = base.indexOf('.');
    return dot > 0 ? base.slice(0, dot) : base;
  }

  /** Test file path for a styled stem given the detected framework + ext. */
  function _testFile(styledStem, framework, ext) {
    if (framework === 'pytest' || framework === 'unittest') {
      return `test_${toNamingStyle(styledStem, 'snake_case')}.py`;
    }
    return `${styledStem}.test.${ext}`;
  }

  /**
   * Propose a convention-matched scaffold, gated by a confidence floor.
   * @param {string} name desired module name (any casing; extension ignored)
   * @param {object} conventions an `extractConventions` result
   * @param {object} [opts]
   * @param {number} [opts.threshold=0.7] soft threshold (clamped to ≥ hard floor)
   * @param {boolean} [opts.force=false] allow proposing below the soft threshold
   *   (never below the hard floor)
   * @param {string} [opts.ext='js'] file extension for the proposed files
   * @returns {{ ok:boolean, refused:boolean, name:string, tier:string,
   *   confidence:number, threshold:number, hardFloor:number, forced:boolean,
   *   warning:string|null, reason:string, proposal:object|null, conflicts:object }}
   */
  function proposeScaffold(name, conventions, opts = {}) {
    const threshold = Math.max(HARD_FLOOR, opts.threshold != null ? opts.threshold : DEFAULT_THRESHOLD);
    const force = !!opts.force;
    const ext = opts.ext || 'js';
    const fileNaming = (conventions && conventions.fileNaming) || { dominant: null, dominantPct: 0, total: 0 };
    const exportStyle = (conventions && conventions.exportStyle) || { dominant: null };
    const confidence = fileNaming.dominantPct || 0;
    const tier = fileNaming.total > 0 ? _tier(confidence) : 'unknown';
    const conflicts = analyzeConflicts(conventions || {});

    const base = {
      ok: false, refused: true, name: String(name || ''), tier, confidence,
      threshold, hardFloor: HARD_FLOOR, forced: false, warning: null,
      reason: '', proposal: null, conflicts,
    };

    if (!fileNaming.dominant || fileNaming.total === 0) {
      return { ...base, reason: 'no file-naming convention detected — cannot propose a name' };
    }
    if (confidence < HARD_FLOOR) {
      return {
        ...base,
        reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the hard floor ${(HARD_FLOOR * 100).toFixed(0)}% — refusing (not overridable)`,
      };
    }
    if (confidence < threshold && !force) {
      return {
        ...base,
        reason: `file-naming consistency ${(confidence * 100).toFixed(0)}% is below the threshold ${(threshold * 100).toFixed(0)}% — refusing (use --force to override above the ${(HARD_FLOOR * 100).toFixed(0)}% floor)`,
      };
    }

    const styledStem = toNamingStyle(_stem(name), fileNaming.dominant);
    const forced = confidence < threshold && force;
    const proposal = {
      filename: `${styledStem}.${ext}`,
      namingStyle: fileNaming.dominant,
      exportStyle: exportStyle.dominant || 'named',
      testFile: _testFile(styledStem, conventions.testFramework, ext),
      testFramework: conventions.testFramework || null,
    };

    return {
      ...base,
      ok: true,
      refused: false,
      forced,
      warning: forced
        ? `proposed below the ${(threshold * 100).toFixed(0)}% threshold (--force); conventions are only ${tier}`
        : null,
      reason: forced ? 'forced proposal above the hard floor' : `conventions are ${tier} — proposing`,
      proposal,
    };
  }

  module.exports = { proposeScaffold, DEFAULT_THRESHOLD, HARD_FLOOR };
  
};

// ── ./src/security/patterns ──
__factories["./src/security/patterns"] = function(module, exports) {
  
  /**
   * Secret detection patterns for SigMap scanner.
   * Each pattern has a name and a regex tested against signature strings.
   */
  const PATTERNS = [
    {
      name: 'AWS Access Key',
      regex: /AKIA[0-9A-Z]{16}/,
    },
    {
      name: 'AWS Secret Key',
      // 40-char base64-like string following common AWS secret key assignment patterns
      regex: /(?:aws_secret|secret_access_key|SecretAccessKey)\s*[:=]\s*['"]?[0-9a-zA-Z/+]{40}/i,
    },
    {
      name: 'GCP API Key',
      regex: /AIza[0-9A-Za-z\-_]{35}/,
    },
    {
      name: 'GitHub Token',
      regex: /gh[pousr]_[A-Za-z0-9_]{36,}/,
    },
    {
      name: 'JWT Token',
      regex: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/,
    },
    {
      name: 'DB Connection String',
      regex: /(mongodb|postgres|postgresql|mysql|redis):\/\/[^:]+:[^@]+@/i,
    },
    {
      name: 'SSH Private Key',
      regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
    },
    {
      name: 'Stripe Key',
      regex: /sk_(live|test)_[0-9a-zA-Z]{24,}/,
    },
    {
      name: 'Twilio Key',
      regex: /SK[0-9a-fA-F]{32}/,
    },
    {
      name: 'Generic Secret',
      regex: /(secret|password|passwd|api_key|apikey|auth_token|access_token)\s*[:=]\s*['"][^'"]{8,}['"]/i,
    },
  ];

  module.exports = { PATTERNS };
  
};

// ── ./src/security/scanner ──
__factories["./src/security/scanner"] = function(module, exports) {
  
  const { PATTERNS } = __require('./src/security/patterns');

  /**
   * Scan an array of signature strings for secrets.
   *
   * @param {string[]} signatures - Array of extracted signature strings
   * @param {string} filePath - Source file path (used in redaction message)
   * @returns {{ safe: string[], redacted: boolean }}
   *   safe      — signatures with any secret-containing entries replaced
   *   redacted  — true if at least one signature was redacted
   */
  function scan(signatures, filePath) {
    if (!Array.isArray(signatures)) return { safe: [], redacted: false };

    try {
      let redacted = false;
      const safe = signatures.map((sig) => {
        if (typeof sig !== 'string') return sig;
        for (const pattern of PATTERNS) {
          if (pattern.regex.test(sig)) {
            redacted = true;
            return `[REDACTED — ${pattern.name} detected in ${filePath}]`;
          }
        }
        return sig;
      });
      return { safe, redacted };
    } catch (_) {
      // Never throw — return original signatures on any error
      return { safe: signatures, redacted: false };
    }
  }

  module.exports = { scan };
  
};

// ── ./src/session/memory ──
__factories["./src/session/memory"] = function(module, exports) {
  
  const fs = require('fs');
  const path = require('path');

  module.exports = { loadSession, saveSession, mergeSessionContext, clearSession };

  const SESSION_TTL_MS = 4 * 60 * 60 * 1000;   // 4 hours — one coding session

  function sessionPath(cwd) {
    return path.join(cwd, '.context', 'session.json');
  }

  function loadSession(cwd) {
    const p = sessionPath(cwd);
    if (!fs.existsSync(p)) return null;
    try {
      const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
      if (Date.now() - raw.ts > SESSION_TTL_MS) return null;  // expired
      return raw;
    } catch {
      return null;
    }
  }

  function saveSession(cwd, { intent, topFiles, query }) {
    const p = sessionPath(cwd);
    fs.mkdirSync(path.dirname(p), { recursive: true });
    fs.writeFileSync(p, JSON.stringify({
      ts: Date.now(),
      intent,
      topFiles,   // [{ file, score }] — top 5 from last ask
      lastQuery: query,
    }));
  }

  // Merge session context as additional ranking signal:
  // any file that appeared in the previous session's top-5 gets a +0.2 boost
  // Reduce to +0.1 if the intent has changed (topic switch guard)
  function mergeSessionContext(scores, session, currentIntent) {
    if (!session) return scores;

    const boostAmount = session.intent === currentIntent ? 0.20 : 0.10;
    const sessionBoost = new Map(session.topFiles.map(f => [f.file, boostAmount]));

    return scores.map(r => ({
      ...r,
      // Additive boost — cannot reduce a score
      score: r.score + (sessionBoost.get(r.file) || 0),
    }));
  }

  function clearSession(cwd) {
    const p = sessionPath(cwd);
    if (fs.existsSync(p)) fs.unlinkSync(p);
  }
  
};

// ── ./src/session/notes ──
__factories["./src/session/notes"] = function(module, exports) {
  
  /**
   * Decision-log notes (Memory, v6.15.0).
   *
   * A tiny append-only log of human/agent notes that survives across sessions,
   * so an agent starting cold can recall "what were we doing / why". Stored as
   * NDJSON under `.context/` to match the project's other state logs
   * (usage.ndjson, benchmark-history.ndjson). Zero dependencies.
   *
   * Consumed by the `read_memory` MCP tool and the `sigmap note` / `sigmap status`
   * commands. Complements `src/session/memory.js` (ranking session) rather than
   * duplicating it.
   */

  const fs = require('fs');
  const path = require('path');
  const { tryGit } = __require('./src/util/git');

  const NOTES_FILE = path.join('.context', 'notes.ndjson');
  const MAX_TEXT = 2000;

  function notesPath(cwd) {
    return path.join(cwd, NOTES_FILE);
  }

  function _currentBranch(cwd) {
    return tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }) || null;
  }

  /**
   * Append a note. Returns the stored entry.
   * @param {string} cwd
   * @param {string} text
   * @param {object} [opts]
   * @param {string|null} [opts.branch]  override branch (default: current git branch)
   * @param {string} [opts.tag]          optional category tag
   */
  function addNote(cwd, text, opts = {}) {
    const clean = String(text == null ? '' : text).trim().slice(0, MAX_TEXT);
    if (!clean) throw new Error('note text is empty');
    const entry = {
      ts: new Date().toISOString(),
      text: clean,
      branch: opts.branch !== undefined ? opts.branch : _currentBranch(cwd),
    };
    if (opts.tag) entry.tag = String(opts.tag).trim().slice(0, 40);
    const p = notesPath(cwd);
    fs.mkdirSync(path.dirname(p), { recursive: true });
    fs.appendFileSync(p, JSON.stringify(entry) + '\n');
    return entry;
  }

  /**
   * Read notes in chronological order (oldest first).
   * @param {string} cwd
   * @param {number} [limit=0]  keep only the most recent N (0 = all)
   * @returns {object[]}
   */
  function readNotes(cwd, limit = 0) {
    let raw;
    try { raw = fs.readFileSync(notesPath(cwd), 'utf8'); }
    catch (_) { return []; }
    const out = [];
    for (const line of raw.split('\n')) {
      const t = line.trim();
      if (!t) continue;
      try { out.push(JSON.parse(t)); } catch (_) { /* skip corrupt line */ }
    }
    if (limit > 0 && out.length > limit) return out.slice(out.length - limit);
    return out;
  }

  /** Format notes as a Markdown list (pass already-ordered notes). */
  function formatNotes(notes) {
    if (!notes || !notes.length) {
      return 'No notes recorded yet. Add one with: sigmap note "<text>"';
    }
    return notes.map((n) => {
      const when = String(n.ts || '').replace('T', ' ').slice(0, 16);
      const br = n.branch ? ` (${n.branch})` : '';
      const tag = n.tag ? ` #${n.tag}` : '';
      return `- [${when}${br}]${tag} ${n.text}`;
    }).join('\n');
  }

  /** Delete the notes log. Returns true if a file was removed. */
  function clearNotes(cwd) {
    try { fs.unlinkSync(notesPath(cwd)); return true; }
    catch (_) { return false; }
  }

  module.exports = { notesPath, addNote, readNotes, formatNotes, clearNotes };
  
};

// ── ./src/squeeze/cilog ──
__factories["./src/squeeze/cilog"] = function(module, exports) {
  
  /**
   * CI / build-log squeeze (v7.0.0).
   *
   * Strips timestamps, progress bars, and repeated noise; keeps every error line
   * plus a small context window around it. Never returns empty — when there are
   * no errors it falls back to a head/tail summary. Also reused by the stacktrace
   * squeezer to clean noise surrounding a trace.
   */

  const TS_PREFIX_RE = /^\s*(?:\[?\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?Z?\]?\s*|\[?\d{1,2}:\d{2}:\d{2}(?:[.,]\d+)?\]?\s*)+/;
  const PROGRESS_LINE_RE = /(?:^|\s)(?:\d{1,3}%|Downloading|Receiving objects|Resolving deltas|Compressing objects|npm (?:WARN|notice|http|sill|verb)|##\[(?:group|endgroup|command|section)\]|\[\d+\/\d+\]|ETA[: ]|█|━|▕|⣿)/;
  const ERROR_RE = /\b(?:error|err!|fail(?:ed|ure)?|exception|panic|fatal|traceback|ERR_|E[A-Z]{3,})\b|✗|❌/i;

  /** Remove a leading timestamp prefix from a single line. */
  function stripTimestamp(line) {
    return line.replace(TS_PREFIX_RE, '');
  }

  /**
   * @param {string} input
   * @param {object} [opts]
   * @param {number} [opts.context=2]  context lines kept around each error
   * @returns {{ squeezed: string, kept: string[], stripped: string[] }}
   */
  function squeezeCiLog(input, opts = {}) {
    const ctx = opts.context != null ? opts.context : 2;
    const lines = input.split('\n');
    const keep = new Set();
    const errorIdx = [];

    for (let i = 0; i < lines.length; i++) {
      if (ERROR_RE.test(lines[i])) {
        errorIdx.push(i);
        for (let j = Math.max(0, i - ctx); j <= Math.min(lines.length - 1, i + ctx); j++) keep.add(j);
      }
    }

    let body;
    let keptDesc;
    if (errorIdx.length === 0) {
      const head = lines.slice(0, 10).map(stripTimestamp);
      const tail = lines.length > 20 ? lines.slice(-10).map(stripTimestamp) : [];
      body = head.slice();
      if (tail.length) body.push(`… (${lines.length - 20} lines omitted) …`, ...tail);
      keptDesc = ['head/tail summary (no error lines found)'];
    } else {
      const sorted = [...keep].sort((a, b) => a - b);
      body = [];
      let prev = -2;
      for (const idx of sorted) {
        // Drop pure progress/noise lines unless they are themselves errors.
        const line = stripTimestamp(lines[idx]);
        if (PROGRESS_LINE_RE.test(line) && !ERROR_RE.test(line)) continue;
        if (idx > prev + 1) body.push(`… (${idx - prev - 1} lines) …`);
        body.push(line);
        prev = idx;
      }
      if (body.length === 0) body = errorIdx.map((i) => stripTimestamp(lines[i])); // safety net
      keptDesc = [`${errorIdx.length} error line(s) + ${ctx}-line context`];
    }

    return {
      squeezed: body.join('\n'),
      kept: keptDesc,
      stripped: [`${Math.max(0, lines.length - body.length)} timestamp/progress/noise line(s)`],
    };
  }

  module.exports = { squeezeCiLog, stripTimestamp, ERROR_RE };
  
};

// ── ./src/squeeze/classify ──
__factories["./src/squeeze/classify"] = function(module, exports) {
  
  /**
   * Squeeze input classifier (v7.0.0).
   *
   * Deterministic, zero-dep detector that labels a pasted blob as a
   * `stacktrace`, `cilog`, or `json` payload — or `null` (pass through). Pure
   * regex/heuristics; runs in well under 10ms even on large input.
   *
   * Order matters: stack traces are highest value and a CI log often *contains*
   * a trace, so stacktrace is checked first, then cilog, then json.
   */

  const FRAME_RE = [
    /^\s*at\s+.+\(.+:\d+:\d+\)\s*$/,          // JS: at fn (file:line:col)
    /^\s*at\s+.+:\d+:\d+\s*$/,                 // JS: at file:line:col
    /^\s*at\s+[\w$.<>]+\(.+\.\w+:\d+\)/,       // Java/Kotlin: at pkg.Cls.m(File.java:42)
    /^\s*File\s+".+",\s+line\s+\d+/,           // Python frame
    /^\s+\w+.*\([^)]*\.(go|rs):\d+\)/,         // Go/Rust frame with file:line
    /^\s*#\d+\s+0x[0-9a-f]+/,                  // native/gdb frame
  ];

  const STACK_HEADER_RE = [
    /Traceback \(most recent call last\)/,
    /Exception in thread/,
    /\bat Object\.<anonymous>\b/,
    /thread '.*' panicked at/,
    /^panic:/m,
    /goroutine \d+ \[/,
  ];

  function countFrames(lines) {
    let n = 0;
    for (const line of lines) {
      if (FRAME_RE.some((re) => re.test(line))) n++;
    }
    return n;
  }

  function matchesStackTrace(input, lines) {
    const frames = countFrames(lines);
    const header = STACK_HEADER_RE.some((re) => re.test(input));
    // A single explicit header (Traceback/panic) counts as strong evidence even
    // with few parsed frames; otherwise require 2+ frame-like lines.
    if (!header && frames < 2) return { match: false, confidence: 0, frames };
    // Confidence scales with frame count; a header alone floors it at 0.6.
    let confidence = Math.min(0.97, 0.15 * frames);
    if (header) confidence = Math.max(confidence, 0.6 + Math.min(0.3, 0.05 * frames));
    return { match: true, confidence: Number(confidence.toFixed(2)), frames };
  }

  const TS_RE = /(\b\d{1,2}:\d{2}:\d{2}\b)|(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})/;
  const PROGRESS_RE = /(\d{1,3}%)|(\bDownloading\b)|(\bnpm (WARN|notice|http)\b)|(##\[(group|endgroup|command)\])|(\[\d+\/\d+\])|(▕|█|━|⠿)|(\bETA\b)|(\r$)/;

  function matchesCiLog(input, lines) {
    if (lines.length < 8) return { match: false, confidence: 0 };
    let logish = 0;
    const seen = new Map();
    let repeats = 0;
    for (const line of lines) {
      if (TS_RE.test(line) || PROGRESS_RE.test(line)) logish++;
      const norm = line.replace(/\d+/g, '#').trim();
      if (norm) {
        const c = (seen.get(norm) || 0) + 1;
        seen.set(norm, c);
        if (c > 1) repeats++;
      }
    }
    const density = logish / lines.length;
    const repeatRatio = repeats / lines.length;
    if (density < 0.4 && repeatRatio < 0.4) return { match: false, confidence: 0 };
    const confidence = Math.min(0.95, Math.max(density, repeatRatio) + 0.15);
    return { match: true, confidence: Number(confidence.toFixed(2)) };
  }

  function matchesJsonPayload(input) {
    const trimmed = input.trim();
    if (/^[[{]/.test(trimmed)) {
      try {
        JSON.parse(trimmed);
        return { match: true, confidence: 0.95 };
      } catch (_) { /* not strict JSON — fall through to heuristic */ }
    }
    const lines = trimmed.split('\n');
    if (lines.length < 4) return { match: false, confidence: 0 };
    let kv = 0;
    for (const line of lines) {
      if (/^\s*"[^"]+"\s*:\s*.+/.test(line) || /^\s*[}\]],?\s*$/.test(line)) kv++;
    }
    const ratio = kv / lines.length;
    if (ratio < 0.6) return { match: false, confidence: 0 };
    return { match: true, confidence: Number(Math.min(0.9, ratio).toFixed(2)) };
  }

  /**
   * @param {string} input
   * @returns {{ category: 'stacktrace'|'cilog'|'json'|null, confidence: number }}
   */
  function classify(input) {
    if (typeof input !== 'string' || !input.trim()) return { category: null, confidence: 0 };
    const lines = input.split('\n');

    const st = matchesStackTrace(input, lines);
    if (st.match) return { category: 'stacktrace', confidence: st.confidence };

    const ci = matchesCiLog(input, lines);
    if (ci.match) return { category: 'cilog', confidence: ci.confidence };

    const js = matchesJsonPayload(input);
    if (js.match) return { category: 'json', confidence: js.confidence };

    return { category: null, confidence: 0 };
  }

  module.exports = { classify, countFrames };
  
};

// ── ./src/squeeze/index ──
__factories["./src/squeeze/index"] = function(module, exports) {
  
  /**
   * Squeeze orchestrator (v7.0.0): classify → squeeze → reduction → decision.
   *
   * Always-on and silent: callers run `squeeze()` on pasted input, then use
   * `shouldPrompt()` to decide whether the reduction is worth interrupting for.
   * Everything is deterministic and offline; the symbol index for stack-trace
   * enrichment is passed through via `opts.symbolIndex`.
   */

  const { classify } = __require('./src/squeeze/classify');
  const { squeezeStackTrace } = __require('./src/squeeze/stacktrace');
  const { squeezeCiLog } = __require('./src/squeeze/cilog');
  const { squeezeJsonPayload } = __require('./src/squeeze/jsonpayload');

  function estimateTokens(s) { return Math.ceil(String(s || '').length / 4); }

  /**
   * @param {string} input
   * @param {object} [opts]  forwarded to the category squeezer (srcDirs, symbolIndex, …)
   * @returns {{ category, confidence, original, squeezed, rawTokens, squeezedTokens, reduction, kept, stripped, enriched, applies }}
   */
  function squeeze(input, opts = {}) {
    const { category, confidence } = classify(input);
    const rawTokens = estimateTokens(input);
    const base = {
      category, confidence, original: input, squeezed: input,
      rawTokens, squeezedTokens: rawTokens, reduction: 0,
      kept: [], stripped: [], enriched: false, applies: false,
    };
    if (!category) return base;

    let r;
    if (category === 'stacktrace') r = squeezeStackTrace(input, opts);
    else if (category === 'cilog') r = squeezeCiLog(input, opts);
    else r = squeezeJsonPayload(input, opts);

    const squeezedTokens = estimateTokens(r.squeezed);
    const reduction = rawTokens > 0 ? (rawTokens - squeezedTokens) / rawTokens : 0;
    return {
      category, confidence,
      original: input, squeezed: r.squeezed,
      rawTokens, squeezedTokens,
      reduction: Math.max(0, Number(reduction.toFixed(4))),
      kept: r.kept || [], stripped: r.stripped || [], enriched: !!r.enriched,
      applies: squeezedTokens < rawTokens,
    };
  }

  /** True when the reduction clears the threshold (accepts 0–1 or 0–100). */
  function shouldPrompt(reduction, threshold) {
    const t = threshold > 1 ? threshold / 100 : threshold;
    return reduction >= t;
  }

  /** A compact human summary of what squeeze would do (for the prompt). */
  function formatSummary(result) {
    const pct = Math.round(result.reduction * 100);
    const lines = [
      `Input: ${result.rawTokens.toLocaleString()} tokens`,
      `Can reduce to ${result.squeezedTokens.toLocaleString()} tokens (${pct}% smaller):`,
    ];
    for (const k of result.kept) lines.push(`  ✓ Kept: ${k}`);
    for (const s of result.stripped) lines.push(`  ✗ Stripped: ${s}`);
    return lines.join('\n');
  }

  module.exports = { squeeze, shouldPrompt, formatSummary, estimateTokens };
  
};

// ── ./src/squeeze/jsonpayload ──
__factories["./src/squeeze/jsonpayload"] = function(module, exports) {
  
  /**
   * JSON-payload squeeze (v7.0.0).
   *
   * Collapses repeated array elements, truncates long string values, and
   * preserves the schema shape at every depth — so an LLM still sees the
   * structure of an API/GraphQL/validation error without the bulk.
   */

  const MAX_STR = 500;
  const ARRAY_KEEP = 2;

  function squeezeValue(v, opts) {
    const maxStr = opts.maxStr;
    const keep = opts.arrayKeep;
    if (Array.isArray(v)) {
      if (v.length <= keep + 1) return v.map((x) => squeezeValue(x, opts));
      const head = v.slice(0, keep).map((x) => squeezeValue(x, opts));
      head.push(`…${v.length - keep} more similar items`);
      return head;
    }
    if (v && typeof v === 'object') {
      const o = {};
      for (const k of Object.keys(v)) o[k] = squeezeValue(v[k], opts);
      return o;
    }
    if (typeof v === 'string' && v.length > maxStr) {
      return v.slice(0, maxStr) + `…(${v.length} chars)`;
    }
    return v;
  }

  /**
   * @param {string} input
   * @param {object} [opts]
   * @param {number} [opts.maxStr=500]    truncate strings longer than this
   * @param {number} [opts.arrayKeep=2]   array items kept before collapsing
   * @returns {{ squeezed, kept, stripped }}
   */
  function squeezeJsonPayload(input, opts = {}) {
    let parsed;
    try { parsed = JSON.parse(input); }
    catch (_) { return { squeezed: input, kept: ['(not valid JSON — unchanged)'], stripped: [] }; }
    const cfg = { maxStr: opts.maxStr != null ? opts.maxStr : MAX_STR, arrayKeep: opts.arrayKeep != null ? opts.arrayKeep : ARRAY_KEEP };
    const squeezed = JSON.stringify(squeezeValue(parsed, cfg), null, 2);
    return {
      squeezed,
      kept: ['schema shape preserved at all depths'],
      stripped: ['collapsed repeated array items; truncated long string values'],
    };
  }

  module.exports = { squeezeJsonPayload, squeezeValue };
  
};

// ── ./src/squeeze/stacktrace ──
__factories["./src/squeeze/stacktrace"] = function(module, exports) {
  
  /**
   * Stack-trace squeeze (v7.0.0) — the highest-value squeeze module.
   *
   * Dedupes repeated exceptions, strips vendor frames, keeps frames in the user's
   * own source dirs, and — the differentiator — **enriches the top kept frame**
   * with its real signature from the SigMap symbol index (`buildSigIndex`).
   * Generic log summarizers can't do this; SigMap has the repo's symbol map.
   *
   * Pure/deterministic. The symbol index is injected via `opts.symbolIndex` so
   * the module is unit-testable without touching the filesystem.
   */

  const path = require('path');

  const VENDOR_RE = /(?:^|[\\/])(?:node_modules|vendor|site-packages|dist|build|\.venv|venv|third_party|external|\.cargo|go\/pkg\/mod)[\\/]/;

  /** Parse a frame line across JS/TS, Python, Java/Kotlin, Go, Rust, native. */
  function parseFrame(line) {
    let m;
    if ((m = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
    if ((m = line.match(/^\s*at\s+(.+?):(\d+):(\d+)\s*$/))) return { fn: '', file: m[1], line: +m[2], raw: line };
    if ((m = line.match(/^\s*at\s+([\w$.<>]+)\((.+?):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
    if ((m = line.match(/^\s*File\s+"(.+?)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?/))) return { fn: (m[3] || '').trim(), file: m[1], line: +m[2], raw: line };
    if ((m = line.match(/^\s*(.+\.(?:go|rs)):(\d+)/))) return { fn: '', file: m[1], line: +m[2], raw: line };
    return null;
  }

  function isVendor(file) { return VENDOR_RE.test(String(file).replace(/\\/g, '/')); }

  function inSrcDirs(file, srcDirs) {
    const f = String(file).replace(/\\/g, '/');
    return srcDirs.some((d) => {
      const dd = String(d).replace(/^\.\//, '').replace(/\/$/, '');
      return dd && (f === dd || f.startsWith(dd + '/') || f.includes('/' + dd + '/'));
    });
  }

  /** Look up the real signature for a frame in the SigMap symbol index. */
  function enrichFrame(frame, symbolIndex) {
    if (!symbolIndex || !frame) return null;
    const want = String(frame.file).replace(/\\/g, '/');
    const base = path.basename(want);
    let key = null;
    for (const k0 of symbolIndex.keys()) {
      const k = String(k0).replace(/\\/g, '/');
      if (k === want || want.endsWith('/' + k) || k.endsWith('/' + want)) { key = k0; break; }
      if (!key && path.basename(k) === base) key = k0;
    }
    if (!key) return null;
    const sigs = symbolIndex.get(key) || [];
    const wantFn = frame.fn ? frame.fn.split('.').pop() : '';
    let byLine = null, byName = null;
    for (const sig of sigs) {
      const s = String(sig);
      const mm = s.match(/:(\d+)(?:-(\d+))?\s*$/);
      if (mm) {
        const a = +mm[1], b = mm[2] ? +mm[2] : a;
        if (frame.line >= a && frame.line <= b) byLine = s;
      }
      if (wantFn && new RegExp('\\b' + wantFn.replace(/[^\w$]/g, '') + '\\b').test(s)) byName = byName || s;
    }
    const sig = byLine || byName;
    return sig ? { file: key, sig: sig.replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim() } : null;
  }

  /**
   * @param {string} input
   * @param {object} [opts]
   * @param {string[]} [opts.srcDirs]      user source dirs (default ['src'])
   * @param {Map}      [opts.symbolIndex]  SigMap signature index for enrichment
   * @param {number}   [opts.maxFrames=8]  cap on kept source frames
   * @returns {{ squeezed, kept, stripped, enriched }}
   */
  function squeezeStackTrace(input, opts = {}) {
    const srcDirs = (opts.srcDirs && opts.srcDirs.length) ? opts.srcDirs : ['src'];
    const maxFrames = opts.maxFrames != null ? opts.maxFrames : 8;
    const lines = input.split('\n');

    const headerCount = new Map();
    const headerOrder = [];
    const frames = [];
    for (const line of lines) {
      const f = parseFrame(line);
      if (f) { frames.push(f); continue; }
      const t = line.trim();
      if (!t) continue;
      if (!headerCount.has(t)) headerOrder.push(t);
      headerCount.set(t, (headerCount.get(t) || 0) + 1);
    }

    const seen = new Set();
    let dupFrames = 0;
    const nonVendor = [];
    const sourceFrames = [];
    let vendorCount = 0;
    for (const f of frames) {
      const k = f.file + ':' + f.line;
      if (seen.has(k)) { dupFrames++; continue; }
      seen.add(k);
      if (isVendor(f.file)) { vendorCount++; continue; }
      nonVendor.push(f);
      if (inSrcDirs(f.file, srcDirs)) sourceFrames.push(f);
    }

    // Prefer source frames; never return empty (fall back to top non-vendor, then raw).
    const shown = sourceFrames.length ? sourceFrames.slice(0, maxFrames)
      : (nonVendor.length ? nonVendor.slice(0, 3) : frames.slice(0, 3));

    const enrichment = shown.length ? enrichFrame(shown[0], opts.symbolIndex) : null;

    const out = [];
    for (const h of headerOrder) {
      const n = headerCount.get(h);
      out.push(n > 1 ? `${h}   (occurred ×${n})` : h);
    }
    for (let i = 0; i < shown.length; i++) {
      out.push('    ' + shown[i].raw.trim());
      if (i === 0 && enrichment) out.push(`      ↳ ${enrichment.sig}   [${enrichment.file}]`);
    }

    return {
      squeezed: out.join('\n'),
      kept: [
        `${headerOrder.length} unique exception(s)`,
        `top ${shown.length} ${sourceFrames.length ? 'source ' : ''}frame(s)`,
        ...(enrichment ? [`enriched ${path.basename(shown[0].file)}:${shown[0].line}`] : []),
      ],
      stripped: [`${vendorCount} vendor frame(s)`, `${dupFrames} duplicate frame(s)`],
      enriched: !!enrichment,
    };
  }

  module.exports = { squeezeStackTrace, parseFrame, isVendor, inSrcDirs, enrichFrame };
  
};

// ── ./src/tracking/aggregate ──
__factories["./src/tracking/aggregate"] = function(module, exports) {
  
  /**
   * SigMap usage aggregation for the `gain` dashboard.
   *
   * Pure, zero-dependency functions that turn raw NDJSON usage records into the
   * totals / by-operation / time-bucket shapes the terminal renderer consumes.
   *
   * Tolerant of BOTH schemas:
   *   - new:    { op, baselineTokens, actualTokens, savedTokens, savedPct, durationMs, model }
   *   - legacy: { rawTokens, finalTokens, reductionPct }   (from logger.js v0.9)
   *
   * "saved" is a counterfactual estimate (baseline − actual), never a measured
   * delta. Callers are responsible for labeling it as such in the UI.
   */

  const { resolvePrice } = __require('./src/tracking/pricing');

  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));

  /**
   * Normalize one raw record into a canonical shape.
   * @param {object} rec
   */
  function normalize(rec) {
    const baseline = num(rec.baselineTokens != null ? rec.baselineTokens : rec.rawTokens);
    const actual = num(rec.actualTokens != null ? rec.actualTokens : rec.finalTokens);
    const saved = rec.savedTokens != null ? num(rec.savedTokens) : Math.max(0, baseline - actual);
    const savedPct = rec.savedPct != null
      ? num(rec.savedPct)
      : rec.reductionPct != null
        ? num(rec.reductionPct)
        : baseline > 0 ? (saved / baseline) * 100 : 0;
    return {
      ts: rec.ts || null,
      op: normalizeOp(rec.op),
      baseline,
      actual,
      saved,
      savedPct: clamp(savedPct, 0, 100),
      durationMs: num(rec.durationMs),
      model: rec.model || null,
    };
  }

  function normalizeOp(op) {
    if (!op) return 'generate';
    return String(op);
  }

  function num(v) {
    const n = Number(v);
    return Number.isFinite(n) ? n : 0;
  }

  /**
   * Parse a --since value into a cutoff Date (or null for "all time").
   * Accepts: "7d", "30d", "12h", or an ISO date "2026-06-01".
   * @param {string} since
   * @param {number} [nowMs] - injectable clock for tests
   * @returns {Date|null}
   */
  function parseSince(since, nowMs) {
    if (!since) return null;
    const now = nowMs != null ? nowMs : Date.now();
    const rel = /^(\d+)([dhw])$/.exec(String(since).trim());
    if (rel) {
      const n = parseInt(rel[1], 10);
      const unit = rel[2];
      const ms = unit === 'h' ? 3.6e6 : unit === 'w' ? 6.048e8 : 8.64e7;
      return new Date(now - n * ms);
    }
    const d = new Date(since);
    return Number.isNaN(d.getTime()) ? null : d;
  }

  /**
   * Bucket records by calendar granularity.
   * @param {object[]} records - normalized records
   * @param {'day'|'week'|'month'} granularity
   * @returns {Array<{key,count,baseline,actual,saved,savedPct,ms}>} ascending by key
   */
  function bucketBy(records, granularity) {
    const map = new Map();
    for (const r of records) {
      if (!r.ts) continue;
      const key = bucketKey(r.ts, granularity);
      if (!key) continue;
      let b = map.get(key);
      if (!b) { b = { key, count: 0, baseline: 0, actual: 0, saved: 0, ms: 0 }; map.set(key, b); }
      b.count += 1;
      b.baseline += r.baseline;
      b.actual += r.actual;
      b.saved += r.saved;
      b.ms += r.durationMs;
    }
    return [...map.values()]
      .map((b) => ({ ...b, savedPct: b.baseline > 0 ? clamp((b.saved / b.baseline) * 100, 0, 100) : 0 }))
      .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
  }

  function bucketKey(ts, granularity) {
    const d = new Date(ts);
    if (Number.isNaN(d.getTime())) return null;
    const y = d.getUTCFullYear();
    const m = String(d.getUTCMonth() + 1).padStart(2, '0');
    const day = String(d.getUTCDate()).padStart(2, '0');
    if (granularity === 'month') return `${y}-${m}`;
    if (granularity === 'week') {
      // ISO-ish: key by the Monday (UTC) of that week.
      const tmp = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate()));
      const dow = (tmp.getUTCDay() + 6) % 7; // 0 = Monday
      tmp.setUTCDate(tmp.getUTCDate() - dow);
      return `${tmp.getUTCFullYear()}-${String(tmp.getUTCMonth() + 1).padStart(2, '0')}-${String(tmp.getUTCDate()).padStart(2, '0')}`;
    }
    return `${y}-${m}-${day}`;
  }

  /**
   * Full aggregation for the `gain` dashboard.
   * @param {object[]} rawRecords
   * @param {object} [opts]
   * @param {string} [opts.model]   pricing model
   * @param {string} [opts.since]   window filter
   * @param {number} [opts.top]     limit byOp rows (0 = all)
   * @param {number} [opts.nowMs]   injectable clock
   * @returns {object}
   */
  function aggregate(rawRecords, opts = {}) {
    const price = resolvePrice(opts.model);
    const cutoff = parseSince(opts.since, opts.nowMs);

    let records = (rawRecords || []).map(normalize);
    if (cutoff) records = records.filter((r) => r.ts && new Date(r.ts) >= cutoff);

    const totals = {
      count: records.length,
      baseline: 0,
      actual: 0,
      saved: 0,
      totalMs: 0,
      savedPct: 0,
      avgMs: 0,
      usdSaved: 0,
      firstTs: null,
      lastTs: null,
    };

    const opMap = new Map();
    for (const r of records) {
      totals.baseline += r.baseline;
      totals.actual += r.actual;
      totals.saved += r.saved;
      totals.totalMs += r.durationMs;
      if (r.ts) {
        if (!totals.firstTs || r.ts < totals.firstTs) totals.firstTs = r.ts;
        if (!totals.lastTs || r.ts > totals.lastTs) totals.lastTs = r.ts;
      }
      let o = opMap.get(r.op);
      if (!o) { o = { op: r.op, count: 0, baseline: 0, saved: 0, ms: 0 }; opMap.set(r.op, o); }
      o.count += 1;
      o.baseline += r.baseline;
      o.saved += r.saved;
      o.ms += r.durationMs;
    }

    totals.savedPct = totals.baseline > 0 ? clamp((totals.saved / totals.baseline) * 100, 0, 100) : 0;
    totals.avgMs = totals.count > 0 ? Math.round(totals.totalMs / totals.count) : 0;
    totals.usdSaved = totals.saved * price.perToken;

    let byOp = [...opMap.values()].map((o) => ({
      op: o.op,
      count: o.count,
      saved: o.saved,
      avgPct: o.baseline > 0 ? clamp((o.saved / o.baseline) * 100, 0, 100) : 0,
      avgMs: o.count > 0 ? Math.round(o.ms / o.count) : 0,
      usdSaved: o.saved * price.perToken,
      sharePct: totals.saved > 0 ? (o.saved / totals.saved) * 100 : 0,
    })).sort((a, b) => b.saved - a.saved);

    if (opts.top && opts.top > 0) byOp = byOp.slice(0, opts.top);

    return {
      price,
      totals,
      byOp,
      buckets: {
        daily: bucketBy(records, 'day'),
        weekly: bucketBy(records, 'week'),
        monthly: bucketBy(records, 'month'),
      },
    };
  }

  module.exports = { aggregate, bucketBy, parseSince, normalize };
  
};

// ── ./src/tracking/logger ──
__factories["./src/tracking/logger"] = function(module, exports) {
  
  /**
   * SigMap usage logger (v0.9)
   *
   * Writes an append-only newline-delimited JSON (NDJSON) log at
   *   .context/usage.ndjson
   *
   * Each line is one JSON object describing a gen-context run.
   * Zero npm dependencies — pure Node.js fs.
   *
   * Enabled by:
   *   config.tracking: true   (gen-context.config.json)
   *   --track CLI flag
   */

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

  const LOG_FILE = path.join('.context', 'usage.ndjson');
  // Dedicated log for the `gain` dashboard (extended schema). Kept separate from
  // usage.ndjson so the legacy health/nudge history never collides with it.
  const GAIN_FILE = path.join('.context', 'gain.ndjson');

  /**
   * Append one run entry to the usage log.
   * @param {object} entry - Run metrics from runGenerate()
   * @param {string} cwd   - Project root (absolute path)
   */
  function logRun(entry, cwd) {
    try {
      const logPath = path.join(cwd, LOG_FILE);
      const dir = path.dirname(logPath);
      if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

      const record = {
        ts: new Date().toISOString(),
        version: entry.version || '0.9.0',
        fileCount: entry.fileCount || 0,
        droppedCount: entry.droppedCount || 0,
        rawTokens: entry.rawTokens || 0,
        finalTokens: entry.finalTokens || 0,
        reductionPct: entry.rawTokens > 0
          ? parseFloat((100 - (entry.finalTokens / entry.rawTokens) * 100).toFixed(1))
          : 0,
        overBudget: entry.overBudget || false,
        budgetLimit: entry.budgetLimit || 6000,
      };

      fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
    } catch (err) {
      // Never crash the main process — tracking is optional
      process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
    }
  }

  /**
   * Read and parse all usage log entries.
   * @param {string} cwd - Project root (absolute path)
   * @returns {object[]} Array of parsed log records (oldest first)
   */
  function readLog(cwd) {
    try {
      const logPath = path.join(cwd, LOG_FILE);
      if (!fs.existsSync(logPath)) return [];
      const raw = fs.readFileSync(logPath, 'utf8');
      return raw
        .split('\n')
        .filter(Boolean)
        .map((line) => {
          try { return JSON.parse(line); } catch (_) { return null; }
        })
        .filter(Boolean);
    } catch (_) {
      return [];
    }
  }

  /**
   * Read and parse all `gain` dashboard records (oldest first).
   * @param {string} cwd
   * @returns {object[]}
   */
  function readGainLog(cwd) {
    try {
      const logPath = path.join(cwd, GAIN_FILE);
      if (!fs.existsSync(logPath)) return [];
      return fs.readFileSync(logPath, 'utf8')
        .split('\n')
        .filter(Boolean)
        .map((line) => { try { return JSON.parse(line); } catch (_) { return null; } })
        .filter(Boolean);
    } catch (_) {
      return [];
    }
  }

  /**
   * Compute summary statistics from an array of log records.
   * @param {object[]} entries
   * @returns {object} Summary stats
   */
  function summarize(entries) {
    if (!entries || entries.length === 0) {
      return {
        totalRuns: 0,
        avgReductionPct: 0,
        avgFinalTokens: 0,
        avgRawTokens: 0,
        minFinalTokens: 0,
        maxFinalTokens: 0,
        firstRun: null,
        lastRun: null,
        overBudgetRuns: 0,
      };
    }

    const reductions = entries.map((e) => e.reductionPct || 0);
    const finals = entries.map((e) => e.finalTokens || 0);
    const raws = entries.map((e) => e.rawTokens || 0);

    const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;

    return {
      totalRuns: entries.length,
      avgReductionPct: parseFloat(avg(reductions).toFixed(1)),
      avgFinalTokens: Math.round(avg(finals)),
      avgRawTokens: Math.round(avg(raws)),
      minFinalTokens: Math.min(...finals),
      maxFinalTokens: Math.max(...finals),
      firstRun: entries[0].ts || null,
      lastRun: entries[entries.length - 1].ts || null,
      overBudgetRuns: entries.filter((e) => e.overBudget).length,
    };
  }

  /**
   * Whether `gain` savings capture is enabled. Default: ON (privacy-safe,
   * local-only, counts only — no paths, source, or query text). This is
   * intentionally decoupled from the legacy `config.tracking` flag (which gates
   * the usage.ndjson health log and defaults OFF). Opt out of gain capture via:
   *   config.gainTracking === false   ·   --no-track   ·   SIGMAP_NO_TRACK=1
   * @param {object} [config]
   * @param {string[]} [argv]
   * @returns {boolean}
   */
  function isTrackingEnabled(config, argv) {
    const a = argv || (typeof process !== 'undefined' ? process.argv : []);
    if (process.env && process.env.SIGMAP_NO_TRACK) return false;
    if (a && a.includes('--no-track')) return false;
    if (config && config.gainTracking === false) return false;
    return true;
  }

  /**
   * Append one operation to the usage log using the extended `gain` schema.
   * Reuses the same NDJSON file as logRun and is tolerant of partial input.
   * Never throws — tracking must never break the main process.
   *
   * @param {object} entry
   * @param {string} entry.op             e.g. 'ask' | 'generate' | 'query' | 'mcp:get_map'
   * @param {number} entry.baselineTokens whole-file / candidate baseline (counterfactual)
   * @param {number} entry.actualTokens   tokens SigMap actually emitted
   * @param {number} [entry.durationMs]
   * @param {string} [entry.model]
   * @param {string} [entry.version]
   * @param {string} cwd
   */
  function recordUsage(entry, cwd) {
    try {
      const logPath = path.join(cwd, GAIN_FILE);
      const dir = path.dirname(logPath);
      if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

      const baseline = Math.max(0, Number(entry.baselineTokens) || 0);
      const actual = Math.max(0, Number(entry.actualTokens) || 0);
      const saved = Math.max(0, baseline - actual);
      const record = {
        ts: new Date().toISOString(),
        v: entry.version || '0.9.0',
        op: entry.op || 'generate',
        baselineTokens: baseline,
        actualTokens: actual,
        savedTokens: saved,
        savedPct: baseline > 0 ? parseFloat(((saved / baseline) * 100).toFixed(1)) : 0,
        durationMs: Math.max(0, Math.round(Number(entry.durationMs) || 0)),
        model: entry.model || null,
        ok: entry.ok !== false,
      };
      fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
    } catch (err) {
      // Never crash the main process — tracking is optional.
      if (process.stderr) process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
    }
  }

  module.exports = { logRun, recordUsage, readLog, readGainLog, summarize, isTrackingEnabled, GAIN_FILE };
  
};

// ── ./src/tracking/pricing ──
__factories["./src/tracking/pricing"] = function(module, exports) {
  
  /**
   * SigMap pricing table — input-token $/Mtok assumptions for the `gain` dashboard.
   *
   * These are ASSUMPTIONS used only to translate "tokens saved" into an estimated
   * dollar figure. They are deliberately conservative and configurable via
   *   --model <name>   or   config.pricingModel
   * The `gain` views always print the model + rate inline so the $ is never
   * presented as exact. Zero npm dependencies.
   */

  // USD per 1,000,000 input tokens. Claude rates verified 2026-07 against
  // platform.claude.com (Opus 4.8 $5, Sonnet 5/4.6 $3, Haiku 4.5 $1); GPT-4o $2.50.
  const PRICES = {
    'claude-sonnet': 3.0,
    'claude-opus': 5.0,
    'claude-haiku': 1.0,
    'gpt-4o': 2.5,
    'gpt-4o-mini': 0.15,
    'gemini-1.5-pro': 1.25,
    'gemini-1.5-flash': 0.075,
  };

  const DEFAULT_MODEL = 'claude-sonnet';

  /**
   * Resolve a price (USD per token) for a model name.
   * @param {string} [model]
   * @returns {{ model: string, perMtok: number, perToken: number }}
   */
  function resolvePrice(model) {
    const key = (model || DEFAULT_MODEL).toLowerCase();
    const perMtok = PRICES[key] != null ? PRICES[key] : PRICES[DEFAULT_MODEL];
    const resolved = PRICES[key] != null ? key : DEFAULT_MODEL;
    return { model: resolved, perMtok, perToken: perMtok / 1_000_000 };
  }

  /** @returns {string[]} known model keys */
  function listModels() {
    return Object.keys(PRICES);
  }

  module.exports = { PRICES, DEFAULT_MODEL, resolvePrice, listModels };
  
};

// ── ./src/util/git ──
__factories["./src/util/git"] = function(module, exports) {
  
  /**
   * Shell-free git invocation.
   *
   * Uses `execFileSync('git', [...])`, which executes the git binary directly —
   * it never spawns a system shell (`/bin/sh -c`). That means:
   *   - no shell-injection surface (arguments are passed as an array, never
   *     interpolated into a command string), and
   *   - supply-chain scanners (e.g. Socket) do not flag a "Shell access" capability.
   *
   * stderr is discarded by default (replaces the old `2>/dev/null` redirects).
   */

  const { execFileSync } = require('child_process');

  function git(args, opts = {}) {
    return execFileSync('git', args, {
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
      ...opts,
    });
  }

  // Convenience: run git and return trimmed stdout, or '' on any failure.
  function tryGit(args, opts = {}) {
    try { return git(args, opts).toString().trim(); }
    catch (_) { return ''; }
  }

  module.exports = { git, tryGit };
  
};

// ── ./src/verify/closest-match ──
__factories["./src/verify/closest-match"] = function(module, exports) {
  
  /**
   * Closest-match suggestions for the Hallucination Guard (v6.15.0).
   *
   * Heuristic layer (plan §5 labels this "Medium" confidence): given a name the
   * detectors flagged as fake, find the nearest *real* candidate by Levenshtein
   * distance so the report can say "Did you mean `loadConfig()` in
   * src/config/loader.js:42?". Pure, deterministic, offline — no network, no LLM.
   *
   * All inputs are passed in (symbol/file/script candidate lists) so this module
   * stays unit-testable without touching the filesystem or the SigMap index.
   */

  /**
   * Levenshtein edit distance with an early-exit ceiling.
   * Returns `max + 1` as soon as the best achievable distance exceeds `max`,
   * so callers can cheaply reject far-apart strings.
   */
  function levenshtein(a, b, max = Infinity) {
    if (a === b) return 0;
    const al = a.length;
    const bl = b.length;
    if (al === 0) return bl;
    if (bl === 0) return al;
    if (Math.abs(al - bl) > max) return max + 1;

    let prev = new Array(bl + 1);
    let curr = new Array(bl + 1);
    for (let j = 0; j <= bl; j++) prev[j] = j;

    for (let i = 1; i <= al; i++) {
      curr[0] = i;
      let rowMin = curr[0];
      const ca = a.charCodeAt(i - 1);
      for (let j = 1; j <= bl; j++) {
        const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
        curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
        if (curr[j] < rowMin) rowMin = curr[j];
      }
      if (rowMin > max) return max + 1;
      const tmp = prev;
      prev = curr;
      curr = tmp;
    }
    return prev[bl];
  }

  /** Bucket a normalized edit distance into a confidence label (plan §5). */
  function suggestionConfidence(distance, targetLen) {
    const ratio = distance / Math.max(targetLen, 1);
    if (distance === 0 || ratio <= 0.2) return 'high';
    if (ratio <= 0.4) return 'medium';
    return 'low';
  }

  /**
   * Find the nearest candidate name to `target`.
   *
   * @param {string} target
   * @param {Array<string | { name: string, file?: string, line?: number }>} candidates
   * @param {object} [opts]
   * @param {number} [opts.maxRatio=0.5]  reject matches farther than ratio·len edits
   * @param {number} [opts.minLen=3]      skip very short targets (too noisy)
   * @returns {{ name, file, line, distance, confidence } | null}
   */
  function closestMatch(target, candidates, opts = {}) {
    const maxRatio = opts.maxRatio != null ? opts.maxRatio : 0.5;
    const minLen = opts.minLen != null ? opts.minLen : 3;
    if (!target || target.length < minLen) return null;
    if (!candidates || candidates.length === 0) return null;

    const lower = target.toLowerCase();
    const cap = Math.max(1, Math.ceil(target.length * maxRatio));
    let best = null;

    for (const c of candidates) {
      const name = typeof c === 'string' ? c : c && c.name;
      if (!name || name === target) continue;
      if (Math.abs(name.length - target.length) > cap) continue;
      const d = levenshtein(lower, name.toLowerCase(), cap);
      if (d > cap) continue;
      if (!best || d < best.distance ||
          (d === best.distance && name.length < best.name.length)) {
        best = {
          name,
          file: typeof c === 'object' ? c.file : undefined,
          line: typeof c === 'object' ? c.line : undefined,
          distance: d,
        };
        if (d === 0) break; // case-only difference — can't beat it
      }
    }

    if (!best) return null;
    best.confidence = suggestionConfidence(best.distance, target.length);
    return best;
  }

  /**
   * Build `[{ name, file, line }]` symbol candidates from a SigMap signature
   * index (`Map<file, string[]>` whose entries may carry a `:start-end` anchor).
   */
  function buildSymbolCandidates(sigIndex) {
    const out = [];
    const seen = new Set();
    if (!sigIndex) return out;
    for (const [file, sigs] of sigIndex) {
      for (const sig of sigs) {
        const s = String(sig);
        const lineM = s.match(/:(\d+)(?:-\d+)?\s*$/);
        const line = lineM ? parseInt(lineM[1], 10) : null;
        const cleaned = s.replace(/\s*:\d+(?:-\d+)?\s*$/, '');
        const m = cleaned.match(/\b(?:async\s+function|function|class|def|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/)
          || cleaned.match(/([A-Za-z_$][\w$]*)\s*\(/)
          || cleaned.match(/([A-Za-z_$][\w$]*)/);
        if (!m) continue;
        const name = m[1];
        const key = name + '@' + file;
        if (seen.has(key)) continue;
        seen.add(key);
        out.push({ name, file, line });
      }
    }
    return out;
  }

  /** Format a suggestion object into a human one-liner for reports/CLI. */
  function formatSuggestion(match, asCall) {
    if (!match) return null;
    const sym = asCall ? `${match.name}()` : match.name;
    let where = '';
    if (match.file) {
      where = match.line ? ` in ${match.file}:${match.line}` : ` in ${match.file}`;
    }
    return `Did you mean \`${sym}\`${where}?`;
  }

  module.exports = {
    levenshtein,
    closestMatch,
    buildSymbolCandidates,
    suggestionConfidence,
    formatSuggestion,
  };
  
};

// ── ./src/verify/hallucination-guard ──
__factories["./src/verify/hallucination-guard"] = function(module, exports) {
  
  /**
   * Hallucination Guard — deterministic core (Reliable MVP, v6.15.0).
   *
   * Given the text of an AI answer, flag claims that do not match the repo:
   *   - fake-file      : a referenced path is not on disk
   *   - fake-test-file : a referenced *test* path is not on disk (sub-type)
   *   - fake-import    : a relative import does not resolve; a bare import is
   *                      absent from package.json deps (builtins allow-listed)
   *   - fake-symbol    : a called function/class is absent from the symbol index
   *   - fake-npm-script: `npm run X` where X is not a package.json script
   *
   * Each issue carries a `confidence` (detection certainty) and, where a near
   * match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
   * LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
   * is injectable via `opts` so the core stays unit-testable.
   */

  const fs = require('fs');
  const path = require('path');
  const parsers = __require('./src/verify/parsers');
  const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
  const { buildLibraryIndex } = __require('./src/verify/lib-index');

  // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
  // a tests/__tests__ directory). Used to flag fake-test-file separately.
  const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
  function isTestPath(p) { return TEST_PATH_RE.test(p); }

  const NODE_BUILTINS = new Set([
    'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
    'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
    'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
    'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
    'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
  ]);

  const PY_BUILTINS = new Set([
    'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
    'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
    'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
    'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
  ]);

  const LANG_GLOBALS = new Set([
    // JS
    'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
    'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
    'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
    'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
    'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
    // Python
    'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
    'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
    'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
    'setattr', 'hasattr',
  ]);

  const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
  const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];

  // Obvious documentation-placeholder imports the model writes in illustrative
  // snippets — not real dependency claims. e.g. @scope/utils, some-module, ./local-file.
  const PLACEHOLDER_IMPORT_RE = new RegExp([
    '^@(?:scope|org|your-org|my-org|company|example)(?:/|$)', // @scope/utils
    '(?:^|/)(?:some|your|my)-(?:module|package|lib|component|file|dep)(?:$|/)', // some-module
    '(?:^|/)(?:local-file|your-file|my-file|module-name|package-name|your-package|example-package)(?:$|/)',
    '(?:^|/)path/to/', // ./path/to/x
  ].join('|'), 'i');

  /**
   * Build the set of known symbol identifiers from the SigMap signature index,
   * plus `{ name, file, line }` candidates (for closest-match suggestions).
   */
  function buildSymbolSet(cwd) {
    const set = new Set();
    let fileKeys = [];
    let symbolCandidates = [];
    try {
      const { buildSigIndex } = __require('./src/retrieval/ranker');
      const idx = buildSigIndex(cwd);
      fileKeys = [...idx.keys()];
      for (const sigs of idx.values()) {
        for (const sig of sigs) {
          const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
          const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
          for (const id of ids) set.add(id);
        }
      }
      symbolCandidates = buildSymbolCandidates(idx);
    } catch (_) {}
    return { set, fileKeys, symbolCandidates };
  }

  /** Load declared dependency names from package.json. */
  function loadDeps(cwd) {
    const deps = new Set();
    let hasPkg = false;
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      hasPkg = true;
      for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
        if (pkg[k] && typeof pkg[k] === 'object') {
          for (const name of Object.keys(pkg[k])) deps.add(name);
        }
      }
    } catch (_) {}
    return { deps, hasPkg };
  }

  /** Load the set of npm script names declared in package.json. */
  function loadScripts(cwd) {
    const scripts = new Set();
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      if (pkg.scripts && typeof pkg.scripts === 'object') {
        for (const name of Object.keys(pkg.scripts)) scripts.add(name);
      }
    } catch (_) {}
    return scripts;
  }

  /** Default file-existence check: resolve a referenced path against cwd. */
  function defaultFileExists(cwd, ref) {
    const clean = ref.replace(/^\.\//, '');
    for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
      try {
        if (fs.existsSync(c)) return true;
      } catch (_) {}
    }
    return false;
  }

  /** Default relative-import resolver: fs candidates + basename match in index. */
  function defaultRelativeResolvable(cwd, mod, fileBasenames) {
    const base = path.resolve(cwd, mod);
    for (const e of REL_EXTS) {
      try {
        if (fs.existsSync(base + e)) return true;
      } catch (_) {}
    }
    for (const idx of REL_INDEX) {
      try {
        if (fs.existsSync(path.join(base, idx))) return true;
      } catch (_) {}
    }
    // Fall back to basename match against the indexed file set (the answer's
    // import is relative to a file we cannot know, so a name match is enough
    // to avoid false positives).
    const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
    return fileBasenames.has(wantBase);
  }

  /**
   * Verify an AI answer against the repository.
   *
   * Each issue has the shape:
   *   { type, value, line, location, message, confidence, suggestion }
   * where `confidence` is the *detection* certainty ('high' for path/dep/script
   * checks, 'medium' for symbol checks) and `suggestion` is a heuristic
   * closest-match hint (or null).
   *
   * @param {string} answerText
   * @param {string} cwd
   * @param {object} [opts]
   * @param {Set<string>} [opts.symbolSet]      override known symbols
   * @param {Array}       [opts.symbolCandidates] override { name, file, line } list
   * @param {Array<string>} [opts.fileCandidates]  override repo file paths (suggestions)
   * @param {Set<string>} [opts.deps]           override package deps
   * @param {Set<string>} [opts.scripts]        override package.json script names
   * @param {boolean}     [opts.hasPkg]         whether a package.json exists
   * @param {(ref: string) => boolean} [opts.fileExists]          override file check
   * @param {(mod: string) => boolean} [opts.relativeResolvable]  override rel-import check
   * @returns {{ issues: object[], summary: object }}
   */
  function verify(answerText, cwd, opts = {}) {
    let symbolSet = opts.symbolSet;
    let fileBasenames = opts.fileBasenames;
    let symbolCandidates = opts.symbolCandidates || [];
    let fileCandidates = opts.fileCandidates || [];
    if (!symbolSet) {
      const built = buildSymbolSet(cwd);
      symbolSet = built.set;
      fileBasenames = new Set(built.fileKeys.map(
        (k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
      ));
      symbolCandidates = built.symbolCandidates;
      fileCandidates = built.fileKeys;
    }
    if (!fileBasenames) fileBasenames = new Set();

    // Installed-library grounding (G5/D5, the moat): union the exported symbols of
    // the libraries actually installed in node_modules, so genuine library calls
    // stop false-flagging as fake-symbol and the summary can pin the versions the
    // answer was verified against. Auto-runs only when the caller did not override
    // the symbol set (keeps hermetic callers unchanged); disable with libIndex:false.
    let libraries = opts.libraries || [];
    {
      let libSyms = opts.libSymbols;
      if (!libSyms && opts.libIndex !== false && !opts.symbolSet) {
        try {
          const li = buildLibraryIndex(cwd, { version: opts.version });
          libSyms = li.symbols;
          if (!opts.libraries) libraries = li.libraries;
        } catch (_) { libSyms = null; }
      }
      if (libSyms && libSyms.size) {
        const merged = new Set(symbolSet);
        for (const s of libSyms) merged.add(s);
        symbolSet = merged;
      }
    }

    let deps = opts.deps;
    let hasPkg = opts.hasPkg;
    if (!deps) {
      const loaded = loadDeps(cwd);
      deps = loaded.deps;
      if (hasPkg === undefined) hasPkg = loaded.hasPkg;
    }
    const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());

    const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
    const relativeResolvable = opts.relativeResolvable
      || ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));

    // Pre-derive basename candidates for file suggestions (compare on basename so
    // a wrong directory still surfaces the right file).
    const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));

    const issues = [];
    const dedupe = new Set();
    const add = (issue) => {
      const key = `${issue.type}::${issue.value}`;
      if (dedupe.has(key)) return;
      dedupe.add(key);
      if (!('suggestion' in issue)) issue.suggestion = null;
      issue.location = `L${issue.line}`;
      issues.push(issue);
    };

    // 1. fake-file / fake-test-file
    for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
      if (fileExists(p)) continue;
      const isTest = isTestPath(p);
      const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
      add({
        type: isTest ? 'fake-test-file' : 'fake-file',
        value: p,
        line,
        message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
        confidence: 'high',
        suggestion: match ? formatSuggestion(match, false) : null,
      });
    }

    // 2. fake-import
    for (const imp of parsers.extractImports(answerText)) {
      if (PLACEHOLDER_IMPORT_RE.test(imp.module)) continue;
      if (imp.relative) {
        if (!relativeResolvable(imp.module)) {
          add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
        }
        continue;
      }
      // Bare module — only verifiable for JS when a package.json exists.
      const top = imp.module.split('/')[0];
      if (imp.kind === 'js') {
        if (!hasPkg) continue;
        if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
        if (top.startsWith('@')) {
          const scoped = imp.module.split('/').slice(0, 2).join('/');
          if (deps.has(scoped) || deps.has(imp.module)) continue;
        } else if (deps.has(top) || deps.has(imp.module)) {
          continue;
        }
        const match = closestMatch(top, [...deps], { minLen: 3 });
        add({
          type: 'fake-import',
          value: imp.module,
          line: imp.line,
          message: `Package not in dependencies: ${imp.module}`,
          confidence: 'high',
          suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
        });
      }
      // Python bare imports: stdlib is unbounded offline — skip to keep precision.
    }

    // 3. fake-symbol
    if (symbolSet.size > 0) {
      for (const { name, line } of parsers.extractSymbols(answerText)) {
        if (symbolSet.has(name)) continue;
        if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
        const match = closestMatch(name, symbolCandidates, { minLen: 4 });
        add({
          type: 'fake-symbol',
          value: name,
          line,
          message: `Symbol not found in repo index: ${name}()`,
          confidence: 'medium',
          suggestion: match ? formatSuggestion(match, true) : null,
        });
      }
    }

    // 4. fake-npm-script
    if (hasPkg && scripts.size > 0) {
      for (const { name, line } of parsers.extractNpmScripts(answerText)) {
        if (scripts.has(name)) continue;
        const match = closestMatch(name, [...scripts], { minLen: 2 });
        add({
          type: 'fake-npm-script',
          value: name,
          line,
          message: `npm script not in package.json: ${name}`,
          confidence: 'high',
          suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
        });
      }
    }

    issues.sort((a, b) => a.line - b.line);

    const byType = {
      'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
      'fake-symbol': 0, 'fake-npm-script': 0,
    };
    for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;

    const summary = {
      total: issues.length,
      byType,
      clean: issues.length === 0,
      symbolsIndexed: symbolSet.size,
      withSuggestion: issues.filter((i) => i.suggestion).length,
      librariesIndexed: libraries.length,
      libraries: libraries.map((l) => ({ name: l.name, version: l.version, symbols: l.symbols, typed: l.typed })),
    };

    return { issues, summary };
  }

  module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
  
};

// ── ./src/verify/lib-index ──
__factories["./src/verify/lib-index"] = function(module, exports) {
  
  /**
   * Local-library signature index (v9.0 G5/D5 — the private-API grounding moat).
   *
   * Context7 knows only *public* library docs. SigMap can do something no
   * competitor can: index the signatures of the libraries **actually installed**
   * in `node_modules` and verify AI suggestions against repo + private +
   * installed-lib symbols. This module builds the installed-lib half.
   *
   * Two ecosystems, one index:
   *   - **JS/TS** — each **direct** dependency in `package.json` resolved under
   *     `node_modules/<dep>`; exports read from its TypeScript declaration entry
   *     (`types`/`typings`, else `index.d.ts`).
   *   - **Python** — each direct dependency in `requirements.txt`/`pyproject.toml`
   *     resolved in the project's venv `site-packages`; exports read from the
   *     package's `__init__.py`/`.pyi`. No Python runtime is spawned (North-Star #1).
   *
   * Pure, zero-dependency, deterministic: byte-stable given a fixed installed
   * tree. Bounded (per-file read cap + dep cap) and cached via
   * `src/cache/sig-cache.js` so repeat builds are near-free.
   */

  const fs = require('fs');
  const path = require('path');
  const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = __require('./src/cache/sig-cache');

  const MAX_DTS_BYTES = 512 * 1024; // per-file read cap
  const MAX_DEPS = 1000;            // dep count cap
  const DEP_KEYS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
  const VENV_DIRS = ['.venv', 'venv', 'env', '.env'];

  /**
   * Extract exported symbol names from a `.d.ts` declaration file. Deterministic,
   * regex-based (declaration files are already normalized, so this is robust
   * without a full TS parser and stays zero-dependency).
   * @param {string} src
   * @returns {string[]} sorted unique exported names
   */
  function extractDtsExports(src) {
    const names = new Set();
    if (!src) return [];

    // export [declare] [default] function|const|let|var|class|interface|type|enum|namespace Name
    const declRe = /\bexport\s+(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|const|let|var|class|interface|type|enum|namespace|module)\s+([A-Za-z_$][\w$]*)/g;
    let m;
    while ((m = declRe.exec(src)) !== null) names.add(m[1]);

    // export { a, b as c, default as d }
    const listRe = /\bexport\s*(?:type\s*)?\{([^}]*)\}/g;
    while ((m = listRe.exec(src)) !== null) {
      for (const part of m[1].split(',')) {
        const name = part.trim().split(/\s+as\s+/).pop().trim();
        if (/^[A-Za-z_$][\w$]*$/.test(name) && name !== 'default') names.add(name);
      }
    }

    // export as namespace Name  /  export = Name
    const nsRe = /\bexport\s+as\s+namespace\s+([A-Za-z_$][\w$]*)/g;
    while ((m = nsRe.exec(src)) !== null) names.add(m[1]);
    const assignRe = /\bexport\s*=\s*([A-Za-z_$][\w$]*)/g;
    while ((m = assignRe.exec(src)) !== null) names.add(m[1]);

    return [...names].sort();
  }

  /** Read direct dependency names declared in the project's package.json. */
  function directDeps(cwd) {
    const names = new Set();
    try {
      const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
      for (const k of DEP_KEYS) {
        if (pkg[k] && typeof pkg[k] === 'object') {
          for (const n of Object.keys(pkg[k])) names.add(n);
        }
      }
    } catch (_) { /* no/invalid package.json → no deps */ }
    return [...names].sort();
  }

  /**
   * Resolve an installed dependency's version + entry `.d.ts` path.
   * @returns {{ version: string|null, dtsPath: string|null }|null} null if not installed
   */
  function resolveEntry(cwd, dep) {
    const pkgDir = path.join(cwd, 'node_modules', dep);
    let pkg;
    try { pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')); } catch (_) { return null; }
    const version = typeof pkg.version === 'string' ? pkg.version : null;

    const candidates = [];
    const typesField = pkg.types || pkg.typings;
    if (typeof typesField === 'string') {
      candidates.push(typesField);
      candidates.push(path.join(typesField, 'index.d.ts')); // typesField may be a dir
    }
    candidates.push('index.d.ts');
    if (typeof pkg.main === 'string') candidates.push(pkg.main.replace(/\.(js|cjs|mjs)$/, '.d.ts'));

    for (const c of candidates) {
      const p = path.join(pkgDir, c);
      try { if (fs.statSync(p).isFile()) return { version, dtsPath: p }; } catch (_) { /* next */ }
    }
    return { version, dtsPath: null }; // installed but untyped
  }

  // ── Python ──────────────────────────────────────────────────────────────────

  /**
   * Extract exported symbol names from a Python module's `__init__.py`/`.pyi`.
   * Deterministic, regex-based, top-level only: `__all__`, `def`/`class`, public
   * module-level assignments, and `from … import …` re-exports (a package's
   * public API is largely re-exports). Private names (leading `_`) are skipped
   * unless listed in `__all__`.
   * @param {string} src
   * @returns {string[]} sorted unique exported names
   */
  function extractPyExports(src) {
    const names = new Set();
    if (!src) return [];

    // __all__ = [ 'a', 'b', ... ]  (authoritative when present; keeps privates)
    const allMatch = src.match(/^__all__\s*[:+]?=\s*[\[(]([\s\S]*?)[\])]/m);
    if (allMatch) {
      for (const m of allMatch[1].matchAll(/['"]([A-Za-z_]\w*)['"]/g)) names.add(m[1]);
    }

    // top-level def / class (column 0)
    for (const m of src.matchAll(/^(?:async\s+)?def\s+([A-Za-z_]\w*)/gm)) if (!m[1].startsWith('_')) names.add(m[1]);
    for (const m of src.matchAll(/^class\s+([A-Za-z_]\w*)/gm)) if (!m[1].startsWith('_')) names.add(m[1]);

    // top-level public assignments: NAME = …  /  NAME: type = …  (not ==, +=, etc.)
    for (const m of src.matchAll(/^([A-Za-z_]\w*)\s*(?::[^=\n]+)?=(?!=)/gm)) {
      if (!m[1].startsWith('_')) names.add(m[1]);
    }

    // re-exports: from .mod import Name, Other as Alias
    for (const m of src.matchAll(/^from\s+[^\n]+?\s+import\s+([^\n#]+)/gm)) {
      for (const part of m[1].split(',')) {
        const name = part.trim().replace(/[()]/g, '').split(/\s+as\s+/).pop().trim();
        if (/^[A-Za-z_]\w*$/.test(name) && !name.startsWith('_')) names.add(name);
      }
    }

    return [...names].sort();
  }

  /** Read direct Python dependency names from requirements.txt + pyproject.toml. */
  function pythonDirectDeps(cwd) {
    const names = new Set();
    try {
      const req = fs.readFileSync(path.join(cwd, 'requirements.txt'), 'utf8');
      for (const line of req.split('\n')) {
        const t = line.trim();
        if (!t || t.startsWith('#') || t.startsWith('-')) continue;
        const m = t.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
        if (m) names.add(m[1]);
      }
    } catch (_) { /* none */ }
    try {
      const py = fs.readFileSync(path.join(cwd, 'pyproject.toml'), 'utf8');
      // PEP 621: [project] dependencies = ["foo>=1", "bar"]
      const projDeps = py.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
      if (projDeps) for (const m of projDeps[1].matchAll(/['"]([A-Za-z0-9][A-Za-z0-9._-]*)/g)) names.add(m[1]);
      // Poetry: [tool.poetry.dependencies]\n foo = "^1"
      const poetry = py.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?:\n\[|$)/);
      if (poetry) for (const m of poetry[1].matchAll(/^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/gm)) {
        if (m[1] !== 'python') names.add(m[1]);
      }
    } catch (_) { /* none */ }
    return [...names].sort();
  }

  /** Locate the project's venv `site-packages` directories (no Python runtime). */
  function findSitePackages(cwd) {
    const out = [];
    for (const v of VENV_DIRS) {
      const base = path.join(cwd, v);
      const libDir = path.join(base, 'lib'); // POSIX: <venv>/lib/pythonX.Y/site-packages
      let pyDirs = [];
      try { pyDirs = fs.readdirSync(libDir).filter((d) => /^python\d/.test(d)).sort(); } catch (_) { /* none */ }
      for (const py of pyDirs) {
        const sp = path.join(libDir, py, 'site-packages');
        try { if (fs.statSync(sp).isDirectory()) out.push(sp); } catch (_) { /* next */ }
      }
      const winSp = path.join(base, 'Lib', 'site-packages'); // Windows
      try { if (fs.statSync(winSp).isDirectory()) out.push(winSp); } catch (_) { /* next */ }
    }
    return out;
  }

  /** PEP 503 name normalization (case-insensitive, `-`/`_`/`.` collapsed). */
  function normalizePy(name) {
    return String(name).toLowerCase().replace(/[-_.]+/g, '-');
  }

  /** Find an installed distribution's version from its `*.dist-info`/`*.egg-info`. */
  function findPyVersion(sitePkgsDir, dep) {
    const norm = normalizePy(dep);
    let entries;
    try { entries = fs.readdirSync(sitePkgsDir); } catch (_) { return null; }
    for (const e of entries.sort()) {
      const m = e.match(/^(.+?)-(\d[^-]*)\.(?:dist-info|egg-info)$/);
      if (m && normalizePy(m[1]) === norm) return m[2];
    }
    return null;
  }

  /**
   * Resolve a Python dependency to its installed module entry file + version.
   * @returns {{ version: string|null, sourcePath: string|null }|null} null if not installed
   */
  function resolvePyEntry(sitePkgsDirs, dep) {
    const candidates = [...new Set([dep, dep.replace(/-/g, '_'), dep.toLowerCase(), dep.toLowerCase().replace(/-/g, '_')])];
    for (const sp of sitePkgsDirs) {
      const version = findPyVersion(sp, dep);
      for (const cand of candidates) {
        for (const entry of ['__init__.pyi', '__init__.py']) { // package
          const p = path.join(sp, cand, entry);
          try { if (fs.statSync(p).isFile()) return { version, sourcePath: p }; } catch (_) { /* next */ }
        }
        for (const ext of ['.pyi', '.py']) { // single-module
          const p = path.join(sp, cand + ext);
          try { if (fs.statSync(p).isFile()) return { version, sourcePath: p }; } catch (_) { /* next */ }
        }
      }
    }
    return null;
  }

  /**
   * Build the installed-library signature index for `cwd`.
   *
   * @param {string} cwd
   * @param {object} [opts]
   * @param {string} [opts.version='0']  sigmap version, for cache busting
   * @param {boolean} [opts.cache=true]  use the on-disk sig-cache
   * @returns {{ symbols: Set<string>, libraries: Array<{name,version,symbols,typed}>, count: number }}
   */
  function buildLibraryIndex(cwd, opts = {}) {
    const version = opts.version || '0';
    const useCache = opts.cache !== false;

    // Collect entries from both ecosystems; each carries its extractor kind.
    const entries = []; // { name, version, sourcePath, kind: 'dts'|'py' }
    for (const dep of directDeps(cwd).slice(0, MAX_DEPS)) {
      const r = resolveEntry(cwd, dep);
      if (r) entries.push({ name: dep, version: r.version, sourcePath: r.dtsPath, kind: 'dts' });
    }
    const sitePkgs = findSitePackages(cwd);
    if (sitePkgs.length) {
      for (const dep of pythonDirectDeps(cwd).slice(0, MAX_DEPS)) {
        const r = resolvePyEntry(sitePkgs, dep);
        if (r) entries.push({ name: dep, version: r.version, sourcePath: r.sourcePath, kind: 'py' });
      }
    }

    const cache = useCache ? loadCache(cwd, version) : new Map();
    const files = entries.filter((e) => e.sourcePath).map((e) => e.sourcePath);
    const { unchanged } = getChangedFiles(files, cache);
    const unchangedSet = new Set(unchanged);

    const symbols = new Set();
    const libraries = [];
    const fresh = [];

    for (const e of entries) {
      let names;
      if (!e.sourcePath) {
        names = [];
      } else if (unchangedSet.has(e.sourcePath) && cache.get(e.sourcePath)) {
        names = cache.get(e.sourcePath).sigs || [];
      } else {
        let src = '';
        try {
          if (fs.statSync(e.sourcePath).size <= MAX_DTS_BYTES) src = fs.readFileSync(e.sourcePath, 'utf8');
        } catch (_) { /* unreadable → empty */ }
        names = e.kind === 'py' ? extractPyExports(src) : extractDtsExports(src);
        fresh.push({ file: e.sourcePath, sigs: names });
      }
      for (const n of names) symbols.add(n);
      libraries.push({ name: e.name, version: e.version, symbols: names.length, typed: !!e.sourcePath });
    }

    if (useCache && fresh.length) {
      updateCacheEntries(cache, fresh);
      saveCache(cwd, version, cache);
    }

    libraries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
    return { symbols, libraries, count: symbols.size };
  }

  /** D8: render `name@version` pins for the typed/installed libraries. */
  function formatVersionPins(libraries) {
    return (libraries || [])
      .filter((l) => l.version)
      .map((l) => `${l.name}@${l.version}`);
  }

  /**
   * D8: collect `name@version` pins for direct dependencies — versions only, no
   * symbol extraction, no cache. Cheap enough to run on every context build so
   * the generated header can ground against what is actually installed (JS +
   * Python). Deterministic: sorted, then capped.
   * @param {string} cwd
   * @param {object} [opts]
   * @param {number} [opts.limit=40] max pins returned (after sort)
   * @returns {{ pins: string[], total: number }} pins capped to limit; total = all resolved
   */
  function collectVersionPins(cwd, opts = {}) {
    const limit = Number.isInteger(opts.limit) && opts.limit >= 0 ? opts.limit : 40;
    const pins = [];
    for (const dep of directDeps(cwd).slice(0, MAX_DEPS)) {
      const r = resolveEntry(cwd, dep);
      if (r && r.version) pins.push(`${dep}@${r.version}`);
    }
    const sitePkgs = findSitePackages(cwd);
    if (sitePkgs.length) {
      for (const dep of pythonDirectDeps(cwd).slice(0, MAX_DEPS)) {
        const r = resolvePyEntry(sitePkgs, dep);
        if (r && r.version) pins.push(`${dep}@${r.version}`);
      }
    }
    pins.sort();
    return { pins: limit ? pins.slice(0, limit) : pins, total: pins.length };
  }

  module.exports = {
    buildLibraryIndex, extractDtsExports, directDeps, resolveEntry, formatVersionPins,
    collectVersionPins,
    extractPyExports, pythonDirectDeps, findSitePackages, resolvePyEntry,
  };
  
};

// ── ./src/verify/parsers ──
__factories["./src/verify/parsers"] = function(module, exports) {
  
  /**
   * Parsers for the Hallucination Guard (verify-ai-output).
   *
   * Extract the verifiable claims an AI answer makes about a codebase:
   *   - file paths it references
   *   - import / require statements it shows
   *   - function / class symbols it calls
   *   - fenced code blocks (so callers can scope checks to code vs prose)
   *
   * Everything here is deterministic and offline — pure string analysis.
   */

  // Extensions we are confident name a source/code/config file (no slash required).
  const KNOWN_CODE_EXT = new Set([
    'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
    'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
    'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
    'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
    'gd', 'gdscript',
  ]);

  // Well-known "X.js" runtime/library product names — never repo files.
  const LIBRARY_TOKENS = new Set([
    'node.js', 'next.js', 'nuxt.js', 'vue.js', 'react.js', 'express.js', 'koa.js',
    'nest.js', 'three.js', 'd3.js', 'chart.js', 'ember.js', 'backbone.js',
    'angular.js', 'meteor.js', 'moment.js', 'anime.js', 'p5.js', 'next.config.js',
  ]);

  // Illustrative placeholder names the model writes in prose, not repo claims:
  // e.g. example.js, minimal-example.js, sample.ts, demo.js, placeholder.js.
  const PLACEHOLDER_RE = /(?:^|[-_.])(?:example|sample|demo|placeholder)(?:[-_.]|s?$)/i;
  // camelCase / Pascal placeholders: myExample.js, exampleConfig.js, fooSample.ts.
  // Requires a case boundary so ordinary words (resample.js) are NOT suppressed.
  const PLACEHOLDER_CAMEL_RE = /(?:^|[a-z])(?:Example|Sample|Demo|Placeholder)|(?:^|[-_.])(?:example|sample|demo|placeholder)(?=[A-Z])/;

  /**
   * Extract fenced code blocks.
   * @param {string} text
   * @returns {{ lang: string, content: string, line: number }[]}
   */
  function extractCodeBlocks(text) {
    const blocks = [];
    const lines = text.split('\n');
    let inBlock = false;
    let lang = '';
    let buf = [];
    let startLine = 0;
    for (let i = 0; i < lines.length; i++) {
      const m = lines[i].match(/^```(\w*)/);
      if (m) {
        if (!inBlock) {
          inBlock = true;
          lang = m[1] || '';
          buf = [];
          startLine = i + 2; // first content line (1-based)
        } else {
          blocks.push({ lang, content: buf.join('\n'), line: startLine });
          inBlock = false;
        }
        continue;
      }
      if (inBlock) buf.push(lines[i]);
    }
    return blocks;
  }

  /**
   * Extract file-path references (deduped, first-seen line kept).
   * A token counts as a path when it has a `.<letter…>` extension AND
   * either contains a `/` or carries a known code/config extension.
   * @param {string} text
   * @returns {{ path: string, line: number }[]}
   */
  function extractFilePaths(text) {
    const lines = text.split('\n');
    const seen = new Map();
    const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      let m;
      re.lastIndex = 0;
      while ((m = re.exec(line)) !== null) {
        const p = m[1];
        if (/^https?:/i.test(p)) continue;
        const ext = (p.split('.').pop() || '').toLowerCase();
        const hasSlash = p.includes('/');
        if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
        if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
        const base = p.split('/').pop();
        if (PLACEHOLDER_RE.test(base) || PLACEHOLDER_CAMEL_RE.test(base)) continue;
        if (!seen.has(p)) seen.set(p, i + 1);
      }
    }
    return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
  }

  /**
   * Extract import / require statements.
   * @param {string} text
   * @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
   */
  function extractImports(text) {
    const lines = text.split('\n');
    const out = [];
    const push = (module, kind, line, raw) => {
      if (!module) return;
      out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
    };
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      let m;
      // JS/TS: import ... from 'x'  |  export ... from 'x'
      if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
        push(m[1], 'js', i + 1, line);
      } else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
        // side-effect import 'x'
        push(m[1], 'js', i + 1, line);
      }
      // require('x') / dynamic import('x') — may co-occur, scan separately
      const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
      let r;
      while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);

      // TS: import X = require('mod')
      if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
        push(m[1], 'js', i + 1, line);
      }

      // Python: from x import y  |  import x
      if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
        push(m[1], 'py', i + 1, line);
      } else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
        push(m[1], 'py', i + 1, line);
      }
    }

    // Multi-line JS/TS imports, e.g.
    //   import {
    //     A as B,
    //   } from './mod';
    // The per-line pass above misses these because `from '…'` sits on a later
    // line. Trigger only when the opening line has no quote and no `from` yet,
    // then gather forward until the source string appears.
    for (let i = 0; i < lines.length; i++) {
      const start = lines[i];
      if (!/^\s*(?:import|export)\b/.test(start)) continue;
      if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
      let joined = start;
      for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
        joined += ' ' + lines[j];
        const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
        if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
        if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
      }
    }
    return out;
  }

  /**
   * Extract npm/pnpm/yarn script invocations (`npm run <name>`).
   * Only the explicit `run` form is matched, to avoid confusing package-manager
   * subcommands (`yarn add`, `pnpm install`) with script names.
   * @param {string} text
   * @returns {{ name: string, line: number }[]}
   */
  function extractNpmScripts(text) {
    const lines = text.split('\n');
    const out = [];
    const seen = new Set();
    const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
    for (let i = 0; i < lines.length; i++) {
      let m;
      re.lastIndex = 0;
      while ((m = re.exec(lines[i])) !== null) {
        const name = m[1];
        if (seen.has(name)) continue;
        seen.add(name);
        out.push({ name, line: i + 1 });
      }
    }
    return out;
  }

  /**
   * Extract function/class symbol references that look like calls.
   * Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
   * @param {string} text
   * @returns {{ name: string, line: number }[]}
   */
  function extractSymbols(text) {
    const lines = text.split('\n');
    const out = [];
    const seen = new Set();
    const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
    for (let i = 0; i < lines.length; i++) {
      let m;
      re.lastIndex = 0;
      while ((m = re.exec(lines[i])) !== null) {
        const name = m[1];
        const key = name + '@' + (i + 1);
        if (seen.has(key)) continue;
        seen.add(key);
        out.push({ name, line: i + 1 });
      }
    }
    return out;
  }

  module.exports = {
    extractCodeBlocks,
    extractFilePaths,
    extractImports,
    extractSymbols,
    extractNpmScripts,
  };
  
};

// ── ./src/workspace/detector ──
__factories["./src/workspace/detector"] = function(module, exports) {
  
  const fs   = require('fs');
  const path = require('path');
  module.exports = { detectWorkspaces, inferPackage, scopeToPackage };

  function detectWorkspaces(cwd) {
    const pkgPath = path.join(cwd, 'package.json');
    if (!fs.existsSync(pkgPath)) return [];

    let pkg;
    try {
      pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
    } catch {
      return [];
    }

    const patterns = pkg.workspaces || [];
    const dirs = [];

    // Handle both flat array and object with packages field (Yarn v2 format)
    const patternArray = Array.isArray(patterns) ? patterns : (patterns.packages || []);

    for (const p of patternArray) {
      const base = p.replace(/\/\*\*?$/, '');
      const resolved = path.join(cwd, base);
      if (fs.existsSync(resolved)) {
        try {
          for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
            if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
          }
        } catch (_) {}
      }
    }

    return dirs;
  }

  // Infer package from query tokens: "add rate limiting to payments" → "packages/payments"
  function inferPackage(query, workspaceDirs, cwd) {
    const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);

    // Find longest matching package name
    let bestMatch = null;
    let bestLen = 0;
    let bestMatchLen = 0;

    for (const dir of workspaceDirs) {
      const name = path.basename(dir).toLowerCase();
      for (const token of tokens) {
        const matchLen = _getMatchLength(name, token);
        // Only consider matches; use longest match, and break ties by longest package name
        if (matchLen > 0 && (matchLen > bestLen || (matchLen === bestLen && name.length > bestMatchLen))) {
          bestMatch = dir;
          bestLen = matchLen;
          bestMatchLen = name.length;
        }
      }
    }

    return bestMatch;
  }

  function _getMatchLength(name, token) {
    if (name === token) return 1000 + name.length;  // Exact match is best
    if (name.startsWith(token) && token.length >= 3) return 100 + token.length;
    if (token.startsWith(name) && name.length >= 3) return name.length;
    return 0;
  }

  // Return boost multiplier for files inside the inferred package
  function scopeToPackage(filePath, packageDir) {
    const normalized = filePath.replace(/\\/g, '/');
    const normalizedPkg = packageDir.replace(/\\/g, '/');

    // Ensure we match the directory boundary, not just a prefix
    // e.g., packages/payment should not match packages/payment-old
    if (normalized.startsWith(normalizedPkg)) {
      const afterPrefix = normalized.slice(normalizedPkg.length);
      // Check if next char is / or if it's the exact match
      if (afterPrefix === '' || afterPrefix[0] === '/') {
        return 0.30;
      }
    }
    return 0;
  }
  
};

// ═══ END SIGMAP BUNDLED MODULES ═══

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

// Shell-free subprocess helpers. execFileSync runs the binary directly (never
// /bin/sh), so there is no shell-injection surface and no "Shell access"
// capability for supply-chain scanners. stderr is discarded by default.
function __git(args, opts = {}) {
  const { execFileSync } = require('child_process');
  return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
}
function __tryGit(args, opts = {}) {
  try { return __git(args, opts).toString().trim(); }
  catch (_) { return ''; }
}

const VERSION = '8.7.1';
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';

function requireSourceOrBundled(key) {
  try {
    const rel = key.replace(/^\.\//, '') + '.js';
    const abs = path.join(__dirname, rel);
    if (fs.existsSync(abs)) return require(abs);
  } catch (_) {}
  return __require(key);
}

// ---------------------------------------------------------------------------
// Config — delegate to src/config/loader.js
// ---------------------------------------------------------------------------
const { loadConfig } = requireSourceOrBundled('./src/config/loader');
const { DEFAULTS } = requireSourceOrBundled('./src/config/defaults');

// ---------------------------------------------------------------------------
// Language → extractor mapping (by file extension)
// ---------------------------------------------------------------------------
const EXT_MAP = {
  '.ts': 'typescript', '.tsx': 'typescript_react',
  '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
  '.py': 'python', '.pyw': 'python',
  '.java': 'java',
  '.kt': 'kotlin', '.kts': 'kotlin',
  '.go': 'go',
  '.rs': 'rust',
  '.cs': 'csharp',
  '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
  '.rb': 'ruby', '.rake': 'ruby',
  '.php': 'php',
  '.swift': 'swift',
  '.dart': 'dart',
  '.scala': 'scala', '.sc': 'scala',
  '.r': 'r', '.R': 'r',
  '.vue': 'vue_sfc',
  '.svelte': 'svelte',
  '.html': 'html', '.htm': 'html',
  '.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
  '.yml': 'yaml', '.yaml': 'yaml',
  '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
  // P1 languages
  '.sql': 'sql',
  '.graphql': 'graphql', '.gql': 'graphql',
  '.tf': 'terraform', '.tfvars': 'terraform',
  '.proto': 'protobuf',
  // Phase A formats
  '.toml': 'toml',
  '.properties': 'properties',
  '.xml': 'xml',
  '.md': 'markdown',
};

// Dockerfile handled separately (no extension)
function isDockerfile(filename) {
  return filename === 'Dockerfile' || filename.startsWith('Dockerfile.');
}

// ---------------------------------------------------------------------------
// .contextignore parser (gitignore-style subset)
// ---------------------------------------------------------------------------
function loadIgnorePatterns(cwd) {
  const patterns = [];
  for (const name of ['.contextignore', '.repomixignore']) {
    const p = path.join(cwd, name);
    if (fs.existsSync(p)) {
      const lines = fs.readFileSync(p, 'utf8').split('\n');
      for (const line of lines) {
        const trimmed = line.trim();
        if (trimmed && !trimmed.startsWith('#')) patterns.push(trimmed);
      }
    }
  }
  return patterns;
}

function matchesIgnore(relPath, patterns) {
  for (const pat of patterns) {
    const normalized = pat.replace(/\\/g, '/');
    // Strip trailing slash (gitignore style — directory patterns)
    const patternToUse = normalized.endsWith('/')
      ? normalized.slice(0, -1)
      : normalized;
    // Escape regex special chars but NOT brackets (keep them for character classes)
    const regexStr = patternToUse
      .replace(/[.+^${}()|\\]/g, '\\$&')
      .replace(/\*\*/g, '___DOUBLE___')
      .replace(/\*/g, '[^/]*')
      .replace(/___DOUBLE___/g, '.*');
    try {
      const regex = new RegExp(`(^|/)${regexStr}($|/)`);
      if (regex.test(relPath)) return true;
    } catch (_) {
      // Malformed bracket syntax or invalid regex — skip this pattern
    }
  }
  return false;
}

// ---------------------------------------------------------------------------
// File walker
// ---------------------------------------------------------------------------
function walkDir(dir, exclude, maxDepth, depth = 0) {
  if (depth > maxDepth) return [];
  let results = [];
  let entries;
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch (_) {
    return [];
  }
  for (const entry of entries) {
    if (exclude.includes(entry.name)) continue;
    const full = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      results = results.concat(walkDir(full, exclude, maxDepth, depth + 1));
    } else if (entry.isFile()) {
      results.push(full);
    }
  }
  return results;
}

function buildFileList(cwd, config) {
  const files = [];
  for (const srcDir of config.srcDirs) {
    const abs = path.join(cwd, srcDir);
    if (!fs.existsSync(abs)) continue;
    const found = walkDir(abs, config.exclude, config.maxDepth);
    files.push(...found);
  }
  // Deduplicate
  return [...new Set(files)];
}

// ---------------------------------------------------------------------------
// Extractor loader (lazy, cached)
// ---------------------------------------------------------------------------
const _extractorCache = {};
function getExtractor(name) {
  if (_extractorCache[name]) return _extractorCache[name];
  try {
    const mod = requireSourceOrBundled(`./src/extractors/${name}`);
    _extractorCache[name] = mod;
    return mod;
  } catch (err) {
    console.warn(`[sigmap] failed to load extractor ${name}: ${err.message}`);
    return null;
  }
}

function detectAndExtract(filePath, content, maxSigsPerFile) {
  const base = path.basename(filePath);
  const ext = path.extname(base).toLowerCase();
  let extractorName = EXT_MAP[ext] || null;
  if (!extractorName && isDockerfile(base)) extractorName = 'dockerfile';
  // Feature 7: generic fallback — catches Nix, Elixir, Gleam, Zig, Go templates, etc.
  if (!extractorName) extractorName = 'generic';

  const extractor = getExtractor(extractorName);
  if (!extractor) return [];

  try {
    const sigs = extractor.extract(content);
    return Array.isArray(sigs) ? sigs.slice(0, maxSigsPerFile) : [];
  } catch (err) {
    console.warn(`[sigmap] extractor failed for ${filePath}: ${err.message}`);
    return [];
  }
}

function extractFileDeps(filePath, content, config) {
  if (config && config.depMap === false) return [];
  try {
    const { extractPythonDeps, extractTSDeps, extractRDeps } = requireSourceOrBundled('./src/extractors/deps');
    const ext = path.extname(filePath).toLowerCase();
    if (ext === '.py' || ext === '.pyw') return extractPythonDeps(content);
    if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return extractTSDeps(content);
    if (ext === '.r') return extractRDeps ? extractRDeps(content) : [];
  } catch (_) {}
  return [];
}

function extractSignatureName(sig) {
  const s = (sig || '').trim();
  const m = s.match(/(?:def|function|func|constructor|async\s+def|async\s+function)?\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
  return m ? m[1] : '';
}

function annotateCoverage(sigs, testIndex, enabled) {
  if (!enabled || !testIndex || testIndex.size === 0) return sigs;
  const { isTested } = requireSourceOrBundled('./src/extractors/coverage');
  return sigs.map((sig) => {
    const name = extractSignatureName(sig);
    if (!name) return sig;
    // Skip non-callable/type declarations.
    if (/^(class|interface|enum|type|module|trait|struct|record)\b/.test(sig.trim())) return sig;
    return `${sig}  ${isTested(name, testIndex) ? '✓' : '✗'}`;
  });
}

// ---------------------------------------------------------------------------
// Token budget enforcement
// ---------------------------------------------------------------------------
function estimateTokens(str) {
  return Math.ceil(str.length / 4);
}

function isTestFile(filePath) {
  return /\.(test|spec)\.[a-z]+$/.test(filePath) || /_test\.[a-z]+$/.test(filePath);
}

function isConfigFile(filePath) {
  return /\.(config|conf)\.[a-z]+$/.test(filePath) ||
    path.extname(filePath) === '.json';
}

function isGeneratedFile(filePath) {
  return /(\.generated\.|\.pb\.|_pb\.)/.test(filePath);
}

function isMockFile(filePath) {
  const p = filePath.replace(/\\/g, '/');
  return /\/(mock|mocks|stub|stubs|fake|fakes|demo|demos|__mocks__|fixtures)\//i.test(p) ||
    /\.(mock|stub|fake)\.[jt]sx?$/.test(p) ||
    /mock\.(ts|js|tsx|jsx)$/.test(p);
}

/**
 * Compute the effective token budget based on repo size and config.
 *
 * Formula:
 *   totalSigTokens  = sum of estimated tokens for all extracted sig blocks
 *   needed          = ceil(totalSigTokens * coverageTarget)   // tokens for target% coverage
 *   hardCap         = floor(modelContextLimit * maxTokensHeadroom)
 *   effective       = clamp(needed, 4000, hardCap)
 *
 * When autoMaxTokens is false the configured maxTokens is returned unchanged.
 *
 * @param {Array}  fileEntries - All file entries BEFORE budget enforcement
 * @param {object} config
 * @returns {number} effective token budget
 */
function computeEffectiveMaxTokens(fileEntries, config) {
  if (config.autoMaxTokens === false) return config.maxTokens;

  const coverageTarget    = (config.coverageTarget    != null) ? config.coverageTarget    : 0.80;
  const modelContextLimit = (config.modelContextLimit != null) ? config.modelContextLimit : 128000;
  const maxTokensHeadroom = (config.maxTokensHeadroom != null) ? config.maxTokensHeadroom : 0.20;

  const totalSigTokens = fileEntries.reduce(
    (s, e) => s + estimateTokens((e.sigs || []).join('\n')), 0
  );
  if (totalSigTokens === 0) return config.maxTokens;

  const hardCap  = Math.floor(modelContextLimit * maxTokensHeadroom);
  const needed   = Math.ceil(totalSigTokens * coverageTarget);
  const MIN      = 4000;
  const effective = Math.min(Math.max(MIN, needed), hardCap);

  // Warn when repo is so large the hard cap prevents hitting the coverage target
  if (needed > hardCap) {
    const estimatedCovPct = Math.round((hardCap / totalSigTokens) * 100);
    const targetPct       = Math.round(coverageTarget * 100);
    if (estimatedCovPct < targetPct - 10) {
      console.warn(
        `[sigmap] auto-budget: ${fileEntries.length} files need ~${Math.round(needed / 1000)}K tokens ` +
        `for ${targetPct}% coverage`
      );
      console.warn(
        `[sigmap] auto-budget: capped at ${hardCap} ` +
        `(${Math.round(maxTokensHeadroom * 100)}% of ${Math.round(modelContextLimit / 1000)}K model limit) ` +
        `→ est. ${estimatedCovPct}% coverage`
      );
      console.warn(
        `[sigmap] auto-budget: tip — set strategy:"per-module" for full coverage on large repos`
      );
    }
  }

  return effective;
}

function applyTokenBudget(fileEntries, maxTokens) {
  // fileEntries: [{ filePath, sigs, mtime }]
  // Per-file rendered overhead: "### <path>", code fences, trailing blank line.
  // A signature-only budget undercounts this, so when many files survive the
  // section headers alone can blow maxTokens.
  const sectionOverhead = (e) => estimateTokens(String(e.filePath || '')) + 6;
  const renderedTotal = (entries) =>
    entries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')) + sectionOverhead(e), 0);
  // The generated file also carries a ~150-token fixed preamble (SigMap command
  // table + headers). Reserve at least that much so small budgets don't overflow;
  // for large budgets this matches the historical 10% reserve.
  const budgetForEntries = Math.max(1, maxTokens - Math.max(200, Math.ceil(maxTokens * 0.10)));
  let total = renderedTotal(fileEntries);
  if (total <= budgetForEntries) return fileEntries;

  // Over budget — degrade gracefully, best file first:
  //   1. keep FULL signatures (params + return type) while they fit;
  //   2. when a file no longer fits with full sigs, collapse just THAT file to
  //      its line-anchor pointers (still discoverable via buildSigIndex / the
  //      ranker — it parses the context file, so an anchored file stays findable);
  //   3. drop a file only when even the anchor form won't fit.
  // The important, high-priority files therefore keep their real signatures —
  // the user-visible value — while only low-priority overflow degrades to
  // anchors instead of vanishing. (Earlier versions collapsed EVERY file to an
  // anchor the moment you went 1 token over budget, gutting all signatures.)
  const withPriority = fileEntries.map((e) => {
    let priority = 0;
    let dropReason = 'budget: low recency';
    if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
    else if (isMockFile(e.filePath)) { priority = 9;  dropReason = 'budget: mock file'; }
    else if (isTestFile(e.filePath)) { priority = 8;  dropReason = 'budget: test file'; }
    else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
    else priority = 4;
    const loc = e.content ? e.content.split('\n').length : 1;
    const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
    return { ...e, priority, dropReason, signalQuality };
  });

  // Best-first order: lowest drop-priority, then most-recent, then most-informative.
  const bestFirst = withPriority.slice().sort((a, b) => {
    if (a.priority !== b.priority) return a.priority - b.priority;
    if ((b.mtime || 0) !== (a.mtime || 0)) return (b.mtime || 0) - (a.mtime || 0);
    return (b.signalQuality || 0) - (a.signalQuality || 0);
  });

  const entryCost = (e) => estimateTokens(e.sigs.join('\n')) + sectionOverhead(e);
  const collapseEntry = (e) => ({
    ...e,
    sigs: (e.sigs || []).map((s) => {
      const line = toIndexLine(s);
      return line && /:\d+-\d+/.test(line) ? line : s;
    }),
  });
  // Keep FULL signatures for the best files while they fit (signatures are the
  // value the user reads). When a file's full form no longer fits, fall back to
  // its anchor form (still discoverable via the ranker, which parses this file);
  // drop only when even the anchor won't fit. So the important files keep real
  // signatures and only the lowest-priority overflow degrades — instead of every
  // signature being gutted to an anchor the moment you go over budget.
  const finalByPath = new Map();
  const verboseDropped = [];
  let collapsedCount = 0;
  let used = 0;
  for (const entry of bestFirst) { // best first
    if (used + entryCost(entry) <= budgetForEntries) {
      finalByPath.set(entry.filePath, entry); used += entryCost(entry); continue;
    }
    const slim = collapseEntry(entry);
    if (used + entryCost(slim) <= budgetForEntries) {
      finalByPath.set(entry.filePath, slim); used += entryCost(slim); collapsedCount++; continue;
    }
    verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
  }
  // Restore the original file order for stable output.
  const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));

  if (verboseDropped.length > 0 || collapsedCount > 0) {
    const parts = [];
    if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
    if (collapsedCount) parts.push(`collapsed ${collapsedCount} low-priority file(s) to anchors`);
    console.warn(`[sigmap] budget: ${parts.join(', ')} to stay under ${maxTokens} tokens`);
    if (process.argv.includes('--verbose')) {
      for (const { filePath, reason } of verboseDropped) {
        console.warn(`[sigmap] dropped: ${path.relative(process.cwd(), filePath)} — ${reason}`);
      }
      console.warn(`[sigmap] included: ${kept.length} files (${collapsedCount} collapsed), dropped: ${verboseDropped.length}`);
    }
  }
  return kept;
}

// ---------------------------------------------------------------------------
// Recently committed files (git, optional)
// ---------------------------------------------------------------------------
function getRecentlyCommittedFiles(cwd, count) {
  const n = count || 10;
  try {
    const out = __git(['log', '--name-only', '--format=', '-n', String(n)], { cwd });
    return new Set(out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
  } catch (_) {
    return new Set();
  }
}

// ---------------------------------------------------------------------------
// Diff mode: files changed in working tree or staging area
// ---------------------------------------------------------------------------
function getDiffFiles(cwd, stagedOnly) {
  try {
    const gitArgs = stagedOnly ? ['diff', '--cached', '--name-only'] : ['diff', 'HEAD', '--name-only'];
    const out = __git(gitArgs, { cwd });
    return new Set(
      out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f))
    );
  } catch (_) {
    return new Set();
  }
}

function getFilesChangedSinceBase(cwd, baseRef) {
  if (!/^[A-Za-z0-9._/\-~^]+$/.test(baseRef)) {
    console.warn(`[sigmap] --diff: invalid base ref '${baseRef}'`);
    return new Set();
  }
  try {
    const { execFileSync } = require('child_process');
    const out = execFileSync('git', ['diff', `${baseRef}..HEAD`, '--name-only'], {
      cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
    });
    return new Set(out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
  } catch (_) {
    return new Set();
  }
}

function buildDiffSectionFromBase(cwd, baseRef, currentEntries, config) {
  const { diffSignatures, extractName } = requireSourceOrBundled('./src/extractors/prdiff');
  const lines = ['## diff (vs ' + baseRef + ')', '```'];

  for (const entry of currentEntries) {
    const rel = path.relative(cwd, entry.filePath);
    let baseSrc = '';
    try {
      const { execFileSync } = require('child_process');
      baseSrc = execFileSync('git', ['show', `${baseRef}:${rel}`], {
        cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
      });
    } catch (_) {
      baseSrc = '';
    }

    const baseSigs = baseSrc ? detectAndExtract(entry.filePath, baseSrc, config.maxSigsPerFile) : [];
    const d = diffSignatures(baseSigs, entry.sigs || []);

    const markers = [];
    for (const a of d.added.slice(0, 4)) {
      const n = extractName(a);
      if (n) markers.push(`+${n}`);
    }
    for (const r of d.removed.slice(0, 4)) {
      const n = extractName(r);
      if (n) markers.push(`-${n}`);
    }
    for (const m of d.modified.slice(0, 4)) markers.push(`~${m}`);
    if (markers.length === 0) continue;
    lines.push(`${rel}`);
    lines.push(`  ${[...new Set(markers)].slice(0, 6).join('  ')}`);
  }

  lines.push('```', '');
  return lines.length > 3 ? lines : [];
}

// ---------------------------------------------------------------------------
// Output formatter
// ---------------------------------------------------------------------------
function buildDepMap(fileEntries, cwd) {
  const lines = [];
  for (const entry of fileEntries) {
    const deps = entry.deps;
    if (!deps || deps.length === 0) continue;
    const rel = path.relative(cwd, entry.filePath);
    lines.push(`${rel} \u2190 ${deps.join(', ')}`);
  }
  return lines;
}

function buildTodoSection(fileEntries, cwd, config) {
  if (config && config.todos === false) return [];
  const { extractTodos } = requireSourceOrBundled('./src/extractors/todos');
  const out = [];
  for (const entry of fileEntries) {
    const todos = extractTodos(entry.content || '');
    if (!todos || todos.length === 0) continue;
    const rel = path.relative(cwd, entry.filePath);
    for (const t of todos) {
      out.push(`${rel}:${t.line}  # ${t.tag}: ${t.text}`);
    }
  }
  return out.slice(0, 20);
}

function buildChangesSection(cwd, config, fileEntries) {
  if (config && config.changes === false) return [];
  const n = (config && config.changesCommits) || 5;
  try {
    let range = `HEAD~${n}..HEAD`;
    try {
      __git(['rev-parse', '--verify', `HEAD~${n}`], { cwd });
    } catch (_) {
      // HEAD~n doesn't exist (shallow repo) — clamp to deepest valid ancestor
      let best = 1;
      for (let k = n - 1; k >= 2; k--) {
        try {
          __git(['rev-parse', '--verify', `HEAD~${k}`], { cwd });
          best = k;
          break;
        } catch (_) {}
      }
      range = `HEAD~${best}..HEAD`;
    }

    const namesOnly = __git(['diff', range, '--name-only'], { cwd });
    const changed = new Set(namesOnly.split('\n').map((l) => l.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
    if (changed.size === 0) return [];

    const timeAgo = __git(['log', '-1', '--format=%cr'], { cwd }).trim();

    const lines = [`## changes (last ${n} commits — ${timeAgo})`, '```'];
    let hasDelta = false;
    for (const entry of fileEntries) {
      if (!changed.has(entry.filePath)) continue;
      const rel = path.relative(cwd, entry.filePath);
      let diff = '';
      try {
        diff = __git(['diff', range, '--', rel], { cwd });
      } catch (_) {
        continue;
      }
      const plus = [...diff.matchAll(/^\+.*(?:def\s+|function\s+|class\s+|func\s+)(\w+)/gm)].map((m) => `+${m[1]}`);
      const modDef = [...diff.matchAll(/^-.*(?:def\s+|function\s+|class\s+|func\s+)(\w+)/gm)].map((m) => `~${m[1]}`);
      // Hunk headers (Python/Go/Java: git shows enclosing func in @@ ... @@ context)
      const modCtx = [...diff.matchAll(/^@@[^@]*@@\s+(?:(?:async|export|public|private|static)\s+)*(?:def|function|class|func)\s+(\w+)/gm)].map((m) => `~${m[1]}`);
      // Context lines (TS/JS: git doesn't detect funcname, so scan the unchanged lines adjacent to hunks)
      const ctxFn = [...diff.matchAll(/^ +(?:export\s+)?(?:async\s+)?(?:default\s+)?(?:function\s+(\w+)|class\s+(\w+))/gm)].map((m) => `~${m[1] || m[2]}`);
      const delta = [...new Set([...plus, ...modDef, ...modCtx, ...ctxFn])].slice(0, 4).join('  ');
      if (delta) {
        hasDelta = true;
        lines.push(`${rel.padEnd(45)} ${delta}`);
      }
    }
    lines.push('```', '');
    return hasDelta ? lines : [];
  } catch (_) {
    return [];
  }
}

function normalizeRelNoExt(rel) {
  return rel.replace(/\.[^.]+$/, '').replace(/\\/g, '/');
}

function resolveImpactRadius(fileEntries, cwd, config) {
  const map = new Map();
  if (!config || !config.impactRadius) return map;
  const rels = fileEntries.map((e) => path.relative(cwd, e.filePath));
  const keys = rels.map((r) => ({ rel: r, norm: normalizeRelNoExt(r), base: path.basename(normalizeRelNoExt(r)) }));

  for (const entry of fileEntries) {
    const importer = path.relative(cwd, entry.filePath);
    const importerTop = importer.split('/')[0];
    for (const dep of (entry.deps || [])) {
      const depNorm = dep.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\.[^.]+$/, '');
      const matches = keys.filter((k) => {
        if (k.norm === depNorm) return true;
        // Path-segment or base-only match: both require same top-level directory
        // to prevent cross-subsystem matches (e.g. 'app' dep in server/ matching desktop/gui/app.py)
        const matchTop = k.rel.split('/')[0];
        if (k.norm.endsWith('/' + depNorm) || k.base === depNorm) return matchTop === importerTop;
        return false;
      });
      for (const m of matches) {
        if (!map.has(m.rel)) map.set(m.rel, []);
        map.get(m.rel).push(importer);
      }
    }
  }

  for (const [k, v] of map.entries()) {
    map.set(k, [...new Set(v)].slice(0, 5));
  }
  return map;
}

function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
  // One canonical usage block for every context file (CLAUDE.md, AGENTS.md,
  // copilot-instructions.md, …). Lives here — the single source all writers
  // consume — so adapters don't each invent their own (now-removed) variant.
  const { usageBlock } = requireSourceOrBundled('./src/format/usage-guidance');
  const lines = [
    '<!-- Generated by SigMap gen-context.js v' + VERSION + ' -->',
    '<!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->',
    '',
    '# Code signatures',
    '',
    usageBlock(),
  ];

  // Compact dependency map section (shows import relationships, ~50-100 tokens)
  const depLines = buildDepMap(fileEntries, cwd);
  if ((!config || config.depMap !== false) && depLines.length) {
    lines.push('## deps');
    lines.push('```');
    lines.push(...depLines);
    lines.push('```');
    lines.push('');
  }

  // D8: installed direct-dependency version pins — grounds agents against what
  // is actually installed here (byte-stable given a fixed installed tree).
  if (!config || config.versionPins !== false) {
    try {
      const { collectVersionPins } = requireSourceOrBundled('./src/verify/lib-index');
      const { pins, total } = collectVersionPins(cwd);
      if (pins.length) {
        lines.push('## versions (installed direct deps)');
        lines.push('```');
        lines.push(...pins);
        if (total > pins.length) lines.push(`… +${total - pins.length} more`);
        lines.push('```');
        lines.push('');
      }
    } catch (_) { /* no/uninstalled deps → skip */ }
  }

  const todoLines = buildTodoSection(fileEntries, cwd, config || {});
  if (todoLines.length) {
    lines.push('## todos');
    lines.push('```');
    lines.push(...todoLines);
    lines.push('```');
    lines.push('');
  }

  const changesLines = buildChangesSection(cwd, config || {}, fileEntries);
  if (changesLines.length) lines.push(...changesLines);

  if (extras && Array.isArray(extras.diffSection) && extras.diffSection.length) {
    lines.push(...extras.diffSection);
  }

  const impactMap = resolveImpactRadius(fileEntries, cwd, config || {});

  // Group by top-level src dir
  const groups = {};
  for (const entry of fileEntries) {
    const rel = path.relative(cwd, entry.filePath);
    const parts = rel.split(path.sep);
    const group = parts.length > 1 ? parts[0] : '.';
    if (!groups[group]) groups[group] = [];
    groups[group].push({ rel, sigs: entry.sigs });
  }

  for (const [group, entries] of Object.entries(groups).sort()) {
    lines.push(`## ${group}`);
    lines.push('');
    for (const { rel, sigs } of entries) {
      if (sigs.length === 0) continue;
      const usedBy = impactMap.get(rel);
      const usedByStr = usedBy && usedBy.length ? ` (used by: ${usedBy.join(', ')})` : '';
      lines.push(`### ${rel}${usedByStr}`);
      lines.push('```');
      lines.push(...sigs);
      lines.push('```');
      lines.push('');
    }
  }

  if (routingEnabled) {
    try {
      const { classifyAll } = requireSourceOrBundled('./src/routing/classifier');
      const { formatRoutingSection } = requireSourceOrBundled('./src/routing/hints');
      const groups = classifyAll(fileEntries, cwd);
      lines.push(formatRoutingSection(groups));
    } catch (err) {
      console.warn(`[sigmap] routing hints skipped: ${err.message}`);
    }
  }

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

// ---------------------------------------------------------------------------
// Output writers
// ---------------------------------------------------------------------------
function ensureDir(filePath) {
  const dir = path.dirname(filePath);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

// ---------------------------------------------------------------------------
// Resolve output path for a given adapter
// ---------------------------------------------------------------------------
// Takes config.output into account for the copilot adapter.
// Returns the full path where output should be written.
function resolveAdapterPath(adapter, cwd, config) {
  config = config || {};
  
  // Copilot uses config.output if available, otherwise default
  if (adapter === 'copilot') {
    return config.output 
      ? path.join(cwd, config.output)
      : path.join(cwd, '.github', 'copilot-instructions.md');
  }
  
  // Fixed paths for other adapters
  if (adapter === 'claude') return path.join(cwd, 'CLAUDE.md');
  if (adapter === 'cursor') return path.join(cwd, '.cursorrules');
  if (adapter === 'windsurf') return path.join(cwd, '.windsurfrules');
  if (adapter === 'codex') return path.join(cwd, 'AGENTS.md');
  
  return null;
}

// ---------------------------------------------------------------------------
// Cache output writer (v0.8)
// ---------------------------------------------------------------------------
function writeCacheOutput(content, cwd) {
  try {
    const { formatCache } = __require('./src/format/cache');
    const cachePath = path.join(cwd, '.github', 'copilot-instructions.cache.json');
    ensureDir(cachePath);
    fs.writeFileSync(cachePath, formatCache(content), 'utf8');
    console.warn(`[sigmap] cache: wrote ${path.relative(cwd, cachePath)}`);
  } catch (err) {
    console.warn(`[sigmap] cache: failed to write cache output: ${err.message}`);
  }
}

function writeOutputs(content, targets, cwd, config) {
  config = config || {};
  // v3.0+: adapter-aware output targets
  const ADAPTER_TARGETS = new Set(['copilot', 'openai', 'gemini', 'codex']);

  const targetMap = {
    copilot: resolveAdapterPath('copilot', cwd, config),
    cursor: resolveAdapterPath('cursor', cwd, config),
    windsurf: resolveAdapterPath('windsurf', cwd, config),
  };

  for (const target of targets) {
    if (target === 'claude') {
      writeClaude(content, cwd);
      continue;
    }
    // v3.0+ adapter targets (copilot, openai, gemini, codex) — use bundled adapter to format + write
    if (ADAPTER_TARGETS.has(target)) {
      try {
        const adapterMod = __require('./packages/adapters/' + target);
        // Adapters add their own header via format() — strip the formatOutput() header
        // so the content body (## src, ## deps, etc.) is passed without a pre-existing
        // header, preventing duplicate "# Code signatures" blocks on every run.
        const adapterContent = stripFormatHeader(content);
        // copilot: honour config.output custom path (redirects away from default .github/copilot-instructions.md)
        if (target === 'copilot') {
          const outPath = resolveAdapterPath('copilot', cwd, config);
          const defaultPath = path.join(cwd, '.github', 'copilot-instructions.md');
          if (outPath !== defaultPath) {
            // custom path: format and write directly (no append logic)
            const formatted = adapterMod.format(adapterContent, { version: VERSION });
            ensureDir(outPath);
            fs.writeFileSync(outPath, formatted, 'utf8');
            console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)}`);
            continue;
          }
        }
        if (typeof adapterMod.write === 'function') {
          adapterMod.write(adapterContent, cwd, { version: VERSION });
          const outPath = adapterMod.outputPath(cwd);
          console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)} (appended signatures)`);
        } else {
          const formatted = adapterMod.format(adapterContent, { version: VERSION });
          const outPath = adapterMod.outputPath(cwd);
          ensureDir(outPath);
          fs.writeFileSync(outPath, formatted, 'utf8');
          console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)}`);
        }
      } catch (err) {
        console.warn(`[sigmap] adapter "${target}" failed: ${err.message}`);
      }
      continue;
    }
    const outPath = targetMap[target];
    if (!outPath) {
      console.warn(`[sigmap] unknown output target: ${target}`);
      continue;
    }
    ensureDir(outPath);
    fs.writeFileSync(outPath, content, 'utf8');
    console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)}`);
  }
}

// Strip the formatOutput() header block before passing content to adapters.
// formatOutput() prepends "<!-- Generated... -->\n\n# Code signatures\n\n" which
// is the raw-file header. Adapters add their own header via format(), so passing
// pre-headered content produces duplicate headers on every run.
function stripFormatHeader(content) {
  if (!content || typeof content !== 'string') return content;
  // Header ends after "# Code signatures\n\n" (with blank line)
  const marker = '\n# Code signatures\n';
  const idx = content.indexOf(marker);
  if (idx === -1) return content;
  // Skip past the marker line and one blank line
  const afterMarker = content.slice(idx + marker.length);
  return afterMarker.startsWith('\n') ? afterMarker.slice(1) : afterMarker;
}

function writeClaude(content, cwd) {
  const claudePath = path.join(cwd, 'CLAUDE.md');
  let existing = '';
  if (fs.existsSync(claudePath)) {
    existing = fs.readFileSync(claudePath, 'utf8');
  }
  // Strip the formatOutput() header — CLAUDE.md gets its own header via the MARKER
  const body = stripFormatHeader(content);
  const sigHeader = '# Code signatures\n\n';
  const markerIdx = existing.indexOf('## Auto-generated signatures');
  let newContent;
  if (markerIdx !== -1) {
    newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + sigHeader + body;
  } else {
    newContent = existing + MARKER + sigHeader + body;
  }
  fs.writeFileSync(claudePath, newContent, 'utf8');
  console.warn(`[sigmap] wrote CLAUDE.md (appended signatures)`);
}

// ---------------------------------------------------------------------------
// Report
// ---------------------------------------------------------------------------
function _coverageBar(pct, width) {
  width = width || 16;
  const filled = Math.round(pct / 100 * width);
  return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
}

function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit, coverageResult, isAutoBudget, configuredMaxTokens) {
  const reduction = inputTokens > 0 ? (100 - (finalTokens / inputTokens) * 100).toFixed(1) : 0;
  const overBudget = finalTokens > (budgetLimit || 6000);
  if (asJson) {
    const payload = {
      version: VERSION,
      timestamp: new Date().toISOString(),
      rawTokens: inputTokens,
      inputTokens,
      finalTokens,
      fileCount,
      droppedCount,
      reductionPct: parseFloat(reduction),
      overBudget,
      budgetLimit: budgetLimit || 6000,
      autoBudget: !!isAutoBudget,
    };
    if (coverageResult) {
      payload.coverage = {
        score: coverageResult.score,
        grade: coverageResult.grade,
        confidence: coverageResult.confidence,
        totalFiles: coverageResult.total,
        includedFiles: coverageResult.included,
        droppedFiles: coverageResult.dropped,
        perModule: Object.fromEntries(
          Array.from(coverageResult.perModule.entries())
            .map(([k, v]) => [k, v])
        ),
      };
    }
    process.stdout.write(JSON.stringify(payload) + '\n');
    // Exit 1 in CI if over budget — lets pipelines fail fast
    if (overBudget) process.exitCode = 1;
  } else {
    const budgetLabel = isAutoBudget
      ? `${budgetLimit || 6000} (auto-scaled)`
      : `${budgetLimit || 6000} (fixed)`;
    if (isAutoBudget && configuredMaxTokens && configuredMaxTokens !== budgetLimit) {
      console.warn(`[sigmap] note: autoMaxTokens is active — your maxTokens:${configuredMaxTokens} config was overridden by auto-scaled budget (${budgetLimit})`);
      console.warn(`  to use your value, set "autoMaxTokens": false in gen-context.config.json`);
    }
    console.log(`[sigmap] report:`);
    console.log(`  version         : ${VERSION}`);
    console.log(`  files processed : ${fileCount}`);
    console.log(`  files dropped   : ${droppedCount}`);
    console.log(`  input tokens    : ~${inputTokens}`);
    console.log(`  output tokens   : ~${finalTokens}`);
    console.log(`  budget limit    : ${budgetLabel}`);
    console.log(`  reduction       : ${reduction}%`);
    if (coverageResult) {
      const skipNote = coverageResult.nonCodeSkipped > 0
        ? `  (${coverageResult.nonCodeSkipped} non-code files skipped — json, md, config)`
        : '';
      console.log(`  coverage        : ${coverageResult.grade} (${coverageResult.score}%)  — ${coverageResult.included} of ${coverageResult.total} code files included`);
      if (skipNote) console.log(`                  ${skipNote}`);
      console.log(`  confidence      : ${coverageResult.confidence}`);
      if (coverageResult.perModule && coverageResult.perModule.size > 0) {
        console.log('');
        console.log('  Module Coverage:');
        for (const [dir, mod] of coverageResult.perModule) {
          if (mod.total === 0) continue;
          const bar = _coverageBar(mod.pct);
          const attention = mod.pct < 50 ? '  \u2190 attention needed' : '';
          console.log(`    ${dir.padEnd(18)} ${bar} ${String(mod.pct).padStart(3)}% (${mod.included}/${mod.total} files)${attention}`);
        }
        const lowModules = [...coverageResult.perModule.entries()].filter(([, m]) => m.pct < 50 && m.total > 0);
        if (lowModules.length > 0) {
          console.log('');
          console.log('  tip: modules marked "\u2190 attention needed" have <50% code-file coverage.');
          console.log('       Common causes:');
          console.log('         \u2022 token budget dropped files \u2014 raise maxTokens or use strategy:"per-module"');
          console.log('         \u2022 srcDir path includes non-code files \u2014 check .contextignore or exclude config');
          console.log('         \u2022 srcDir path is wrong \u2014 verify srcDirs in gen-context.config.json');
        }
      }
    }
    if (overBudget) console.warn(`[sigmap] WARNING: output (${finalTokens} tokens) exceeds budget (${budgetLimit || 6000})`);
  }
}

// ---------------------------------------------------------------------------
// Watch mode
// ---------------------------------------------------------------------------
function watchMode(cwd, config) {
  console.warn('[sigmap] watching for changes (Ctrl+C to stop)…');
  let debounce = null;
  for (const srcDir of config.srcDirs) {
    const abs = path.join(cwd, srcDir);
    if (!fs.existsSync(abs)) continue;
    fs.watch(abs, { recursive: true }, () => {
      if (debounce) clearTimeout(debounce);
      debounce = setTimeout(() => {
        console.warn('[sigmap] change detected, regenerating…');
        runGenerate(cwd, config, false);
      }, config.watchDebounce || 300);
    });
  }
}

// ---------------------------------------------------------------------------
// Git hook installer
// ---------------------------------------------------------------------------
function installHook(cwd, scriptPath) {
  const hookDir = path.join(cwd, '.git', 'hooks');
  if (!fs.existsSync(hookDir)) {
    console.warn('[sigmap] .git/hooks not found — skipping hook install');
    return;
  }
  const hookPath = path.join(hookDir, 'post-commit');
  const resolvedScript = path.resolve(scriptPath);
  const hookLine = `\nnode ${JSON.stringify(resolvedScript)} --generate 2>/dev/null || true\n`;

  if (fs.existsSync(hookPath)) {
    const existing = fs.readFileSync(hookPath, 'utf8');
    const existingHookLines = existing.split('\n').filter((line) => line.includes('gen-context.js'));
    if (existing.includes(hookLine.trim()) && existingHookLines.length === 1) {
      console.warn('[sigmap] post-commit hook already installed');
      return;
    }
    if (existing.includes('gen-context.js')) {
      const updated = existing
        .split('\n')
        .filter((line) => !line.includes('gen-context.js'))
        .join('\n')
        .replace(/\n+$/g, '\n');
      fs.writeFileSync(hookPath, updated.replace(/\n?$/g, '') + hookLine);
      console.warn('[sigmap] updated post-commit hook');
      return;
    }
    fs.appendFileSync(hookPath, hookLine);
  } else {
    fs.writeFileSync(hookPath, `#!/bin/sh${hookLine}`);
    fs.chmodSync(hookPath, '755');
  }
  console.warn('[sigmap] installed post-commit hook');
}

// ---------------------------------------------------------------------------
// Example config writer
// ---------------------------------------------------------------------------
function writeInitConfig(cwd) {
  const dest = path.join(cwd, 'gen-context.config.json');
  if (fs.existsSync(dest)) {
    console.warn('[sigmap] gen-context.config.json already exists — skipping');
  } else {
    const example = path.join(__dirname, 'gen-context.config.json.example');
    if (fs.existsSync(example)) {
      fs.copyFileSync(example, dest);
    } else {
      fs.writeFileSync(dest, JSON.stringify(DEFAULTS, null, 2) + '\n');
    }
    console.warn('[sigmap] wrote gen-context.config.json');
  }

  // Also scaffold .contextignore if it does not already exist
  const ignoreDest = path.join(cwd, '.contextignore');
  if (fs.existsSync(ignoreDest)) {
    console.warn('[sigmap] .contextignore already exists — skipping');
  } else {
    const ignoreContent = [
      '# SigMap ignore file — gitignore syntax',
      '# Files and directories listed here are excluded from signature extraction.',
      '# This file is union-merged with .repomixignore if present.',
      '',
      'node_modules/',
      'dist/',
      'build/',
      'out/',
      '.next/',
      'coverage/',
      'target/',
      'vendor/',
      '*.generated.*',
      '*.pb.*',
      '*_pb.*',
      '*.min.js',
      '*.min.css',
    ].join('\n') + '\n';
    fs.writeFileSync(ignoreDest, ignoreContent);
    console.warn('[sigmap] wrote .contextignore');
  }
}

// ---------------------------------------------------------------------------
// Core generate pipeline
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Strategy: per-module — one output file per top-level srcDir
// ---------------------------------------------------------------------------
function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
  // Group entries by their top-level srcDir
  const modules = {};
  for (const entry of fileEntries) {
    const rel = path.relative(cwd, entry.filePath);
    const topDir = rel.split(path.sep)[0];
    if (!modules[topDir]) modules[topDir] = [];
    modules[topDir].push(entry);
  }

  const moduleNames = Object.keys(modules).sort();
  console.warn(`[sigmap] per-module: ${moduleNames.length} modules — ${moduleNames.join(', ')}`);

  // Thin overview → primary output (always-on, tiny)
  const overviewLines = [
    '<!-- Generated by SigMap gen-context.js v' + VERSION + ' — per-module overview -->',
    '<!-- Load the full module context file for detailed signatures -->',
    '',
    '# Codebase overview',
    '',
    '| Module | Full context file |',
    '|--------|-------------------|',
  ];

  let totalOut = 0;
  for (const mod of moduleNames) {
    const outName = `context-${mod}.md`;
    const outPath = path.join(cwd, '.github', outName);
    const modEntries = modules[mod];

    // Per-module budget: each module gets its own full effective budget
    // (per-module strategy is the recommended path for large repos — no sharing needed)
    const modBudget = Math.max(1000, config.maxTokens);
    const budgeted = applyTokenBudget(modEntries, modBudget);

    const content = formatOutput(budgeted, cwd, false, config, null);
    ensureDir(outPath);
    fs.writeFileSync(outPath, content, 'utf8');
    const modTokens = estimateTokens(content);
    totalOut += modTokens;
    console.warn(`[sigmap] per-module: wrote .github/${outName} (~${modTokens} tokens, ${budgeted.length} files)`);

    overviewLines.push(`| \`${mod}\` | \`.github/${outName}\` |`);
  }

  overviewLines.push('');
  overviewLines.push('> Inject the relevant module file into your IDE context window.');
  overviewLines.push('> For cross-module questions load both files.');
  const overviewContent = overviewLines.join('\n') + '\n';
  const primaryTargets = config.outputs || ['copilot'];
  writeOutputs(overviewContent, primaryTargets, cwd, config);

  const overviewTokens = estimateTokens(overviewContent);
  console.warn(`[sigmap] per-module: overview ~${overviewTokens} tokens (always-on), modules total ~${totalOut} tokens (on-demand)`);
  return { inputTokenTotal, finalTokens: overviewTokens, fileCount: fileEntries.length, droppedCount: 0 };
}

// ---------------------------------------------------------------------------
// Strategy: hot/cold — hot injected always, cold available via MCP only
// ---------------------------------------------------------------------------
function runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTotal) {
  const hotEntries = fileEntries.filter((e) => recentFiles.has(e.filePath));
  const coldEntries = fileEntries.filter((e) => !recentFiles.has(e.filePath));

  // Hot → primary output (auto-injected by IDE)
  const hotContent = hotEntries.length > 0
    ? formatOutput(hotEntries, cwd, false, config, null)
    : '<!-- Generated by SigMap — no recently changed files -->\n';
  const primaryTargets = config.outputs || ['copilot'];
  writeOutputs(hotContent, primaryTargets, cwd, config);
  const hotTokens = estimateTokens(hotContent);

  // Cold → .github/context-cold.md (MCP reads this on demand)
  const coldPath = path.join(cwd, '.github', 'context-cold.md');
  const coldHeader = [
    '<!-- Generated by SigMap gen-context.js v' + VERSION + ' — cold context (MCP only) -->',
    '<!-- NOT auto-injected. Retrieve via MCP: read_context({ module: "..." }) -->',
    '',
  ].join('\n');
  const coldContent = coldHeader + formatOutput(coldEntries, cwd, false, config, null);
  ensureDir(coldPath);
  fs.writeFileSync(coldPath, coldContent, 'utf8');
  const coldTokens = estimateTokens(coldContent);

  console.warn('[sigmap] hot-cold:');
  console.warn(`  hot  (auto-injected) : ${hotEntries.length} files ~${hotTokens} tokens → primary output`);
  console.warn(`  cold (MCP on-demand) : ${coldEntries.length} files ~${coldTokens} tokens → .github/context-cold.md`);
  if (coldEntries.length > 0) {
    console.warn('  retrieve cold: read_context({ module: "<dir>" }) via MCP');
  }

  return { inputTokenTotal, finalTokens: hotTokens, fileCount: fileEntries.length, droppedCount: 0 };
}

// ---------------------------------------------------------------------------
// Diff-mode pipeline — context for changed files only
// ---------------------------------------------------------------------------
function runDiff(cwd, config, stagedOnly, baseRef) {
  const diffFiles = baseRef ? getFilesChangedSinceBase(cwd, baseRef) : getDiffFiles(cwd, stagedOnly);

  if (diffFiles.size === 0) {
    const scope = baseRef ? `base ${baseRef}` : (stagedOnly ? 'staged' : 'working tree');
    console.warn(`[sigmap] --diff: no changed files found in ${scope} — running full generate`);
    runGenerate(cwd, config, false);
    return;
  }

  const ignorePatterns = loadIgnorePatterns(cwd);
  let allFiles = buildFileList(cwd, config);
  allFiles = allFiles.filter((f) => {
    const rel = path.relative(cwd, f).replace(/\\/g, '/');
    return !matchesIgnore(rel, ignorePatterns);
  });

  // Restrict to diff files, keeping only those already in srcDirs
  const diffFiltered = allFiles.filter((f) => diffFiles.has(f));

  if (diffFiltered.length === 0) {
    console.warn('[sigmap] --diff: changed files are outside tracked srcDirs — running full generate');
    runGenerate(cwd, config, false);
    return;
  }

  let inputTokenTotal = 0;
  let fileEntries = [];
  let testIndex = null;
  if (config.testCoverage) {
    try {
      const { buildTestIndex } = requireSourceOrBundled('./src/extractors/coverage');
      testIndex = buildTestIndex(cwd, config.testDirs);
    } catch (err) {
      console.warn(`[sigmap] coverage index failed: ${err.message}`);
    }
  }

  for (const filePath of diffFiltered) {
    let content = '';
    try {
      content = fs.readFileSync(filePath, 'utf8');
    } catch (_) {
      continue;
    }

    let sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
    if (sigs.length === 0) continue;

    inputTokenTotal += estimateTokens(content);

    if (config.secretScan) {
      const { scan } = requireSourceOrBundled('./src/security/scanner');
      const result = scan(sigs, filePath);
      if (result.redacted) {
        console.warn(`[sigmap] secrets redacted in ${path.relative(cwd, filePath)}`);
      }
      sigs = result.safe;
    }

    sigs = annotateCoverage(sigs, testIndex, !!config.testCoverage);

    fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime: Date.now() });
  }

  if (fileEntries.length === 0) {
    console.warn('[sigmap] --diff: no extractable signatures in changed files — running full generate');
    runGenerate(cwd, config, false);
    return;
  }

  const routingEnabled = !!(config.routing || process.argv.includes('--routing'));
  const diffSection = baseRef ? buildDiffSectionFromBase(cwd, baseRef, fileEntries, config) : [];
  const content = formatOutput(fileEntries, cwd, routingEnabled, config, { diffSection });
  const finalTokens = estimateTokens(content);
  writeOutputs(content, config.outputs, cwd, config);

  const scope = baseRef ? `diff-vs-${baseRef}` : (stagedOnly ? 'staged' : 'diff');
  console.warn(`[sigmap] ${scope} files: ${fileEntries.length}, diff tokens: ~${finalTokens}`);

  // v4.0: risk score per changed file
  try {
    const { buildFromCwd } = requireSourceOrBundled('./src/graph/builder');
    const graph = buildFromCwd(cwd, { silent: true });
    const reverseGraph = graph.reverse || new Map();

    function _isRouteFile(f) {
      return /\/(routes?|pages?|controllers?|handlers?|api)\//i.test(f)
        || /\.(route|page|controller|handler)\.\w+$/.test(f);
    }
    function _riskScore(filePath, sigs, revGraph) {
      let s = 0;
      if (sigs.some(sig => sig.includes('export') || sig.includes('module.exports'))) s += 2;
      const deps = revGraph.get(filePath) || new Set();
      if (deps.size > 3) s += 2;
      if (_isRouteFile(filePath)) s += 1;
      if (/\.(config|env|settings)\.\w+$/.test(filePath)) s += 1;
      return s >= 4 ? 'HIGH' : s >= 2 ? 'MEDIUM' : 'LOW';
    }

    console.warn(`[sigmap] Risk: Changed files (${fileEntries.length}):`);
    for (const fe of fileEntries) {
      const level = _riskScore(fe.filePath, fe.sigs, reverseGraph);
      const rel   = path.relative(cwd, fe.filePath).replace(/\\/g, '/');
      const deps  = reverseGraph.get(fe.filePath) || new Set();
      const reasons = [];
      if (fe.sigs.some(s => s.includes('export') || s.includes('module.exports'))) reasons.push('exports public API');
      if (deps.size > 0) reasons.push(`${deps.size} downstream dependent${deps.size === 1 ? '' : 's'}`);
      if (_isRouteFile(fe.filePath)) reasons.push('route file');
      if (/\.(config|env|settings)\.\w+$/.test(fe.filePath)) reasons.push('config file');
      const label = level === 'HIGH' ? '[HIGH]  ' : level === 'MEDIUM' ? '[MEDIUM]' : '[LOW]   ';
      console.warn(`  ${rel.padEnd(40)} ${label}${reasons.length ? '  — ' + reasons.join(', ') : ''}`);
    }
  } catch (_) {}

  if (process.argv.includes('--report')) {
    // Also show what the full run would cost for comparison
    const fullResult = runGenerate(cwd, config, true);
    console.log(`[sigmap] diff report:`);
    console.log(`  mode            : ${scope}`);
    console.log(`  diff files      : ${fileEntries.length}`);
    console.log(`  diff tokens     : ~${finalTokens}`);
    console.log(`  full tokens     : ~${fullResult.finalTokens}`);
    console.log(`  savings         : ~${fullResult.finalTokens - finalTokens} tokens`);
  }
}

// ---------------------------------------------------------------------------
// Core generate pipeline
// ---------------------------------------------------------------------------
function runGenerate(cwd, config, reportMode, reportJson = false) {
  const __genT0 = Date.now();
  const ignorePatterns = loadIgnorePatterns(cwd);
  let allFiles = buildFileList(cwd, config);

  // Apply .contextignore
  allFiles = allFiles.filter((f) => {
    const rel = path.relative(cwd, f).replace(/\\/g, '/');
    return !matchesIgnore(rel, ignorePatterns);
  });

  // Gather mtime and git-committed info
  const hotCommits = config.hotCommits || 10;
  const recentFiles = config.diffPriority ? getRecentlyCommittedFiles(cwd, hotCommits) : new Set();

  // v6.7: Load signature cache if enabled
  let cache = null;
  const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = requireSourceOrBundled('./src/cache/sig-cache');
  if (config.sigCache) {
    cache = loadCache(cwd, VERSION);
  }

  let inputTokenTotal = 0;
  let fileEntries = [];
  let testIndex = null;
  if (config.testCoverage) {
    try {
      const { buildTestIndex } = requireSourceOrBundled('./src/extractors/coverage');
      testIndex = buildTestIndex(cwd, config.testDirs);
    } catch (err) {
      console.warn(`[sigmap] coverage index failed: ${err.message}`);
    }
  }

  for (const filePath of allFiles) {
    let content = '';
    try {
      content = fs.readFileSync(filePath, 'utf8');
    } catch (_) {
      continue;
    }

    // v6.7: Check cache before extracting
    let sigs = [];
    if (cache) {
      const cached = cache.get(filePath);
      let mtime = 0;
      try { mtime = fs.statSync(filePath).mtimeMs; } catch (_) {}
      if (cached && cached.mtime === mtime) {
        sigs = cached.sigs;
      } else {
        sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
        if (sigs.length > 0) {
          cache.set(filePath, { mtime, sigs });
        }
      }
    } else {
      sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
    }
    if (sigs.length === 0) continue;

    // Baseline = estimated tokens of original source content for intuitive reduction stats.
    inputTokenTotal += estimateTokens(content);

    if (config.secretScan) {
      const { scan } = requireSourceOrBundled('./src/security/scanner');
      const result = scan(sigs, filePath);
      if (result.redacted) {
        console.warn(`[sigmap] secrets redacted in ${path.relative(cwd, filePath)}`);
      }
      sigs = result.safe;
    }

    sigs = annotateCoverage(sigs, testIndex, !!config.testCoverage);

    let mtime = 0;
    try {
      mtime = fs.statSync(filePath).mtimeMs;
    } catch (_) {}

    // Boost recently committed files (give them max mtime so they aren't dropped first)
    if (recentFiles.has(filePath)) mtime = Date.now();

    fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime });
  }

  // v6.5: Coverage feedback loop — warn if symbol density is suspiciously low
  const symbolsFound = fileEntries.reduce((n, f) => n + (f.sigs?.length || 0), 0);
  const filesFound   = fileEntries.length;
  const coverageRatio = filesFound > 0 ? symbolsFound / filesFound : 1;
  if (coverageRatio < 0.25 && !config.srcDirs?.length) {
    process.stderr.write('[sigmap] ⚠ low symbol coverage — source roots may be wrong\n');
    process.stderr.write('[sigmap]   run: sigmap roots --explain\n');
  }

  const strategy = config.strategy || 'full';
  const beforeCount = fileEntries.length;

  // Sort for output ordering: recently committed files appear first
  if (config.diffPriority && recentFiles.size > 0) {
    fileEntries.sort((a, b) => {
      const aRecent = recentFiles.has(a.filePath) ? 0 : 1;
      const bRecent = recentFiles.has(b.filePath) ? 0 : 1;
      return aRecent - bRecent;
    });
  }

  // v4.1: compute effective budget once; used by all strategies
  const effectiveMaxTokens = computeEffectiveMaxTokens(fileEntries, config);
  // Propagate to config so per-module / hot-cold strategies pick it up
  const configWithBudget = effectiveMaxTokens !== config.maxTokens
    ? Object.assign({}, config, { maxTokens: effectiveMaxTokens, _autoMaxTokens: effectiveMaxTokens })
    : config;

  let result;
  if (!reportMode) {
    if (strategy === 'per-module') {
      result = runPerModuleStrategy(cwd, configWithBudget, fileEntries, inputTokenTotal);
    } else if (strategy === 'hot-cold') {
      result = runHotColdStrategy(cwd, configWithBudget, fileEntries, recentFiles, inputTokenTotal);
    } else {
      // 'full' — original behaviour
      fileEntries = applyTokenBudget(fileEntries, effectiveMaxTokens);
      const droppedCount = beforeCount - fileEntries.length;
      const routingEnabled = !!(config.routing || process.argv.includes('--routing'));
      const content = formatOutput(fileEntries, cwd, routingEnabled, config, null);
      const finalTokens = estimateTokens(content);
      const formatIdx = process.argv.indexOf('--format');
      const formatValue = formatIdx >= 0 ? process.argv[formatIdx + 1] : (config.format || 'default');
      // Feature 2: --mode write matrix
      const activeMode = process.argv.indexOf('--mode') >= 0
        ? process.argv[process.argv.indexOf('--mode') + 1]
        : (config.mode || 'default');
      if (activeMode === 'fast') {
        const llmTxtMod = requireSourceOrBundled('./src/format/llm-txt');
        const syncContext = { projectName: path.basename(cwd), fileEntries, srcDirs: config.srcDirs || [] };
        fs.writeFileSync(llmTxtMod.outputPath(cwd), llmTxtMod.format(syncContext, cwd, VERSION));
        console.warn(`[sigmap] --mode fast → llm.txt`);
      } else if (activeMode === 'full') {
        const llmFullMod = requireSourceOrBundled('./packages/adapters/llm-full');
        const syncContext = { projectName: path.basename(cwd), fileEntries, srcDirs: config.srcDirs || [] };
        llmFullMod.write(syncContext, cwd, { version: VERSION });
        console.warn(`[sigmap] --mode full → llm-full.txt`);
      } else if (activeMode === 'both') {
        const llmTxtMod  = requireSourceOrBundled('./src/format/llm-txt');
        const llmFullMod = requireSourceOrBundled('./packages/adapters/llm-full');
        const llmsMod    = requireSourceOrBundled('./src/format/llms-txt');
        const syncContext = { projectName: path.basename(cwd), fileEntries, srcDirs: config.srcDirs || [] };
        fs.writeFileSync(llmTxtMod.outputPath(cwd),  llmTxtMod.format(syncContext, cwd, VERSION));
        llmFullMod.write(syncContext, cwd, { version: VERSION });
        writeOutputs(content, config.outputs, cwd, config);
        const llmsContent = llmsMod.format(syncContext, cwd, [
          { path: llmTxtMod.outputPath(cwd),  label: 'llm.txt' },
          { path: llmFullMod.outputPath(cwd), label: 'llm-full.txt' },
        ], VERSION);
        fs.writeFileSync(llmsMod.outputPath(cwd), llmsContent);
        console.warn(`[sigmap] --mode both → copilot-instructions.md + llm.txt + llm-full.txt + llms.txt`);
      } else {
        writeOutputs(content, config.outputs, cwd, config);
      }
      if (formatValue === 'cache') writeCacheOutput(content, cwd);

      // --output <file>: write a copy of the formatted context to the custom path.
      if (config.customOutput) {
        try {
          const absCustom = path.resolve(cwd, config.customOutput);
          const SIGMAP_HEADER = [
            `<!-- Generated by SigMap v${VERSION} -->`,
            `<!-- Updated: ${new Date().toISOString()} -->`,
            `<!-- Regenerate: node gen-context.js --output ${config.customOutput} -->`,
            '',
          ].join('\n');
          fs.mkdirSync(path.dirname(absCustom), { recursive: true });
          fs.writeFileSync(absCustom, SIGMAP_HEADER + content, 'utf8');
          console.warn(`[sigmap] wrote ${path.relative(cwd, absCustom)} (custom output)`);
        } catch (err) {
          console.warn(`[sigmap] --output write failed: ${err.message}`);
        }
      }

      result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
    }
  } else {
    // report mode: always run full pipeline for accurate stats
    const budgeted = applyTokenBudget([...fileEntries], effectiveMaxTokens);
    const droppedCount = beforeCount - budgeted.length;
    const content = formatOutput(budgeted, cwd, false, configWithBudget, null);
    const finalTokens = estimateTokens(content);
    // v4.0: compute coverage score for --report heatmap
    let coverageResult = null;
    try {
      const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
      coverageResult = coverageScore(cwd, budgeted, configWithBudget);
    } catch (_) {}
    result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount, coverageResult };
  }

  if (reportMode || process.argv.includes('--report')) {
    printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, effectiveMaxTokens, result.coverageResult, config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens, config.maxTokens);
  }

  // Usage tracking (v0.9) — legacy health log, opt-in via config.tracking / --track.
  const trackingEnabled = !!(config.tracking || process.argv.includes('--track'));
  if (trackingEnabled && !reportMode) {
    try {
      const { logRun } = requireSourceOrBundled('./src/tracking/logger');
      logRun({
        version: VERSION,
        fileCount: result.fileCount,
        droppedCount: result.droppedCount,
        rawTokens: result.inputTokenTotal,
        finalTokens: result.finalTokens,
        overBudget: result.finalTokens > effectiveMaxTokens,
        budgetLimit: effectiveMaxTokens,
        autoBudget: config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens,
      }, cwd);
    } catch (err) {
      console.warn(`[sigmap] tracking: ${err.message}`);
    }
  }

  // gain: capture generate savings (separate gain.ndjson; default ON, opt-out).
  if (!reportMode) {
    try {
      const { recordUsage, isTrackingEnabled } = requireSourceOrBundled('./src/tracking/logger');
      if (isTrackingEnabled(config, process.argv)) {
        recordUsage({
          version: VERSION,
          op: 'generate',
          baselineTokens: result.inputTokenTotal,
          actualTokens: result.finalTokens,
          durationMs: Date.now() - __genT0,
        }, cwd);
      }
    } catch (_) { /* gain capture is best-effort */ }
  }

  // Feature 8: post-run summary — 6-line stdout after every normal run
  if (!reportMode
    && !process.argv.includes('--json')
    && !process.argv.includes('--report')
    && !process.argv.includes('--quiet')) {
    const bar  = '\u2500'.repeat(43);
    const syms = fileEntries.reduce((n, f) => n + (f.sigs ? f.sigs.length : 0), 0);
    const pct  = result.inputTokenTotal > 0
      ? Math.round((1 - result.finalTokens / result.inputTokenTotal) * 100)
      : 0;
    // v4.0: coverage score in post-run summary
    let coverageLine = '';
    try {
      const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
      const cov = coverageScore(cwd, fileEntries, configWithBudget);
      const autoBudgetNote = (config.autoMaxTokens !== false && effectiveMaxTokens !== config.maxTokens)
        ? `  [budget: ${effectiveMaxTokens} auto-scaled]`
        : '';
      coverageLine = ` Coverage       : ${cov.grade} (${cov.score}%)  \u2014 ${cov.included} of ${cov.total} source files included${autoBudgetNote}`;
      // Extra warning line when coverage is still poor despite auto-scaling
      if (cov.score < 40 && config.strategy !== 'per-module' && config.strategy !== 'hot-cold') {
        coverageLine += '\n [sigmap] tip: large repo — consider strategy:"per-module" for full coverage';
      }
    } catch (_) {}
    const lines = [
      bar,
      ` SigMap v${VERSION}`,
      ` Files scanned  : ${result.fileCount}`,
      ` Symbols found  : ${syms.toLocaleString()}`,
      ` Token reduction: ${pct}%  (${result.inputTokenTotal.toLocaleString()} \u2192 ${result.finalTokens.toLocaleString()})`,
    ];
    if (coverageLine) lines.push(coverageLine);
    lines.push(` Output         : .github/copilot-instructions.md`);
    lines.push(bar);
    lines.push(` Try: "explain the architecture" \u00b7 "find the auth module"`);
    lines.push(bar, '');
    process.stderr.write(lines.join('\n'));
  }

  // v6.7: Save cache if enabled
  if (config.sigCache && cache) {
    try {
      saveCache(cwd, VERSION, cache);
    } catch (err) {
      console.warn(`[sigmap] cache save failed: ${err.message}`);
    }
  }

  // v7.0.1: count a plain context-generation run for the one-time star nudge.
  // Most users only ever run `sigmap` to build the context file — count that too,
  // not just ask/squeeze. Guard so a monorepo (many runGenerate calls per process)
  // counts as a single run. Interactive only — silent under --json/--report or non-TTY.
  if (!global.__sigmapGenCounted) {
    global.__sigmapGenCounted = true;
    try {
      const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
      const showNudge = !!process.stderr.isTTY
        && !reportJson
        && !process.argv.includes('--json')
        && !process.argv.includes('--report')
        && !process.argv.includes('--quiet');
      checkStarNudge(global.__sigmapRootCwd || cwd, true, { silent: !showNudge });
    } catch (_) {}
  }

  return result;
}

// ---------------------------------------------------------------------------
// Monorepo support
// ---------------------------------------------------------------------------
const MONO_ROOTS  = ['packages', 'apps', 'services', 'libs'];
const PKG_MANIFESTS = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'build.gradle', 'pom.xml', 'requirements.txt'];

function detectMonorepoPackages(cwd, config) {
  const packages = [];
  const seen = new Set();

  const addIfPackage = (pkgPath) => {
    if (seen.has(pkgPath)) return;
    seen.add(pkgPath);
    for (const manifest of PKG_MANIFESTS) {
      if (fs.existsSync(path.join(pkgPath, manifest))) {
        packages.push(pkgPath);
        return;
      }
    }
  };

  // Strategy 1: classic JS monorepo roots (packages/, apps/, services/, libs/)
  for (const monoDir of MONO_ROOTS) {
    const abs = path.join(cwd, monoDir);
    if (!fs.existsSync(abs)) continue;
    let entries;
    try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch (_) { continue; }
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      addIfPackage(path.join(abs, entry.name));
    }
  }

  // Strategy 2: treat each srcDir that has a manifest as its own package
  // (covers server/web/desktop-style layouts)
  if (packages.length === 0 && config && config.srcDirs) {
    for (const srcDir of config.srcDirs) {
      const abs = path.join(cwd, srcDir);
      if (!fs.existsSync(abs)) continue;
      addIfPackage(abs);
    }
  }

  return packages;
}

function runMonorepo(cwd, config) {
  const packages = detectMonorepoPackages(cwd, config);
  if (packages.length === 0) {
    console.warn('[sigmap] no monorepo packages found — checked packages/, apps/, services/, libs/');
    return;
  }
  console.warn(`[sigmap] monorepo: found ${packages.length} packages`);

  for (const pkgPath of packages) {
    const pkgName = path.relative(cwd, pkgPath);
    console.warn(`[sigmap] monorepo: processing ${pkgName}`);
    // Per-package config: scan src/ and package root, inherit outputs from parent config
    const pkgConfig = {
      ...config,
      srcDirs: ['src', 'lib', 'app', '.'],
    };
    try {
      runGenerate(pkgPath, pkgConfig, false);
    } catch (err) {
      console.warn(`[sigmap] monorepo: failed for ${pkgName}: ${err.message}`);
    }
  }
  console.warn(`[sigmap] monorepo: done — wrote context files for ${packages.length} packages`);
}

// ---------------------------------------------------------------------------
// Multi-repo support  (--each)
// Run sigmap independently for every immediate subdirectory that looks like
// an independent git repo or project root.  Each sub-repo gets its own
// context files written inside it, using its own gen-context.config.json
// when present.
// ---------------------------------------------------------------------------
function detectRepoDirs(cwd) {
  let entries;
  try {
    entries = fs.readdirSync(cwd, { withFileTypes: true });
  } catch (_) {
    return [];
  }
  const repos = [];
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    if (entry.name.startsWith('.')) continue;
    const abs = path.join(cwd, entry.name);
    // A directory qualifies if it has its own .git folder
    // OR contains a recognised project manifest (fallback for un-initialised repos)
    const hasGit = fs.existsSync(path.join(abs, '.git'));
    const hasManifest = PKG_MANIFESTS.some((m) => fs.existsSync(path.join(abs, m)));
    if (hasGit || hasManifest) repos.push(abs);
  }
  return repos;
}

function runEach(cwd, baseConfig, adapterOverride) {
  const repos = detectRepoDirs(cwd);
  if (repos.length === 0) {
    console.warn('[sigmap] --each: no project subdirectories found');
    console.warn('[sigmap]   A subdirectory qualifies when it contains .git or a package manifest');
    console.warn(`[sigmap]   (package.json / pyproject.toml / Cargo.toml / go.mod / etc.)`);
    return;
  }
  console.warn(`[sigmap] --each: found ${repos.length} repos — ${repos.map((r) => path.basename(r)).join(', ')}`);
  let ok = 0, failed = 0;
  for (const repoDir of repos) {
    const name = path.basename(repoDir);
    // Load each repo's own config independently, fall back to base config
    let repoConfig;
    try {
      repoConfig = loadConfig(repoDir);
    } catch (_) {
      repoConfig = { ...baseConfig };
    }
    // --adapter override: apply on top of per-repo config
    if (adapterOverride) {
      repoConfig = Object.assign({}, repoConfig, {
        outputs: adapterOverride === 'claude' ? ['claude'] : [adapterOverride],
        adapters: [adapterOverride],
      });
    }
    console.warn(`[sigmap] --each: processing ${name} …`);
    try {
      runGenerate(repoDir, repoConfig, false);
      ok++;
    } catch (err) {
      console.warn(`[sigmap] --each: FAILED for ${name}: ${err.message}`);
      failed++;
    }
  }
  console.warn(`[sigmap] --each: done — ${ok} succeeded${failed > 0 ? `, ${failed} failed` : ''}`);
}

// ---------------------------------------------------------------------------
// CLI entry point
// ---------------------------------------------------------------------------
/**
 * Classify a task description and return the recommended model tier.
 *
 * Rules (ordered):
 *  1. Match any 'powerful' keyword → 'powerful'
 *  2. Match any 'fast' keyword     → 'fast'
 *  3. Default                      → 'balanced'
 *
 * @param {string} description - Natural-language task description
 * @returns {{ tier: string, label: string, models: string, costHint: string }}
 */
function suggestTool(description) {
  const lower = description.toLowerCase();
  const { TIERS } = __require('./src/routing/hints');

  const powerfulKeywords = [
    'architecture', 'cross-cutting', 'multi-file', 'security audit', 'owasp',
    'migration plan', 'framework upgrade', 'complex debug', 'async boundar',
    'multi-service', 'distributed system', 'performance audit', 'new module from',
    'redesign', 'from requirements',
  ];
  const fastKeywords = [
    'typo', 'rename symbol', 'format', 'lint', 'style change', 'trivial',
    'quick fix', 'inline suggest', 'autocomplete', 'config file',
    'dockerfile', 'shell script', '.yaml', '.yml', '.json', '.css',
    'html template', 'markup',
  ];

  let tier = 'balanced';
  if (powerfulKeywords.some((kw) => lower.includes(kw))) tier = 'powerful';
  else if (fastKeywords.some((kw) => lower.includes(kw))) tier = 'fast';

  const info = TIERS[tier];
  return { tier, label: info.label, models: info.examples, costHint: info.costHint };
}

function resolveProjectRoot(startDir) {
  try {
    const gitRoot = __git(['rev-parse', '--show-toplevel'], { cwd: startDir }).trim();
    if (gitRoot) return gitRoot;
  } catch (_) {}
  return startDir;
}

function detectInvokedAs() {
  const argv1 = process.argv[1] || '';
  const base = path.basename(argv1);
  const baseNoExt = base.endsWith('.js') ? base.slice(0, -3) : base;
  // npx: the script path goes through an _npx cache directory
  if (argv1.includes('/_npx/') || argv1.includes('\\_npx\\') ||
      argv1.includes('/.npm/_') || argv1.includes('\\.npm\\_')) {
    return 'npx sigmap';
  }
  // globally-installed bin aliases (no .js extension)
  if (baseNoExt === 'sigmap') return 'sigmap';
  if (baseNoExt === 'gen-context') return 'gen-context';
  // default: local file invocation
  return 'node gen-context.js';
}

function printHelp(cmd) {
  cmd = cmd || 'node gen-context.js';
  const header = cmd === 'node gen-context.js'
    ? `SigMap — gen-context.js v${VERSION}`
    : `SigMap v${VERSION}  (${cmd})`;
  console.log(`
${header}
Zero-dependency AI context engine

Usage:
  ${cmd}                                   Generate context once and exit
  ${cmd} --monorepo                        Generate per-package context (monorepo)
  ${cmd} --each                            Run for every repo in the current directory
  ${cmd} --routing                         Include model routing hints in output
  ${cmd} --format cache                    Also write Anthropic prompt-cache JSON
  ${cmd} --track                           Append run metrics to .context/usage.ndjson
  ${cmd} --watch                           Generate + watch for file changes
  ${cmd} --setup                           Generate + install git hook + watch
  ${cmd} --mcp                             Start MCP server on stdio
  ${cmd} --report                          Token reduction stats to stdout
  ${cmd} --report --json                   Token report as JSON (for CI; exits 1 if over budget)
  ${cmd} --report --history                Print usage log summary from .context/usage.ndjson
  ${cmd} --report --history --chart        Include inline SVG charts + Unicode sparklines
  ${cmd} --dashboard                       Write benchmarks/reports/dashboard.html
  ${cmd} --suggest-tool "<task>"           Recommend model tier for a task description
  ${cmd} --suggest-tool "<task>" --json    Machine-readable tier recommendation
  ${cmd} --health                          Print composite health score
  ${cmd} --health --json                   Machine-readable health score
  ${cmd} gain                              Token-savings dashboard (totals + by-operation)
  ${cmd} gain --all                        Add daily / weekly / monthly trend tables
  ${cmd} gain --json                       Aggregate savings as JSON
  ${cmd} gain --since 7d                    Window filter (7d, 30d, 12h, or ISO date)
  ${cmd} gain --top <n> | --model <name>   Limit rows / set $ pricing model
  ${cmd} gain --reset                      Clear the local savings log (.context/gain.ndjson)
  ${cmd} ... --no-track                    Disable gain savings capture for this run
  ${cmd} --diff                            Generate context for git-changed files only
  ${cmd} --diff <base-ref>                 Generate context + structural diff vs base ref (e.g. main)
  ${cmd} --diff --staged                   Generate context for staged files only
  ${cmd} --benchmark                       Run retrieval benchmark (benchmarks/tasks/retrieval.jsonl)
  ${cmd} --adapter <name>                  Generate for a specific adapter only (v3.0+)
  ${cmd} --adapter <name> --json           Show adapter output path as JSON
  ${cmd} --benchmark --json                Benchmark results as JSON
  ${cmd} --eval                            Alias for --benchmark
  ${cmd} --analyze                         Per-file breakdown: sigs, tokens, extractor, coverage
  ${cmd} --analyze --json                  Breakdown as JSON
  ${cmd} --analyze --slow                  Re-time each extractor; flag files >50ms
  ${cmd} --diagnose-extractors             Run all 21 extractors vs fixtures; show pass/fail + diff
  ${cmd} --query "<text>"                  Rank files by relevance to a query
  ${cmd} --query "<text>" --json           Ranked results as JSON
  ${cmd} --query "<text>" --top <n>        Limit results to top N files (default 10)
  ${cmd} learn --good <files...>           Boost files in .context/weights.json
  ${cmd} learn --bad <files...>            Penalize files in .context/weights.json
  ${cmd} learn --reset                     Delete learned file weights
  ${cmd} weights                           Show learned file multipliers
  ${cmd} weights --json                    Learned weights as JSON
  ${cmd} --impact <file>                   Show every file impacted by changing <file>
  ${cmd} --impact <file> --json            Impact as JSON {changed, direct, transitive, tests, routes}
  ${cmd} --impact <file> --depth <n>       BFS depth limit (default 3, 0=unlimited)
  ${cmd} --callers <symbol>                Method-level blast radius — every function that (transitively) calls <symbol> (JS/TS + Python)
  ${cmd} --callees <symbol>                Every repo function that <symbol> (transitively) calls
  ${cmd} --callers <symbol> --json --depth <n>   Call-graph edges as JSON (depth 0 = unlimited)
  ${cmd} verify <answer.md>                Flagship grounding guard — flag fake files/tests/imports/symbols/npm-scripts in an AI answer (alias of verify-ai-output)
  ${cmd} verify <answer.md> --json         Grounding report as JSON (exits 1 if issues)
  ${cmd} verify <answer.md> --report       Write a standalone HTML report (red/amber/green)
  ${cmd} verify-ai-output <answer.md>      Full command name for ${cmd} verify
  ${cmd} conventions                       Extract repo file-naming/export/test conventions (--conflicts, --inject, --report, --fix)
  ${cmd} scaffold "<name>"                 Propose a convention-matched file/dir scaffold (--ext, --threshold, --force, --json)
  ${cmd} verify-plan <plan.md|->           Check a plan vs the live index — files/symbols exist, blast radius, scope (--json)
  ${cmd} review-pr                         Audit a diff — scope drift, god-node edits, missing tests, security files (--staged, --base, --json, --markdown)
  ${cmd} review-pr --markdown              PR Evidence Report — branded Markdown (signatures + blast radius + tests) to post as a PR comment
  ${cmd} create "<task>"                   Grounded-creation pipeline: scaffold → verify-plan → verify-ai-output → review-pr (--staged)
  ${cmd} squeeze <file|->                  Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
  ${cmd} ask "<query>" --squeeze           Auto-accept input minimization (no prompt; for scripts/CI)
  ${cmd} ask "<query>" --no-squeeze        Disable input minimization entirely
  ${cmd} ask "<query>" --squeeze-threshold N  Min reduction %% to prompt (default 30)
  ${cmd} evidence "<query>"                Build a deterministic Evidence Pack (JSON) → .context/evidence-pack.json
  ${cmd} evidence "<query>" --markdown     Emit the Markdown handoff rendering to stdout
  ${cmd} evidence "<query>" --top <n> --budget <n> --out <path>   Tune ranked files / token budget / write rendered output
  ${cmd} note "<text>"                     Append a note to the cross-session decision log
  ${cmd} note                              List recent notes (also: note --list <N>)
  ${cmd} status                            Show repo state — branch, dirty files, index freshness, notes
  ${cmd} doctor                            Diagnose config, index, freshness, coverage, MCP wiring — with fixes (--json; exits 1 on hard failure)
  ${cmd} mcp list                          List MCP clients and their config paths (--json)
  ${cmd} mcp install <client>              Wire MCP for one client (claude|cursor|windsurf|vscode|zed|codex|gemini|opencode|mcp); --global for user-level
  ${cmd} --init                            Write example config + .contextignore scaffold
  ${cmd} --help                            Show this message
  ${cmd} --version                         Show version

Strategies (set via config "strategy" key):
  "full"        Single file, all signatures. Works everywhere. (default)
  "per-module"  One .github/context-<module>.md per srcDir + thin overview.
                ~70% fewer tokens per question. No MCP needed.
  "hot-cold"    Hot (recently changed) auto-injected; cold in .github/context-cold.md
                ~90% fewer tokens. Best with MCP (Claude Code, Cursor).
                Set "hotCommits": N to control how many commits count as hot (default 10).

Adapters (v3.0+): copilot | claude | cursor | windsurf | openai | gemini | codex
  Set "adapters": ["copilot","openai","codex"] in config to write multiple adapter outputs.
  Old "outputs" config key is still accepted (maps to adapters automatically).

Config: gen-context.config.json
Ignore: .contextignore, .repomixignore
Output: .github/copilot-instructions.md (default)
`);
}

// ---------------------------------------------------------------------------
// MCP auto-registration
// ---------------------------------------------------------------------------
function _displayPath(p, cwd) {
  return p.startsWith(os.homedir()) ? '~' + p.slice(os.homedir().length) : path.relative(cwd, p);
}

function _registerMcpJson(filePath, serverEntry, cwd) {
  if (!fs.existsSync(filePath)) return;
  try {
    const raw = fs.readFileSync(filePath, 'utf8');
    const settings = JSON.parse(raw);
    if (!settings.mcpServers) settings.mcpServers = {};
    if (settings.mcpServers['sigmap']) return;
    settings.mcpServers['sigmap'] = serverEntry;
    fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
    console.warn(`[sigmap] registered MCP server in ${_displayPath(filePath, cwd)}`);
  } catch (err) {
    console.warn(`[sigmap] could not update ${_displayPath(filePath, cwd)}: ${err.message}`);
  }
}

function _registerCodexYaml(filePath, scriptPath) {
  if (!fs.existsSync(filePath)) return;
  try {
    const raw = fs.readFileSync(filePath, 'utf8');
    if (raw.includes('sigmap')) return; // already registered
    const yamlBlock = [
      '',
      'mcpServers:',
      '  sigmap:',
      `    command: node`,
      `    args:`,
      `      - ${path.resolve(scriptPath)}`,
      `      - --mcp`,
    ].join('\n');
    fs.writeFileSync(filePath, raw.trimEnd() + yamlBlock + '\n');
    console.warn('[sigmap] registered MCP server in ~/.codex/config.yaml');
  } catch (err) {
    console.warn(`[sigmap] could not update ~/.codex/config.yaml: ${err.message}`);
  }
}

function registerMcp(cwd, scriptPath) {
  const serverEntry = {
    command: 'node',
    args: [path.resolve(scriptPath), '--mcp'],
  };

  // JSON mcpServers targets: portable .mcp.json (priority), Claude, Cursor,
  // Windsurf project, Windsurf global, VS Code (GitHub Copilot 1.99+),
  // OpenCode project, OpenCode global, Gemini CLI
  const jsonTargets = [
    path.join(cwd, '.mcp.json'),
    path.join(cwd, '.claude', 'settings.json'),
    path.join(cwd, '.cursor', 'mcp.json'),
    path.join(cwd, '.windsurf', 'mcp.json'),
    path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
    path.join(cwd, '.vscode', 'mcp.json'),
    path.join(cwd, 'opencode.json'),
    path.join(os.homedir(), '.config', 'opencode', 'config.json'),
    path.join(os.homedir(), '.gemini', 'settings.json'),
  ];

  for (const target of jsonTargets) {
    _registerMcpJson(target, serverEntry, cwd);
  }

  // Zed uses context_servers (different shape from mcpServers)
  const zedSettingsPath = path.join(os.homedir(), '.config', 'zed', 'settings.json');
  if (fs.existsSync(zedSettingsPath)) {
    try {
      const raw      = fs.readFileSync(zedSettingsPath, 'utf8');
      const settings = JSON.parse(raw);
      if (!settings.context_servers) settings.context_servers = {};
      if (!settings.context_servers['sigmap']) {
        settings.context_servers['sigmap'] = {
          command: { path: 'node', args: [path.resolve(scriptPath), '--mcp'] },
        };
        fs.writeFileSync(zedSettingsPath, JSON.stringify(settings, null, 2) + '\n');
        console.warn('[sigmap] registered context server in ~/.config/zed/settings.json');
      }
    } catch (err) {
      console.warn(`[sigmap] could not update ~/.config/zed/settings.json: ${err.message}`);
    }
  }

  // Codex CLI uses YAML (~/.codex/config.yaml)
  _registerCodexYaml(path.join(os.homedir(), '.codex', 'config.yaml'), scriptPath);

  // Print manual snippets for all targets
  console.warn('[sigmap] MCP / context server config snippets:');
  console.warn('  .mcp.json (portable, recommended):');
  console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
  console.warn('  Claude / Cursor / Windsurf / VS Code / OpenCode / Gemini CLI:');
  console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
  console.warn('  Zed (~/.config/zed/settings.json):');
  console.warn(JSON.stringify({ context_servers: { sigmap: { command: { path: 'node', args: [path.resolve(scriptPath), '--mcp'] } } } }, null, 2));
  console.warn('  Codex CLI (~/.codex/config.yaml):');
  console.warn([
    'mcpServers:',
    '  sigmap:',
    `    command: node`,
    `    args:`,
    `      - ${path.resolve(scriptPath)}`,
    `      - --mcp`,
  ].join('\n'));
}

// ---------------------------------------------------------------------------
// v4.2 helpers
// ---------------------------------------------------------------------------
const MODEL_COSTS = {
  'gpt-4':             0.030,
  'gpt-4o':            0.005,
  'gpt-4o-mini':       0.000150,
  'claude-3-5-sonnet': 0.003,
  'claude-3-haiku':    0.00025,
  'claude-opus-4':     0.015,
  'gemini-1.5-pro':    0.00125,
};

function buildMiniContext(ranked, cwd) {
  const lines = ['# SigMap Query Context', `Generated: ${new Date().toISOString()}`, ''];
  for (const { file, sigs } of ranked) {
    lines.push(`## ${file}`, '```', ...sigs.slice(0, 20), '```', '');
  }
  return lines.join('\n');
}

// Surgical Context Phase 2 (v6.12.0): collapse one signature to a symbol-header
// pointer — `<declaration head>  :start-end` — dropping param lists, return types,
// and any body so the agent fetches the real lines on demand via the get_lines MCP tool.
function toIndexLine(sig) {
  if (typeof sig !== 'string') return '';
  // Anchor is `:start-end`, optionally followed by a `# doc hint` (Python extractor).
  const m = sig.match(/\s*:(\d+)-(\d+)(?:\s*#.*)?\s*$/);
  if (!m) {
    // No anchor (e.g. an indented member) — keep a trimmed head so it stays listed.
    return sig.trim().slice(0, 60);
  }
  const range = `:${m[1]}-${m[2]}`;
  let head = sig.slice(0, m.index).trim();
  const paren = head.indexOf('(');
  if (paren > 0) head = head.slice(0, paren).trim();      // drop params
  head = head.replace(/\s*[=→].*$/, '').trim();           // drop return type / assignment tail
  return head ? `${head}  ${range}` : range;
}

// Two-tier index output: emit only `file → [symbol:start-end]` headers (no bodies).
function buildIndexContext(ranked, cwd) {
  const lines = [
    '# SigMap Query Context (index mode)',
    `Generated: ${new Date().toISOString()}`,
    '> Symbol index only — fetch exact lines on demand via the `get_lines` MCP tool.',
    '',
  ];
  for (const { file, sigs } of ranked) {
    const idx = sigs.slice(0, 40).map(toIndexLine).filter(Boolean);
    lines.push(`## ${file}`, '```', ...idx, '```', '');
  }
  return lines.join('\n');
}

function computeCurrentRisk(cwd) {
  try {
    const out = __git(['diff', '--name-only', 'HEAD'], { cwd, timeout: 3000 });
    const count = out.trim().split('\n').filter(Boolean).length;
    if (count === 0) return 'NONE';
    if (count <= 3)  return 'LOW';
    if (count <= 10) return 'MEDIUM';
    return 'HIGH';
  } catch (_) { return 'UNKNOWN'; }
}

function getRawTokenCount(cwd, config) {
  let total = 0;
  const files = buildFileList(cwd, config);
  for (const fp of files) {
    try { total += estimateTokens(fs.readFileSync(fp, 'utf8')); } catch (_) {}
  }
  return total;
}

function getIntentWeights(intent) {
  const { DEFAULT_WEIGHTS } = requireSourceOrBundled('./src/retrieval/ranker');
  const base = Object.assign({}, DEFAULT_WEIGHTS);
  if (intent === 'debug')    return Object.assign({}, base, { recencyBoost: base.recencyBoost * 1.5 });
  if (intent === 'explain')  return Object.assign({}, base, { symbolMatch:  base.symbolMatch  * 1.5 });
  if (intent === 'refactor') return Object.assign({}, base, { pathMatch:    base.pathMatch    * 1.5 });
  if (intent === 'review')   return Object.assign({}, base, { exactToken:   base.exactToken   * 1.3 });
  return base;
}

function extractQuerySymbols(query) {
  return (query.match(/\b[A-Z][a-zA-Z]+|[a-z]+(?:[A-Z][a-z]+)+\b/g) || []);
}

function collectLearnFiles(args, flag, cwd) {
  const idx = args.indexOf(flag);
  if (idx < 0) return { rawCount: 0, files: [], warnings: [] };

  const { normalizeFile } = requireSourceOrBundled('./src/learning/weights');
  const seen = new Set();
  const files = [];
  const warnings = [];
  let rawCount = 0;

  for (let i = idx + 1; i < args.length; i++) {
    const value = args[i];
    if (!value || value.startsWith('--')) break;
    rawCount++;
    const normalized = normalizeFile(cwd, value);
    if (!normalized) {
      warnings.push(`${value} is outside the repo`);
      continue;
    }
    if (!fs.existsSync(path.join(cwd, normalized))) {
      warnings.push(`${normalized} does not exist`);
      continue;
    }
    if (!seen.has(normalized)) {
      seen.add(normalized);
      files.push(normalized);
    }
  }

  return { rawCount, files, warnings };
}

function main() {
  const args = process.argv.slice(2);

  // Feature 1: `sigmap run` alias — strip positional 'run' so existing logic handles it
  if (args[0] === 'run' && !args[0].startsWith('--')) {
    args.shift();
  }

  const invokedFrom = process.cwd();

  // --cwd <dir>: restrict scanning to that directory instead of the project root
  const cwdFlagIdx = args.indexOf('--cwd');
  const cwdFlag = cwdFlagIdx !== -1 ? (args[cwdFlagIdx + 1] || '').trim() : null;
  if (cwdFlag !== null && (!cwdFlag || cwdFlag.startsWith('--'))) {
    console.error('[sigmap] --cwd requires a directory path');
    process.exit(1);
  }
  const cwd = cwdFlag
    ? path.resolve(invokedFrom, cwdFlag)
    : resolveProjectRoot(invokedFrom);
  // v7.0.1: remember the invocation root so the generation star-nudge always tracks
  // usage at the repo root, even when runGenerate is called per-package (monorepo).
  global.__sigmapRootCwd = cwd;
  const scriptPath = process.argv[1] || path.join(invokedFrom, 'gen-context.js');

  if (cwdFlag) {
    if (!fs.existsSync(cwd)) {
      console.error(`[sigmap] --cwd directory does not exist: ${cwd}`);
      process.exit(1);
    }
    if (!fs.statSync(cwd).isDirectory()) {
      console.error(`[sigmap] --cwd must be a directory, not a file: ${cwd}`);
      process.exit(1);
    }
    console.warn(`[sigmap] --cwd: restricting scan to ${cwd}`);
  } else if (cwd !== invokedFrom) {
    console.warn(`[sigmap] using project root: ${cwd}`);
  }

  if (args.includes('--help') || args.includes('-h')) {
    printHelp(detectInvokedAs());
    process.exit(0);
  }

  if (args.includes('--version') || args.includes('-v')) {
    console.log(VERSION);
    process.exit(0);
  }

  // MCP server — start before loading config (reads files on demand)
  if (args.includes('--mcp')) {
    const { start } = __require('./src/mcp/server');
    start(cwd);
    return; // keep process alive — server drives lifecycle via stdin
  }

  const config = loadConfig(cwd);

  // --cwd restricts scanning: override srcDirs so only the given directory is scanned
  if (cwdFlag) {
    config.srcDirs = ['.'];
  }

  // --coverage: enable test coverage annotation without editing config
  if (args.includes('--coverage')) {
    config.testCoverage = true;
  }

  // ── --output <file> — parse early so every subsequent block can use it ─────
  // Resolves the custom output path and merges it into config.customOutput.
  // Also persists the resolved relative path to gen-context.config.json so
  // future --query calls (without --output) find the file automatically.
  (function resolveOutputFlag() {
    const outIdx = args.indexOf('--output');
    if (outIdx < 0) return;
    const raw = (args[outIdx + 1] || '').trim();
    if (!raw || raw.startsWith('--')) {
      console.error('[sigmap] --output requires a file path');
      console.error('  Example: node gen-context.js --output .context/ai-context.md');
      process.exit(1);
    }
    const abs  = path.resolve(cwd, raw);
    const rel  = path.relative(cwd, abs);
    config.customOutput = rel; // consumed by runGenerate

    // Persist to gen-context.config.json for future --query calls
    const cfgPath = path.join(cwd, 'gen-context.config.json');
    try {
      let savedCfg = {};
      if (fs.existsSync(cfgPath)) {
        try { savedCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch (_) {}
      }
      if (savedCfg.customOutput !== rel) {
        savedCfg.customOutput = rel;
        fs.writeFileSync(cfgPath, JSON.stringify(savedCfg, null, 2) + '\n', 'utf8');
      }
    } catch (err) {
      console.warn(`[sigmap] could not persist customOutput to config: ${err.message}`);
    }
  })();

  // Feature 2: `--mode fast|full|both`
  const modeIdx = args.indexOf('--mode');
  const mode = modeIdx !== -1
    ? args[modeIdx + 1]
    : (config.mode || 'default');
  const VALID_MODES = ['default', 'fast', 'full', 'both', 'index'];
  if (mode && !VALID_MODES.includes(mode)) {
    console.error(`[sigmap] unknown --mode "${mode}". Valid: ${VALID_MODES.join(', ')}`);
    process.exit(1);
  }

  // v4.2: `sigmap ask "<query>"` — unified pipeline
  if (args[0] === 'ask') {
    const __askT0 = Date.now();
    // v6.8: Handle --followup flag which may appear before or after query
    let query = args[1];
    if (query === '--followup' && args[2]) {
      query = args[2];
    } else if (query && query.startsWith('--') && query !== '--json' && query !== '--model') {
      // Allow --json and --model but not other flags as query
      console.error('[sigmap] Usage: sigmap ask "<query>" [--followup] [--json] [--model <name>]');
      console.error('  Example: sigmap ask "fix the login bug" --followup');
      process.exit(1);
    }
    if (!query || query.startsWith('--')) {
      console.error('[sigmap] Usage: sigmap ask "<query>" [--followup] [--json] [--model <name>]');
      console.error('  Example: sigmap ask "fix the login bug" --followup');
      process.exit(1);
    }

    const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
    const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
    const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
    const { detectWorkspaces, inferPackage, scopeToPackage } = requireSourceOrBundled('./src/workspace/detector');

    let intent = detectIntent(query);
    let intentWeights = getIntentWeights(intent);

    const sigIndex = buildSigIndex(cwd);
    if (sigIndex.size === 0) {
      console.error('[sigmap] no context file found. Run: sigmap  (to generate first)');
      process.exit(1);
    }

    // v7.0.0: Squeeze — classify and minimize pasted stacktrace/CI-log/JSON
    // input before ranking. Always runs silently; only prompts (interactive
    // TTY) when the reduction clears the threshold. Never blocks pipes/CI.
    let squeezeOffered = false;
    let squeezeAccepted = false;
    if (!args.includes('--no-squeeze')) {
      try {
        const { squeeze: runSqueeze, shouldPrompt, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
        const sq = runSqueeze(query, { srcDirs: config.srcDirs, symbolIndex: sigIndex });
        if (sq.applies && sq.reduction > 0) {
          squeezeOffered = true;
          const thrIdx = args.indexOf('--squeeze-threshold');
          const threshold = thrIdx !== -1 ? parseFloat(args[thrIdx + 1]) : 30;
          const auto = args.includes('--squeeze');
          const interactive = !!(process.stdin.isTTY && process.stderr.isTTY) && !args.includes('--json');
          let accept = false;
          if (auto) {
            accept = true;
          } else if (interactive && shouldPrompt(sq.reduction, threshold)) {
            process.stderr.write('\n' + formatSummary(sq) + '\n\n');
            process.stderr.write('Proceed with minimized input? [Y/n] ');
            const buf = Buffer.alloc(8);
            try {
              const n = fs.readSync(0, buf, 0, 8, null);
              const ans = buf.toString('utf8', 0, n).trim().toLowerCase();
              accept = ans === '' || ans === 'y' || ans === 'yes';
            } catch (_) { accept = false; }
          }
          if (accept) {
            query = sq.squeezed;
            intent = detectIntent(query);
            intentWeights = getIntentWeights(intent);
            squeezeAccepted = true;
          }
        }
      } catch (_) { /* squeeze is best-effort — never break ask */ }
    }

    let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });

    // v6.10: Workspace scoping — infer package from query and apply boost
    const workspaces = detectWorkspaces(cwd);
    const packageFlag = args[args.indexOf('--package') + 1];
    const globalFlag = args.includes('--global');

    let inferredPackage = null;
    if (!globalFlag && workspaces.length > 0) {
      if (packageFlag) {
        inferredPackage = workspaces.find(d => path.basename(d) === packageFlag) || null;
        if (!packageFlag || !inferredPackage) {
          process.stderr.write(`[sigmap] ⚠  package "${packageFlag}" not found — searching entire repo\n`);
        }
      } else {
        inferredPackage = inferPackage(query, workspaces, cwd);
      }
    }

    if (inferredPackage) {
      ranked = ranked.map(r => ({
        ...r,
        score: r.score + scopeToPackage(r.file, inferredPackage),
      })).sort((a, b) => b.score - a.score);

      if (!args.includes('--json') && !globalFlag) {
        process.stderr.write(`[sigmap] 📦 package scope: ${path.relative(cwd, inferredPackage)}\n`);
      }
    }

    // v6.8: --followup support — carry session context forward
    const isFollowup = args.includes('--followup');
    const session = isFollowup ? loadSession(cwd) : null;
    if (session) {
      ranked = mergeSessionContext(ranked, session, intent);
      ranked.sort((a, b) => b.score - a.score);
      if (!args.includes('--json')) {
        process.stderr.write(`[sigmap] ♻  followup — carrying context from: "${session.lastQuery.slice(0, 60)}"\n`);
      }
    }

    // v6.12: Delta context — restrict to files changed since a git ref (--since <ref>).
    const sinceIdx = args.indexOf('--since');
    if (sinceIdx !== -1 && args[sinceIdx + 1]) {
      const baseRef = args[sinceIdx + 1];
      const changed = getFilesChangedSinceBase(cwd, baseRef);
      const before = ranked.length;
      ranked = ranked.filter((r) => changed.has(path.resolve(cwd, r.file)));
      if (!args.includes('--json')) {
        process.stderr.write(`[sigmap] Δ since ${baseRef}: ${ranked.length}/${before} ranked files changed\n`);
      }
    }

    // v6.8: Save session for future --followup calls
    saveSession(cwd, { intent, topFiles: ranked.slice(0, 5).map(r => ({ file: r.file, score: r.score })), query });
    // v6.12: Two-tier output — `--mode index` emits symbol pointers only (bodies via get_lines).
    const indexMode = mode === 'index' || args.includes('--index');
    const miniCtx = indexMode ? buildIndexContext(ranked, cwd) : buildMiniContext(ranked, cwd);
    const outPath = path.join(cwd, '.context', 'query-context.md');
    fs.mkdirSync(path.dirname(outPath), { recursive: true });
    fs.writeFileSync(outPath, miniCtx, 'utf8');
    const ctxTok = estimateTokens(miniCtx);

    const allFiles = buildFileList(cwd, config);
    const fakeEntries = allFiles.map((f) => ({ filePath: f }));
    let coveragePct = 0;
    try { coveragePct = coverageScore(cwd, fakeEntries, config).score; } catch (_) {}

    // Realistic baseline: the full content of the files SigMap actually surfaced
    // for this query. Without SigMap you'd read these files in full; SigMap gives
    // you their signatures instead — so this is the true per-query saving. Falls
    // back to the whole-repo count only if nothing ranked.
    let rawTok = 0;
    for (const r of ranked) {
      try { rawTok += estimateTokens(fs.readFileSync(path.join(cwd, r.file), 'utf8')); } catch (_) {}
    }
    if (rawTok === 0) rawTok = getRawTokenCount(cwd, config);
    const savings = rawTok > 0 ? Math.round((1 - ctxTok / rawTok) * 100) : 0;
    const model = args[args.indexOf('--model') + 1] || 'gpt-4o';
    const rateK = MODEL_COSTS[model] || MODEL_COSTS['gpt-4o'];
    const costRaw = ((rawTok / 1000) * rateK).toFixed(4);
    const costCtx = ((ctxTok / 1000) * rateK).toFixed(4);

    const riskLevel = computeCurrentRisk(cwd);

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify({
        intent, coverage: coveragePct, contextTokens: ctxTok,
        costBefore: costRaw, costAfter: costCtx, savingsPct: savings,
        riskLevel, contextPath: path.relative(cwd, outPath),
      }) + '\n');
    } else {
      if (coveragePct < 70) {
        process.stderr.write(`[sigmap] ⚠  coverage ${coveragePct}% — consider running: sigmap validate\n`);
      }
      const bar = '─'.repeat(44);
      console.log([
        bar,
        ` sigmap ask  "${query}"`,
        ` Intent    : ${intent}`,
        ` Context   : ${ctxTok.toLocaleString()} tokens  →  ${path.relative(cwd, outPath)}`,
        ` Coverage  : ${coveragePct}%`,
        ` Risk      : ${riskLevel}`,
        ` Cost      : $${costCtx}/query  (was $${costRaw} · saved ${savings}%)`,
        bar,
      ].join('\n'));
    }
    // gain: capture this query's savings for the `sigmap gain` dashboard.
    try {
      const { recordUsage, isTrackingEnabled } = requireSourceOrBundled('./src/tracking/logger');
      if (isTrackingEnabled(config, args)) {
        recordUsage({
          version: VERSION,
          op: 'ask',
          baselineTokens: rawTok,
          actualTokens: ctxTok,
          durationMs: Date.now() - __askT0,
          model,
        }, cwd);
      }
    } catch (_) { /* gain capture is best-effort */ }

    // v7.0.0: record the run and show the one-time star nudge (interactive only).
    try {
      const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
      const showNudge = !!process.stderr.isTTY && !args.includes('--json');
      checkStarNudge(cwd, true, {
        silent: !showNudge,
        bump: { squeezeOffered: squeezeOffered ? 1 : 0, squeezeAccepted: squeezeAccepted ? 1 : 0 },
      });
    } catch (_) {}
    process.exit(0);
  }

  // `sigmap evidence "<query>"` — Evidence Pack v1 (v8.0 E1).
  // Deterministic, machine-consumable signature+evidence map. Always writes the
  // JSON artifact to .context/evidence-pack.json; stdout carries the requested
  // mode (JSON default, or Markdown handoff with --markdown/--md).
  if (args[0] === 'evidence') {
    const query = args[1];
    if (!query || query.startsWith('--')) {
      console.error('[sigmap] Usage: sigmap evidence "<query>" [--markdown] [--top <n>] [--budget <n>] [--out <path>]');
      console.error('  Example: sigmap evidence "how does auth work" --markdown');
      process.exit(1);
    }

    const { buildEvidencePack, formatJSON, formatMarkdown } = requireSourceOrBundled('./src/evidence/pack');

    const opts = {};
    const topIdx = args.indexOf('--top');
    if (topIdx !== -1 && args[topIdx + 1]) opts.top = parseInt(args[topIdx + 1], 10);
    const budgetIdx = args.indexOf('--budget');
    if (budgetIdx !== -1 && args[budgetIdx + 1]) opts.budget = parseInt(args[budgetIdx + 1], 10);
    else opts.budget = (config && config.maxTokens) || 6000;

    let pack;
    try {
      pack = buildEvidencePack(query, cwd, opts);
    } catch (e) {
      console.error('[sigmap] evidence: ' + e.message);
      process.exit(1);
    }

    if (pack.files.length === 0) {
      process.stderr.write('[sigmap] ⚠  no matching files indexed. Run: sigmap  (to generate context first)\n');
    }

    const jsonText = formatJSON(pack);
    const artifactPath = path.join(cwd, '.context', 'evidence-pack.json');
    try {
      fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
      fs.writeFileSync(artifactPath, jsonText, 'utf8');
      process.stderr.write(`[sigmap] evidence pack → ${path.relative(cwd, artifactPath)} (${pack.files.length} files, ${pack.grounding.symbolCount} symbols)\n`);
    } catch (_) { /* artifact write is best-effort */ }

    const markdown = args.includes('--markdown') || args.includes('--md');
    const rendered = markdown ? formatMarkdown(pack) : jsonText;

    const outIdx = args.indexOf('--out');
    if (outIdx !== -1 && args[outIdx + 1]) {
      const outPath = path.resolve(cwd, args[outIdx + 1]);
      try {
        fs.mkdirSync(path.dirname(outPath), { recursive: true });
        fs.writeFileSync(outPath, rendered + '\n', 'utf8');
      } catch (e) {
        console.error('[sigmap] evidence: could not write --out ' + outPath + ': ' + e.message);
        process.exit(1);
      }
    }

    process.stdout.write(rendered + '\n');
    process.exit(0);
  }

  // `sigmap gain` — token-savings dashboard (totals, by-operation, trends).
  if (args[0] === 'gain') {
    const valOf = (f, d) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
    if (args.includes('--reset')) {
      try {
        const { GAIN_FILE } = requireSourceOrBundled('./src/tracking/logger');
        const p = path.join(cwd, GAIN_FILE);
        if (fs.existsSync(p)) fs.unlinkSync(p);
        console.log('[sigmap] gain: usage log cleared.');
      } catch (e) { console.error(`[sigmap] gain: ${e.message}`); }
      process.exit(0);
    }
    try {
      const { readGainLog } = requireSourceOrBundled('./src/tracking/logger');
      const { aggregate } = requireSourceOrBundled('./src/tracking/aggregate');
      const { renderSummary, renderBreakdown } = requireSourceOrBundled('./src/format/gain-terminal');
      const records = readGainLog(cwd);
      const agg = aggregate(records, {
        model: valOf('--model', 'claude-sonnet'),
        since: valOf('--since', null),
        top: parseInt(valOf('--top', args.includes('--all') ? '0' : '10'), 10),
      });
      if (args.includes('--json')) {
        process.stdout.write(JSON.stringify(agg, null, 2) + '\n');
      } else {
        const out = args.includes('--all')
          ? renderSummary(agg, { version: VERSION }) + '\n' + renderBreakdown(agg)
          : renderSummary(agg, { version: VERSION });
        process.stdout.write(out + '\n');
      }
    } catch (e) {
      console.error(`[sigmap] gain: ${e.message}`);
      console.error('  (gain modules require src/ — for single-file installs, regenerate the bundle)');
      process.exit(1);
    }
    process.exit(0);
  }

  // v4.2: `sigmap suggest-profile` — auto-detect task type from git state
  if (args[0] === 'suggest-profile') {
    const short = args.includes('--short');
    let msg = '', diff = '';
    try {
      msg  = __git(['log', '-1', '--format=%s'],        { cwd, timeout: 3000 }).trim();
      diff = __git(['diff', '--cached', '--name-only'], { cwd, timeout: 3000 });
    } catch (_) {}

    let profile = 'default';
    let reason  = 'no strong signal in git state';
    if      (/fix|bug|error|crash|exception/i.test(msg))         { profile = 'debug';        reason = `commit: "${msg.slice(0, 60)}"`;  }
    else if (/refactor|architect|redesign|module/i.test(msg))    { profile = 'architecture'; reason = `commit: "${msg.slice(0, 60)}"`;  }
    else if (/review|pr|pull.request|check/i.test(msg))          { profile = 'review';       reason = `commit: "${msg.slice(0, 60)}"`;  }
    else if (diff.includes('.spec.') || diff.includes('.test.')) { profile = 'debug';        reason = 'staged test files detected'; }

    if (short) {
      console.log(profile);
    } else {
      console.log(`[sigmap] suggested profile: --profile ${profile}`);
      console.log(`  Reason: ${reason}`);
    }
    process.exit(0);
  }

  // v4.2: `sigmap compare` — human-readable benchmark CLI
  if (args[0] === 'compare') {
    const { execFileSync } = require('child_process');
    console.log('[sigmap] Running comparison benchmark (this may take ~30s)...\n');

    let raw = '';
    try {
      // Shell-free: run the node binary directly with the script path as an argv.
      raw = execFileSync(
        process.execPath,
        [path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'), '--compare'],
        { cwd, timeout: 90_000, encoding: 'utf8' }
      );
    } catch (e) { raw = (e && e.stdout) ? e.stdout : ''; }

    let results = null;
    try { results = JSON.parse(raw); } catch (_) {}

    if (!results) {
      console.error('[sigmap] Could not parse benchmark output.');
      console.error('  Run manually: node scripts/run-retrieval-benchmark.mjs --skip-run --json');
      process.exit(1);
    }

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(results, null, 2) + '\n');
    } else {
      const pct  = (v) => `${(v * 100).toFixed(1)}%`;
      const lift = (a, b) => (b > 0 ? (a / b).toFixed(1) : '∞');
      const bar  = '─'.repeat(44);
      console.log([
        bar,
        ' SigMap vs Baseline',
        bar,
        ` hit@5         ${pct(results.sigmap.hitAt5)} vs ${pct(results.baseline.hitAt5)}   (${lift(results.sigmap.hitAt5, results.baseline.hitAt5)}× lift)`,
        ` Avg tokens    ${results.sigmap.tokens.toLocaleString()} vs ${results.baseline.tokens.toLocaleString()}`,
        bar,
      ].join('\n'));
    }
    process.exit(0);
  }

  // v4.2: `sigmap share` — shareable one-liner with live benchmark numbers
  if (args[0] === 'share') {
    const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
    let reduction = 97, hitAt5 = 88;

    if (fs.existsSync(histPath)) {
      try {
        const entries = fs.readFileSync(histPath, 'utf8').trim().split('\n')
          .map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
        const tok = [...entries].reverse().find((e) => e.type === 'token-reduction');
        const ret = [...entries].reverse().find((e) => e.type === 'retrieval');
        if (tok && tok.reduction) reduction = tok.reduction;
        if (ret && ret.hitAt5)   hitAt5    = Math.round(ret.hitAt5 * 100);
      } catch (_) {}
    }

    const shareText = [
      'Generated with SigMap — the deterministic, verifiable grounding layer for AI code work',
      `${reduction}% fewer tokens · ${hitAt5}% retrieval accuracy · 6× better results`,
      'https://sigmap.io',
    ].join('\n');

    console.log(shareText);

    try {
      // Shell-free: spawn the clipboard binary directly and write the text to its
      // stdin — no shell, no pipe, no string-built command.
      const { execFileSync } = require('child_process');
      const [clipBin, clipArgs] = process.platform === 'darwin'
        ? ['pbcopy', []]
        : ['xclip', ['-selection', 'clipboard']];
      execFileSync(clipBin, clipArgs, { input: shareText, timeout: 2000 });
      console.log('\n[sigmap] Copied to clipboard.');
    } catch (_) {}

    process.exit(0);
  }

  // v6.8: `sigmap plan "<goal>"` — analyze what files need changes and impact radius
  if (args[0] === 'plan') {
    const goal = args[1];
    if (!goal || goal.startsWith('--')) {
      console.error('[sigmap] Usage: sigmap plan "<goal>"');
      console.error('  Example: sigmap plan "add rate limiting to the API"');
      process.exit(1);
    }

    const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');

    const intent = detectIntent(goal);
    const intentWeights = getIntentWeights(intent);

    const sigIndex = buildSigIndex(cwd);
    if (sigIndex.size === 0) {
      console.error('[sigmap] no context file found. Run: sigmap (to generate first)');
      process.exit(1);
    }

    const ranked = rank(goal, sigIndex, { topK: 15, weights: intentWeights, cwd });

    // Separate into confidence levels
    const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
    const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);

    // Compute impact radius (simplified — no graph for now)
    let impact = null;

    // Identify likely-affected tests (simplified — checks for .test. or .spec. in filename)
    const testedFiles = highConf.filter(r => /\.(test|spec)\.(js|ts|py)$|_test\.(js|ts|py)$/.test(r.file));

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify({
        goal, intent,
        inspectFirst: highConf.map(r => r.file),
        likelyToChange: medConf.map(r => r.file),
        impactRadius: impact,
        testsAffected: testedFiles.map(r => r.file),
      }, null, 2) + '\n');
    } else {
      const bar = '─'.repeat(50);
      console.log(bar);
      console.log(` sigmap plan  "${goal}"`);
      console.log(` Intent     : ${intent}`);
      console.log(bar);
      console.log('');
      console.log(' Inspect first (highest relevance):');
      if (highConf.length === 0) {
        console.log('   (no files found)');
      } else {
        highConf.forEach((r, i) => console.log(`   ${i + 1}. ${path.relative(cwd, r.file)}`));
      }
      console.log('');
      console.log(' Likely to change:');
      if (medConf.length === 0) {
        console.log('   (no files found)');
      } else {
        medConf.forEach((r, i) => console.log(`   ${i + 1}. ${path.relative(cwd, r.file)}`));
      }
      if (testedFiles.length > 0) {
        console.log('');
        console.log(' Tests to run after change:');
        testedFiles.forEach(r => console.log(`   • ${path.relative(cwd, r.file)}`));
      }
      console.log('');
      console.log(bar);
    }
    process.exit(0);
  }

  // v5.9: `sigmap bench --submit` — format local benchmark history as a shareable community block
  if (args[0] === 'bench' && args.includes('--submit')) {
    const versionMeta = (() => {
      try { return JSON.parse(fs.readFileSync(path.join(__dirname, 'version.json'), 'utf8')); }
      catch (_) { return {}; }
    })();

    const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
    let localMetrics = null;
    if (fs.existsSync(histPath)) {
      try {
        const entries = fs.readFileSync(histPath, 'utf8').trim().split('\n')
          .map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
        const ret = [...entries].reverse().find((e) => e.type === 'retrieval');
        const tok = [...entries].reverse().find((e) => e.type === 'token-reduction');
        if (ret || tok) {
          localMetrics = {
            hitAt5Pct:       ret ? (ret.hitAt5Pct || Math.round((ret.hitAt5 || 0) * 100)) : null,
            reductionPct:    tok ? (tok.reduction || tok.avgReductionPct || null) : null,
            runDate:         (ret || tok).ts ? new Date((ret || tok).ts).toISOString().slice(0, 10) : null,
          };
        }
      } catch (_) {}
    }

    const canonical = versionMeta.metrics || {};
    const submission = {
      sigmapVersion:     versionMeta.version || 'unknown',
      benchmarkId:       versionMeta.benchmark_id || 'unknown',
      canonicalHitAt5:   canonical.hit_at_5 != null ? Math.round(canonical.hit_at_5 * 1000) / 10 : null,
      canonicalReduction: canonical.overall_token_reduction_pct || null,
      local:             localMetrics,
      submittedAt:       new Date().toISOString().slice(0, 10),
    };

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(submission, null, 2) + '\n');
    } else {
      const bar = '─'.repeat(56);
      const lines = [
        bar,
        ' SigMap Community Benchmark Submission',
        bar,
        ` SigMap version : ${submission.sigmapVersion}`,
        ` Benchmark ID   : ${submission.benchmarkId}`,
        ` Submitted      : ${submission.submittedAt}`,
        bar,
        ' Canonical metrics (official release):',
        ` hit@5          : ${submission.canonicalHitAt5 != null ? submission.canonicalHitAt5 + '%' : 'n/a'}`,
        ` token reduction: ${submission.canonicalReduction != null ? submission.canonicalReduction + '%' : 'n/a'}`,
      ];
      if (localMetrics) {
        lines.push(bar, ' Local run metrics (this repo):');
        if (localMetrics.hitAt5Pct != null)    lines.push(` hit@5          : ${localMetrics.hitAt5Pct}%`);
        if (localMetrics.reductionPct != null)  lines.push(` token reduction: ${localMetrics.reductionPct}%`);
        if (localMetrics.runDate)               lines.push(` run date       : ${localMetrics.runDate}`);
      } else {
        lines.push(bar, ' Local run metrics: none yet — run node scripts/run-retrieval-benchmark.mjs');
      }
      lines.push(bar);
      lines.push(' Paste the block above into a GitHub Discussion to share your results.');
      lines.push(' https://github.com/manojmallick/sigmap/discussions');
      lines.push(bar);
      console.log(lines.join('\n'));
    }
    process.exit(0);
  }

  // v5.2: `sigmap learn` — manual learning controls for ranking
  if (args[0] === 'learn') {
    const doReset = args.includes('--reset');
    const good = collectLearnFiles(args, '--good', cwd);
    const bad = collectLearnFiles(args, '--bad', cwd);

    if (doReset && (good.rawCount > 0 || bad.rawCount > 0)) {
      console.error('[sigmap] --reset cannot be combined with --good or --bad');
      process.exit(1);
    }

    if (doReset) {
      const { resetWeights } = requireSourceOrBundled('./src/learning/weights');
      resetWeights(cwd);
      console.log('[sigmap] weights reset — all files back to baseline');
      process.exit(0);
    }

    if (good.rawCount === 0 && bad.rawCount === 0) {
      console.error('[sigmap] Usage: sigmap learn --good <files...> [--bad <files...>] | sigmap learn --reset');
      process.exit(1);
    }

    for (const warning of [...good.warnings, ...bad.warnings]) {
      console.warn(`[sigmap] warning: ${warning}`);
    }

    if (good.files.length === 0 && bad.files.length === 0) {
      console.error('[sigmap] No valid files to learn from.');
      process.exit(1);
    }

    const { updateWeights } = requireSourceOrBundled('./src/learning/weights');
    const result = updateWeights(cwd, { goodFiles: good.files, badFiles: bad.files });
    const parts = [];
    if (result.good.length) parts.push(`boosted ${result.good.length} file(s)`);
    if (result.bad.length) parts.push(`penalized ${result.bad.length} file(s)`);
    console.log(`[sigmap] learned: ${parts.join(', ')}`);
    process.exit(0);
  }

  // v5.2: `sigmap weights` — explain learned ranking multipliers
  if (args[0] === 'weights') {
    const { loadWeights, exportWeights, importWeights } = requireSourceOrBundled('./src/learning/weights');

    if (args.includes('--export')) {
      const exportIdx = args.indexOf('--export');
      const maybeFile = args[exportIdx + 1];
      const exportFile = (maybeFile && !maybeFile.startsWith('--')) ? maybeFile : null;
      exportWeights(cwd, exportFile);
      if (exportFile) console.warn(`[sigmap] weights exported to ${exportFile}`);
      process.exit(0);
    }

    if (args.includes('--import')) {
      const importIdx = args.indexOf('--import');
      const importFile = args[importIdx + 1];
      if (!importFile || importFile.startsWith('--')) {
        console.error('[sigmap] --import requires a file path');
        process.exit(1);
      }
      const replace = args.includes('--replace');
      try {
        const result = importWeights(cwd, importFile, replace);
        const count = Object.keys(result).length;
        console.warn(`[sigmap] weights ${replace ? 'replaced' : 'merged'} from ${importFile} — ${count} file(s) with non-baseline weights`);
      } catch (err) {
        console.error(`[sigmap] weights import failed: ${err.message}`);
        process.exit(1);
      }
      process.exit(0);
    }

    const weights = loadWeights(cwd);
    const entries = Object.entries(weights).sort(([, a], [, b]) => b - a || 0);

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(weights, null, 2) + '\n');
      process.exit(0);
    }

    if (entries.length === 0) {
      console.log('[sigmap] No learned weights yet. Run: sigmap learn --good <file>');
      process.exit(0);
    }

    console.log('[sigmap] Learned file weights (xmultiplier vs baseline):');
    for (const [file, mult] of entries) {
      const bar = mult >= 1
        ? `+${'█'.repeat(Math.max(1, Math.round((mult - 1) * 10)))}`
        : `-${'░'.repeat(Math.max(1, Math.round((1 - mult) * 10)))}`;
      console.log(`  ${file.padEnd(50)} x${mult.toFixed(2)}  ${bar}`);
    }
    console.log(`\n  Total files with learned weights: ${entries.length}`);
    console.log('  To reset: sigmap learn --reset');
    process.exit(0);
  }

  // v4.3: `sigmap validate` — config + coverage + optional query symbol check
  if (args[0] === 'validate') {
    const issues   = [];
    const warnings = [];

    // Config checks
    for (const d of (config.srcDirs || [])) {
      if (!fs.existsSync(path.join(cwd, d)))
        issues.push(`srcDir '${d}' does not exist`);
    }
    if ((config.exclude || []).some((p) => p === 'src/**'))
      issues.push(`exclude pattern 'src/**' will exclude all source files`);
    if ((config.maxTokens || 0) < 1000)
      warnings.push(`maxTokens ${config.maxTokens} is very low — consider ≥ 4000`);
    if ((config.maxTokens || 0) > 50000)
      warnings.push(`maxTokens ${config.maxTokens} is very high — may exceed LLM context windows`);

    // Coverage check: files actually in context vs total source files
    const { buildSigIndex: valBuildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
    const valSigIndex  = valBuildSigIndex(cwd);
    const valTotal     = buildFileList(cwd, config).length;
    const coveragePct  = valTotal > 0 ? Math.round((valSigIndex.size / valTotal) * 100) : 0;
    if (coveragePct < 70)
      warnings.push(`coverage ${coveragePct}% is below recommended 70% — increase maxTokens or expand srcDirs`);

    // Optional query symbol check
    const valQueryIdx = args.indexOf('--query');
    if (valQueryIdx !== -1) {
      const q = (args[valQueryIdx + 1] || '').trim();
      if (q && !q.startsWith('--')) {
        try {
          const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
          const ranked = rank(q, buildSigIndex(cwd), { topK: 5, cwd });
          const symbols = extractQuerySymbols(q);
          const missing = symbols.filter((sym) =>
            !ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
          );
          if (missing.length > 0)
            warnings.push(`query "${q}" references symbols not in top-5 context: ${missing.join(', ')}`);
          else if (symbols.length > 0)
            console.log(`[sigmap] ✓ query coverage OK — all ${symbols.length} symbols found`);
        } catch (_) {}
      }
    }

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify({ valid: issues.length === 0, issues, warnings, coverage: coveragePct }) + '\n');
    } else {
      for (const w of warnings) console.warn(`[sigmap] ⚠  ${w}`);
      if (issues.length === 0) {
        console.log(`[sigmap] ✓ config valid  coverage: ${coveragePct}%`);
      } else {
        for (const iss of issues) console.error(`[sigmap] ✗ ${iss}`);
        process.exit(1);
      }
    }
    process.exit(0);
  }

  // v5.0: `sigmap judge --response <file> --context <file>` — groundedness scoring
  if (args[0] === 'judge') {
    const respIdx = args.indexOf('--response');
    const ctxIdx  = args.indexOf('--context');

    if (respIdx < 0 || ctxIdx < 0) {
      console.error('[sigmap] Usage: sigmap judge --response <file> --context <file> [--json] [--threshold 0.25] [--learn]');
      process.exit(1);
    }

    const respFile = (args[respIdx + 1] || '').trim();
    const ctxFile  = (args[ctxIdx + 1]  || '').trim();

    if (!respFile || respFile.startsWith('--') || !ctxFile || ctxFile.startsWith('--')) {
      console.error('[sigmap] --response and --context require file paths');
      process.exit(1);
    }

    let responseText = '', contextText = '';
    try { responseText = fs.readFileSync(path.resolve(cwd, respFile), 'utf8'); }
    catch (e) { console.error(`[sigmap] cannot read --response file: ${e.message}`); process.exit(1); }
    try { contextText = fs.readFileSync(path.resolve(cwd, ctxFile), 'utf8'); }
    catch (e) { console.error(`[sigmap] cannot read --context file: ${e.message}`); process.exit(1); }

    const thrIdx = args.indexOf('--threshold');
    const judgeOpts = thrIdx >= 0 ? { threshold: parseFloat(args[thrIdx + 1]) || 0.25 } : {};
    if (args.includes('--learn')) {
      judgeOpts.learn = true;
      judgeOpts.cwd = cwd;
    }

    const { judge: runJudge } = requireSourceOrBundled('./src/judge/judge-engine');
    const result = runJudge(responseText, contextText, judgeOpts);

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(result) + '\n');
    } else {
      const bar = '─'.repeat(44);
      console.log([
        bar,
        ` sigmap judge`,
        ` Score     : ${result.score}`,
        ` Verdict   : ${result.verdict}`,
        result.reasons.length ? ` Reasons   :\n   ${result.reasons.join('\n   ')}` : ` Reasons   : none`,
        result.learning
          ? ` Learning  : ${result.learning.applied ? result.learning.action : 'skipped'}${result.learning.files.length ? ` (${result.learning.files.join(', ')})` : ''}${result.learning.reason ? ` — ${result.learning.reason}` : ''}`
          : null,
        bar,
      ].filter(Boolean).join('\n'));
    }
    process.exit(result.verdict === 'pass' ? 0 : 1);
  }

  // v5.0: `sigmap history` — show last N usage log entries with sparkline
  if (args[0] === 'history') {
    const { readLog } = requireSourceOrBundled('./src/tracking/logger');
    const entries = readLog(cwd);

    const nIdx = args.indexOf('--last');
    const n    = nIdx >= 0 ? (parseInt(args[nIdx + 1], 10) || 10) : 10;
    const last = entries.slice(-n);

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(last) + '\n');
      process.exit(0);
    }

    const SPARK_CHARS = '▁▂▃▄▅▆▇█';
    function sparkline(values) {
      if (values.length === 0) return '';
      const min = Math.min(...values);
      const max = Math.max(...values);
      const range = max - min || 1;
      return values.map((v) => {
        const idx = Math.round(((v - min) / range) * (SPARK_CHARS.length - 1));
        return SPARK_CHARS[idx];
      }).join('');
    }

    const bar = '─'.repeat(62);
    console.log(bar);
    console.log(` sigmap history  (last ${Math.max(last.length, 1)} runs)`);
    console.log(bar);

    if (last.length === 0) {
      console.log(' No usage log entries. Enable tracking: true in config to start recording runs.');
    } else {
      console.log(` ${'Date'.padEnd(24)} ${'Files'.padStart(5)} ${'Tokens'.padStart(7)} ${'Reduction'.padStart(9)} ${'Budget?'.padStart(7)}`);
      console.log(` ${'─'.repeat(24)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(9)} ${'─'.repeat(7)}`);
      for (const e of last) {
        const date = (e.ts || '').slice(0, 19).replace('T', ' ');
        const files = String(e.fileCount || 0).padStart(5);
        const tok   = String(e.finalTokens || 0).padStart(7);
        const red   = `${e.reductionPct || 0}%`.padStart(9);
        const over  = (e.overBudget ? '  ⚠ yes' : '     no').padStart(7);
        console.log(` ${date.padEnd(24)} ${files} ${tok} ${red} ${over}`);
      }
      console.log(bar);
      const tokens = last.map((e) => e.finalTokens || 0);
      console.log(` Token trend: ${sparkline(tokens)}`);
    }

    // Show benchmark trend row if .context/benchmark-history.ndjson exists
    const benchHistPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
    if (fs.existsSync(benchHistPath)) {
      try {
        const benchEntries = fs.readFileSync(benchHistPath, 'utf8').trim().split('\n')
          .map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
        const retrieval = benchEntries.filter((e) => e.type === 'retrieval').slice(-n);
        if (retrieval.length > 0) {
          const hits = retrieval.map((e) => e.hitAt5Pct || 0);
          console.log(` hit@5 trend: ${sparkline(hits)}  ${hits.at(-1)}% (latest)`);
        }
        const tokenBench = benchEntries.filter((e) => e.type === 'token-reduction').slice(-n);
        if (tokenBench.length > 0) {
          const reds = tokenBench.map((e) => e.reduction || e.avgReductionPct || 0);
          console.log(` tok reduce : ${sparkline(reds)}  ${reds.at(-1)}% (latest)`);
        }
      } catch (_) {}
    }

    console.log(bar);
    process.exit(0);
  }

  // Feature 6: `sigmap sync` — write all outputs + llms.txt + print compact diff
  if (args[0] === 'sync') {
    try {
      // Generate copilot-instructions.md first (existing output)
      runGenerate(cwd, config, false);
      const llmTxt = requireSourceOrBundled('./src/format/llm-txt');
      const llmsGen = requireSourceOrBundled('./src/format/llms-txt');
      const llmFullMod = requireSourceOrBundled('./packages/adapters/llm-full');

      // Gather file entries for llm.txt / llm-full.txt
      const ignorePatterns = loadIgnorePatterns(cwd);
      let syncFiles = buildFileList(cwd, config).filter((f) => {
        const rel = path.relative(cwd, f).replace(/\\/g, '/');
        return !matchesIgnore(rel, ignorePatterns);
      });
      const syncEntries = [];
      for (const fp of syncFiles) {
        let content = '';
        try { content = fs.readFileSync(fp, 'utf8'); } catch (_) { continue; }
        const sigs = detectAndExtract(fp, content, config.maxSigsPerFile || 25);
        if (sigs.length === 0) continue;
        syncEntries.push({ filePath: fp, sigs, language: null });
      }
      const syncContext = {
        projectName: path.basename(cwd),
        fileEntries: syncEntries,
        srcDirs: config.srcDirs || [],
      };

      const llmTxtPath   = llmTxt.outputPath(cwd);
      const llmFullPath  = llmFullMod.outputPath(cwd);
      const llmsTxtPath  = llmsGen.outputPath(cwd);

      fs.writeFileSync(llmTxtPath,  llmTxt.format(syncContext, cwd, VERSION));
      llmFullMod.write(syncContext, cwd, { version: VERSION });

      const prev  = fs.existsSync(llmsTxtPath) ? fs.readFileSync(llmsTxtPath, 'utf8') : '';
      const next  = llmsGen.format(syncContext, cwd, [
        { path: llmTxtPath,  label: 'llm.txt' },
        { path: llmFullPath, label: 'llm-full.txt' },
      ], VERSION);
      const changed = prev !== next ? 'updated' : 'no change';
      fs.writeFileSync(llmsTxtPath, next);

      console.log('[sigmap] sync complete');
      console.log(`  .github/copilot-instructions.md  updated`);
      console.log(`  llm.txt                          updated`);
      console.log(`  llm-full.txt                     updated`);
      console.log(`  llms.txt                         ${changed}`);
    } catch (err) {
      console.error(`[sigmap] sync error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  // v6.5: `sigmap roots` — detect source roots
  if (args[0] === 'roots') {
    const { resolveSourceRoots } = requireSourceOrBundled('./src/discovery/source-root-resolver');
    const result = resolveSourceRoots(cwd);

    if (args.includes('--json')) {
      console.log(JSON.stringify(result, null, 2));
      process.exit(0);
    }

    if (args.includes('--fix')) {
      console.log('[sigmap] Current detected roots:', result.roots.join(', '));
      const readline = require('readline');
      const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
      rl.question('Enter correct srcDirs (comma-separated): ', answer => {
        const dirs = answer.split(',').map(d => d.trim()).filter(Boolean);
        const cfgPath = path.join(cwd, 'gen-context.config.json');
        const cfg = fs.existsSync(cfgPath) ? JSON.parse(fs.readFileSync(cfgPath, 'utf8')) : {};
        cfg.srcDirs = dirs;
        fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
        console.log(`[sigmap] Saved srcDirs: ${dirs.join(', ')} → gen-context.config.json`);
        rl.close();
        process.exit(0);
      });
      return;
    }

    // --explain (default)
    console.log('\nDetected languages:');
    for (const l of result.languages.slice(0, 4)) {
      console.log(`  ${l.name.padEnd(16)} ${l.weight.toFixed(2)}`);
    }
    console.log('\nDetected frameworks:');
    if (result.frameworks.length === 0) console.log('  (none)');
    for (const f of result.frameworks.slice(0, 3)) {
      console.log(`  ${f.name.padEnd(16)} ${f.confidence.toFixed(2)}`);
    }
    console.log('\nChosen source roots:');
    if (result.roots.length === 0) console.log('  (none — legacy fallback used)');
    result.roots.forEach((r, i) => {
      const exp = result.explanation?.find(e => e.dir === r);
      console.log(`  ${i + 1}. ${r.padEnd(20)} ${exp ? 'score ' + exp.score : ''}`);
    });
    console.log('\nMonorepo:', result.isMonorepo ? 'yes' : 'no');
    console.log('Confidence:', result.confidence);
    process.exit(0);
  }

  // `sigmap note "<text>"` — append to the cross-session decision log.
  // With no text, lists recent notes (also `note --list [N]`).
  if (args[0] === 'note') {
    const jsonOut = args.includes('--json');
    const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
    const listIdx = args.indexOf('--list');
    // Build positionals, skipping value-taking flags and their values
    // (e.g. `--cwd <dir>`, `--list <N>`) so they never leak into the note text.
    const VALUE_FLAGS = new Set(['--cwd', '--list']);
    const positional = [];
    for (let i = 1; i < args.length; i++) {
      const a = args[i];
      if (a.startsWith('--')) { if (VALUE_FLAGS.has(a)) i++; continue; }
      positional.push(a);
    }
    const wantsList = listIdx !== -1 || positional.length === 0;

    if (wantsList) {
      const limArg = listIdx !== -1 ? parseInt(args[listIdx + 1], 10) : parseInt(positional[0], 10);
      const limit = Number.isFinite(limArg) && limArg > 0 ? limArg : 10;
      const notes = readNotes(cwd, limit);
      if (jsonOut) {
        process.stdout.write(JSON.stringify({ notes }) + '\n');
      } else {
        console.log(`[sigmap] ${notes.length} recent note${notes.length === 1 ? '' : 's'}`);
        console.log(formatNotes(notes.slice().reverse()));
      }
      process.exit(0);
    }

    const text = positional.join(' ');
    let entry;
    try {
      entry = addNote(cwd, text);
    } catch (err) {
      console.error(`[sigmap] ${err.message}`);
      process.exit(1);
    }
    if (jsonOut) {
      process.stdout.write(JSON.stringify({ added: entry }) + '\n');
    } else {
      console.log(`[sigmap] noted${entry.branch ? ` (${entry.branch})` : ''}: ${entry.text}`);
    }
    process.exit(0);
  }

  // `sigmap status` — environment / repo state at a glance.
  if (args[0] === 'status') {
    const jsonOut = args.includes('--json');
    const gitOpts = { cwd };
    const st = { branch: null, dirty: 0, lastIndex: null, indexVersion: null, indexFiles: null, changedSinceIndex: null, notes: 0, lastNote: null };

    st.branch = __tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], gitOpts) || null;
    // Fallback for an unborn branch (fresh repo, no commits yet).
    if (!st.branch || st.branch === 'HEAD') {
      st.branch = __tryGit(['symbolic-ref', '--short', 'HEAD'], gitOpts) || st.branch;
    }
    try {
      const porcelain = __git(['status', '--porcelain'], gitOpts).trim();
      st.dirty = porcelain ? porcelain.split('\n').filter(Boolean).length : 0;
    } catch (_) {}

    try {
      const { readLog } = requireSourceOrBundled('./src/tracking/logger');
      const log = readLog(cwd);
      if (log.length) {
        const last = log[log.length - 1];
        st.lastIndex = last.ts || null;
        st.indexVersion = last.version || null;
        st.indexFiles = last.fileCount != null ? last.fileCount : null;
      }
    } catch (_) {}

    // Index freshness: count tracked files modified after the last index run.
    if (st.lastIndex) {
      try {
        const since = Date.parse(st.lastIndex);
        const tracked = __git(['ls-files'], gitOpts).split('\n').filter(Boolean);
        let changed = 0;
        for (const f of tracked.slice(0, 5000)) {
          try { if (fs.statSync(path.join(cwd, f)).mtimeMs > since) changed++; } catch (_) {}
        }
        st.changedSinceIndex = changed;
      } catch (_) {}
    }

    try {
      const { readNotes } = requireSourceOrBundled('./src/session/notes');
      const notes = readNotes(cwd);
      st.notes = notes.length;
      if (notes.length) st.lastNote = notes[notes.length - 1];
    } catch (_) {}

    if (jsonOut) {
      process.stdout.write(JSON.stringify(st) + '\n');
      process.exit(0);
    }

    const fmtAgo = (iso) => {
      if (!iso) return 'never';
      const ms = Date.now() - Date.parse(iso);
      if (!Number.isFinite(ms)) return iso;
      const m = Math.floor(ms / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
      if (d > 0) return `${d}d ago`;
      if (h > 0) return `${h}h ago`;
      if (m > 0) return `${m}m ago`;
      return 'just now';
    };

    console.log('[sigmap] status');
    console.log(`  Branch:        ${st.branch || '(not a git repo)'}`);
    console.log(`  Working tree:  ${st.dirty === 0 ? 'clean' : `${st.dirty} file${st.dirty === 1 ? '' : 's'} changed`}`);
    if (st.lastIndex) {
      let fresh = `${fmtAgo(st.lastIndex)} (v${st.indexVersion || '?'}, ${st.indexFiles != null ? st.indexFiles + ' files' : 'n/a'})`;
      if (st.changedSinceIndex != null && st.changedSinceIndex > 0) fresh += ` — STALE: ${st.changedSinceIndex} file${st.changedSinceIndex === 1 ? '' : 's'} changed since`;
      console.log(`  Last index:    ${fresh}`);
    } else {
      console.log('  Last index:    never — run: sigmap');
    }
    if (st.notes > 0) {
      console.log(`  Notes:         ${st.notes} (latest: ${st.lastNote.text.slice(0, 60)}${st.lastNote.text.length > 60 ? '…' : ''})`);
    } else {
      console.log('  Notes:         none — add with: sigmap note "<text>"');
    }
    process.exit(0);
  }

  // `sigmap mcp install <client>` / `sigmap mcp list` — one-command per-client
  // MCP wiring (v8.0 E4). Unlike `--setup` (wires every editor, only touches
  // existing configs), this targets one client and creates the config if absent.
  if (args[0] === 'mcp') {
    const { listClients, installClient } = requireSourceOrBundled('./src/mcp/install');
    const sub = args[1];

    if (sub === 'list') {
      const clients = listClients({ cwd });
      if (args.includes('--json')) {
        process.stdout.write(JSON.stringify(clients, null, 2) + '\n');
        process.exit(0);
      }
      console.log('Supported MCP clients:\n');
      for (const c of clients) {
        const scope = c.scope === 'both' ? 'project (or --global)' : c.scope;
        console.log(`  ${c.client.padEnd(10)} ${c.label}`);
        console.log(`             ${_displayPath(c.target, cwd)}  [${scope}]`);
      }
      console.log('\nInstall with: sigmap mcp install <client>');
      process.exit(0);
    }

    if (sub === 'install') {
      const client = args[2];
      if (!client || client.startsWith('--')) {
        console.error('[sigmap] usage: sigmap mcp install <client>   (see: sigmap mcp list)');
        process.exit(1);
      }
      const result = installClient(client, {
        cwd,
        scriptPath,
        global: args.includes('--global'),
      });
      if (result.status === 'unknown') {
        console.error(`[sigmap] unknown client "${client}". Valid: ${result.valid.join(', ')}`);
        process.exit(1);
      }
      if (result.status === 'already') {
        console.log(`[sigmap] ${result.label}: sigmap already registered in ${_displayPath(result.path, cwd)}`);
      } else {
        console.log(`[sigmap] ${result.label}: registered MCP server in ${_displayPath(result.path, cwd)}`);
      }
      process.exit(0);
    }

    console.error('[sigmap] usage: sigmap mcp install <client> | sigmap mcp list');
    process.exit(1);
  }

  // `sigmap doctor` — diagnose config, index, freshness, coverage, and MCP
  // wiring; print an actionable fix for anything wrong. Exit 1 on a hard
  // failure (no context file / invalid config) so it is usable in CI.
  if (args[0] === 'doctor') {
    const { diagnose, formatDoctor, formatDoctorJSON } = requireSourceOrBundled('./src/doctor/diagnose');
    const result = diagnose(cwd);
    if (args.includes('--json')) {
      process.stdout.write(formatDoctorJSON(result) + '\n');
    } else {
      console.log(formatDoctor(result));
    }
    process.exit(result.ok ? 0 : 1);
  }

  // Layer 3: `sigmap conventions` — extract & report repo coding conventions
  // (file naming, export style, test framework) for TS/JS/Python so generated
  // code matches the house style. Writes .context/conventions.json.
  if (args[0] === 'conventions') {
    const jsonOut = args.includes('--json');
    const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
    const files = buildFileList(cwd, config);
    const result = extractConventions(cwd, files);

    // `--inject`: write/update the conventions block in CLAUDE.md (idempotent).
    if (args.includes('--inject')) {
      const { renderConventionsBlock, injectConventions } = requireSourceOrBundled('./src/conventions/inject');
      const claudePath = path.join(cwd, 'CLAUDE.md');
      let existing = '';
      try { if (fs.existsSync(claudePath)) existing = fs.readFileSync(claudePath, 'utf8'); } catch (_) {}
      const block = renderConventionsBlock(result, VERSION);
      try {
        fs.writeFileSync(claudePath, injectConventions(existing, block));
      } catch (e) {
        console.error(`[sigmap] cannot write ${claudePath}: ${e.message}`);
        process.exit(1);
      }
      console.log(`[sigmap] conventions → injected block into ${path.relative(cwd, claudePath) || 'CLAUDE.md'}`);
      process.exit(0);
    }

    // `--update`: incremental rescan — refresh .context/conventions.json only when stale.
    if (args.includes('--update')) {
      const { planUpdate } = requireSourceOrBundled('./src/conventions/update');
      const outDir = path.join(cwd, '.context');
      const outPath = path.join(outDir, 'conventions.json');
      const plan = planUpdate(cwd, files, outPath);
      let wrote = false;
      if (plan.stale) {
        try {
          fs.mkdirSync(outDir, { recursive: true });
          fs.writeFileSync(outPath, JSON.stringify(result, null, 2) + '\n');
          wrote = true;
        } catch (e) {
          console.error(`[sigmap] cannot write ${outPath}: ${e.message}`);
          process.exit(1);
        }
      }
      const payload = { stale: plan.stale, wrote, snapshotExists: plan.snapshotExists, changed: plan.changed.length, scanned: result.scannedFiles };
      if (jsonOut) {
        process.stdout.write(JSON.stringify(payload) + '\n');
        process.exit(0);
      }
      if (!plan.stale) {
        console.log(`[sigmap] conventions --update  ✓ up to date — ${result.scannedFiles} files, no changes since last scan`);
        process.exit(0);
      }
      const how = plan.snapshotExists ? `${plan.changed.length} changed file${plan.changed.length === 1 ? '' : 's'}` : 'initial scan';
      console.log(`[sigmap] conventions --update  rescanned (${how}) → wrote ${path.relative(cwd, outPath)}`);
      process.exit(0);
    }

    // `--fix`: exhaustive rename checklist — every file not matching the dominant style.
    if (args.includes('--fix')) {
      const { buildFixList } = requireSourceOrBundled('./src/conventions/fix');
      const fixList = buildFixList(cwd, files, result);
      if (jsonOut) {
        process.stdout.write(JSON.stringify(fixList) + '\n');
        process.exit(0);
      }
      console.log('[sigmap] conventions --fix  (TS/JS/Python)');
      if (!fixList.dominant) {
        console.log('  no dominant file-naming convention — nothing to fix');
        process.exit(0);
      }
      if (fixList.count === 0) {
        console.log(`  ✓ no fixes needed — every file matches ${fixList.dominant}`);
        process.exit(0);
      }
      console.log(`  ${fixList.count} file${fixList.count === 1 ? '' : 's'} to rename to ${fixList.dominant}:`);
      for (const r of fixList.renames) console.log(`  - [ ] ${r.from}  →  ${r.to}`);
      process.exit(0);
    }

    // `--ci`: gate — fail when overall consistency is below a threshold (or regresses).
    if (args.includes('--ci')) {
      const { ciGate } = requireSourceOrBundled('./src/conventions/ci');
      const minIdx = args.indexOf('--min');
      const min = minIdx !== -1 && args[minIdx + 1] ? parseFloat(args[minIdx + 1]) : undefined;
      const noRegress = args.includes('--no-regress');
      let prior = null;
      if (noRegress) {
        try {
          const lines = fs.readFileSync(path.join(cwd, '.context', 'conventions-history.ndjson'), 'utf8').split('\n').filter(Boolean);
          if (lines.length) prior = JSON.parse(lines[lines.length - 1]);
        } catch (_) {}
      }
      const gate = ciGate(result, { min, noRegress }, prior);
      if (jsonOut) {
        process.stdout.write(JSON.stringify(gate) + '\n');
        process.exit(gate.ok ? 0 : 1);
      }
      const pctC = (n) => `${(n * 100).toFixed(0)}%`;
      if (gate.ok) {
        console.log(`[sigmap] conventions --ci  ✓ PASS — consistency ${pctC(gate.score)} (min ${pctC(gate.min)})`);
        process.exit(0);
      }
      console.log(`[sigmap] conventions --ci  ✗ FAIL — consistency ${pctC(gate.score)} (min ${pctC(gate.min)})`);
      for (const r of gate.reasons) console.log(`  • ${r}`);
      process.exit(1);
    }

    // `--report`: consistency audit + score + trend vs the last run.
    if (args.includes('--report')) {
      const { scoreReport, snapshot } = requireSourceOrBundled('./src/conventions/report');
      const histPath = path.join(cwd, '.context', 'conventions-history.ndjson');
      let prior = null;
      try {
        const lines = fs.readFileSync(histPath, 'utf8').split('\n').filter(Boolean);
        if (lines.length) prior = JSON.parse(lines[lines.length - 1]);
      } catch (_) {}
      const report = scoreReport(result, prior);
      // Append the new snapshot to the history log.
      try {
        fs.mkdirSync(path.join(cwd, '.context'), { recursive: true });
        fs.appendFileSync(histPath, JSON.stringify(snapshot(result, new Date().toISOString())) + '\n');
      } catch (_) {}

      if (jsonOut) {
        process.stdout.write(JSON.stringify(report) + '\n');
        process.exit(0);
      }
      const pctR = (n) => `${(n * 100).toFixed(0)}%`;
      const arrow = (d) => (d == null ? '' : d > 0.0005 ? ` ▲${(d * 100).toFixed(0)}pp` : d < -0.0005 ? ` ▼${(Math.abs(d) * 100).toFixed(0)}pp` : ' =');
      console.log('[sigmap] conventions --report  (TS/JS/Python)');
      console.log(`  overall consistency: ${pctR(report.score)}${arrow(report.scoreDelta)}${report.prevScore == null ? '  (first run)' : ''}`);
      for (const c of report.conventions) {
        if (c.total === 0) { console.log(`  ${c.name.padEnd(14)} n/a (no samples)`); continue; }
        console.log(`  ${c.name.padEnd(14)} ${c.dominant} ${pctR(c.dominantPct)} [${c.tier}]${arrow(c.delta)}`);
      }
      console.log(`  ${'test framework'.padEnd(14)} ${report.testFramework || 'none detected'}`);
      process.exit(0);
    }

    // `--conflicts`: surface why a convention is mixed (breakdown + rename suggestions).
    if (args.includes('--conflicts')) {
      const { analyzeConflicts } = requireSourceOrBundled('./src/conventions/conflicts');
      const report = analyzeConflicts(result);
      if (jsonOut) {
        process.stdout.write(JSON.stringify(report) + '\n');
        process.exit(0);
      }
      const pctC = (n) => `${(n * 100).toFixed(0)}%`;
      if (!report.hasConflicts) {
        console.log('[sigmap] conventions --conflicts  (TS/JS/Python)');
        console.log('  no conflicts — conventions are consistent ✓');
        process.exit(0);
      }
      console.log('[sigmap] conventions --conflicts  (TS/JS/Python)');
      for (const c of report.conventions) {
        console.log(`\n  ${c.name} — dominant: ${c.dominant} ${pctC(c.dominantPct)} [${c.tier}]`);
        for (const v of c.variants) {
          const barLen = Math.max(1, Math.round(v.pct * 20));
          const bar = '█'.repeat(barLen);
          const tag = v.pattern === c.dominant ? ' (dominant)' : '';
          const ex = v.examples.length ? `  e.g. ${v.examples.join(', ')}` : '';
          console.log(`    ${v.pattern.padEnd(12)} ${String(v.count).padStart(4)}  ${pctC(v.pct).padStart(4)} ${bar}${tag}${ex}`);
        }
        if (c.renames.length) {
          console.log(`    rename to match ${c.dominant}:`);
          for (const r of c.renames) console.log(`      ${r.from}  →  ${r.to}`);
        }
      }
      process.exit(0);
    }

    const outDir = path.join(cwd, '.context');
    const outPath = path.join(outDir, 'conventions.json');
    try {
      fs.mkdirSync(outDir, { recursive: true });
      fs.writeFileSync(outPath, JSON.stringify(result, null, 2) + '\n');
    } catch (e) {
      console.error(`[sigmap] cannot write ${outPath}: ${e.message}`);
      process.exit(1);
    }

    if (jsonOut) {
      process.stdout.write(JSON.stringify(result) + '\n');
      process.exit(0);
    }

    const pct = (n) => `${(n * 100).toFixed(0)}%`;
    const tierLabel = { consistent: 'consistent', mostly: 'mostly', inconsistent: 'mixed', unknown: 'n/a' };
    const line = (name, conv) => {
      if (!conv || conv.total === 0) return `  ${name.padEnd(14)} n/a (no samples)`;
      const variants = conv.variants.slice(1, 3).map((v) => `${v.label} ${pct(v.pct)}`).join(', ');
      const tail = variants ? `  ·  also: ${variants}` : '';
      return `  ${name.padEnd(14)} ${conv.dominant} ${pct(conv.dominantPct)} [${tierLabel[conv.tier]}]${tail}`;
    };

    console.log('[sigmap] conventions  (TS/JS/Python)');
    console.log(`  scanned        ${result.scannedFiles} file${result.scannedFiles === 1 ? '' : 's'}`);
    console.log(line('file naming', result.fileNaming));
    console.log(line('export style', result.exportStyle));
    console.log(`  ${'test framework'.padEnd(14)} ${result.testFramework || 'none detected'}`);
    console.log(`\n  → wrote ${path.relative(cwd, outPath)}`);
    process.exit(0);
  }

  // Layer 4: `sigmap scaffold <name>` — propose a convention-matched structure
  // for a new module, gated by a confidence floor (refuses if conventions are
  // too inconsistent; a wrong proposal systematizes bad code).
  if (args[0] === 'scaffold') {
    const jsonOut = args.includes('--json');
    const name = args[1] && !args[1].startsWith('--') ? args[1] : null;
    if (!name) {
      console.error('[sigmap] usage: sigmap scaffold <name> [--ext <e>] [--threshold <n>] [--force] [--json]');
      process.exit(1);
    }
    const thIdx = args.indexOf('--threshold');
    const threshold = thIdx !== -1 && args[thIdx + 1] ? parseFloat(args[thIdx + 1]) : undefined;
    const extIdx = args.indexOf('--ext');
    const ext = extIdx !== -1 && args[extIdx + 1] ? args[extIdx + 1].replace(/^\./, '') : undefined;
    const force = args.includes('--force');

    const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
    const { proposeScaffold } = requireSourceOrBundled('./src/scaffold/propose');
    const conventions = extractConventions(cwd, buildFileList(cwd, config));
    const decision = proposeScaffold(name, conventions, { threshold, ext, force });

    // §6.2 (2d): persist an accepted proposal so `create`/agents can read it back.
    let persistedTo = null;
    if (decision.ok) {
      const { scaffoldPath, renderScaffoldMarkdown } = requireSourceOrBundled('./src/scaffold/persist');
      const outPath = scaffoldPath(cwd);
      try {
        fs.mkdirSync(path.dirname(outPath), { recursive: true });
        fs.writeFileSync(outPath, renderScaffoldMarkdown(decision, { timestamp: new Date().toISOString() }));
        persistedTo = path.relative(cwd, outPath) || outPath;
      } catch (_) {}
    }

    if (jsonOut) {
      process.stdout.write(JSON.stringify({ ...decision, persistedTo }) + '\n');
      process.exit(decision.ok ? 0 : 1);
    }

    const pctS = (n) => `${(n * 100).toFixed(0)}%`;
    if (decision.ok) {
      console.log(`[sigmap] scaffold "${decision.name}"  — conventions ${decision.tier} (${pctS(decision.confidence)})`);
      if (decision.warning) console.log(`  ⚠ ${decision.warning}`);
      const p = decision.proposal;
      console.log(`  file:           ${p.filename}  (${p.namingStyle})`);
      console.log(`  export style:   ${p.exportStyle}`);
      console.log(`  test file:      ${p.testFile}${p.testFramework ? `  (${p.testFramework})` : ''}`);
      if (persistedTo) console.log(`\n  → wrote ${persistedTo}`);
      process.exit(0);
    }

    console.log(`[sigmap] scaffold "${decision.name}" — REFUSED`);
    console.log(`  ${decision.reason}`);
    if (decision.conflicts && decision.conflicts.hasConflicts) {
      for (const c of decision.conflicts.conventions) {
        console.log(`\n  ${c.name} — dominant: ${c.dominant} ${pctS(c.dominantPct)} [${c.tier}]`);
        for (const v of c.variants) {
          console.log(`    ${v.pattern.padEnd(12)} ${pctS(v.pct).padStart(4)}${v.examples.length ? `  e.g. ${v.examples.join(', ')}` : ''}`);
        }
      }
    }
    process.exit(1);
  }

  // v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
  if (args[0] === 'squeeze') {
    const jsonOut = args.includes('--json');
    const target = args[1] && !args[1].startsWith('--') ? args[1] : '-';
    let input = '';
    try {
      input = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
    } catch (e) {
      console.error(`[sigmap] cannot read input: ${e.message}`);
      process.exit(1);
    }

    const { squeeze: runSqueeze, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
    let symbolIndex = null;
    try {
      const { buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
      symbolIndex = buildSigIndex(cwd);
    } catch (_) {}
    const sq = runSqueeze(input, { srcDirs: config.srcDirs, symbolIndex });

    try {
      const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
      checkStarNudge(cwd, true, { silent: !process.stderr.isTTY || jsonOut, bump: { squeezeOffered: sq.applies ? 1 : 0 } });
    } catch (_) {}

    if (jsonOut) {
      process.stdout.write(JSON.stringify({
        category: sq.category, confidence: sq.confidence,
        rawTokens: sq.rawTokens, squeezedTokens: sq.squeezedTokens,
        reduction: sq.reduction, enriched: sq.enriched, squeezed: sq.squeezed,
      }) + '\n');
      process.exit(0);
    }
    if (!sq.category) {
      process.stderr.write('[sigmap] no squeezable structure detected — input unchanged\n');
      process.stdout.write(input);
      process.exit(0);
    }
    process.stderr.write(formatSummary(sq) + '\n\n');
    process.stdout.write(sq.squeezed + '\n');
    process.exit(0);
  }

  // `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
  // Gap 2 (step 2): `sigmap verify-plan <plan.md>` — check a plan against the
  // live index before execution (files/symbols exist, blast radius, scope).
  if (args[0] === 'verify-plan') {
    const target = args[1] && !args[1].startsWith('--') ? args[1] : null;
    const jsonOut = args.includes('--json');
    if (!target) {
      console.error('[sigmap] Usage: sigmap verify-plan <plan.md|-> [--json]');
      process.exit(1);
    }
    let planText = '';
    try {
      planText = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
    } catch (e) {
      console.error(`[sigmap] cannot read plan: ${e.message}`);
      process.exit(1);
    }
    const { verifyPlan } = requireSourceOrBundled('./src/plan/verify-plan');
    const result = verifyPlan(planText, cwd);

    if (jsonOut) {
      process.stdout.write(JSON.stringify(result) + '\n');
      process.exit(result.summary.ok ? 0 : 1);
    }

    const s = result.summary;
    console.log(`[sigmap] verify-plan — ${s.filesReferenced} file(s), ${s.symbolsReferenced} symbol(s) referenced`);
    if (result.issues.length === 0) {
      console.log('  ✓ plan checks out — all references exist, blast radius and scope in bounds');
      process.exit(0);
    }
    for (const i of result.issues) {
      const mark = i.severity === 'error' ? '✗' : '⚠';
      if (i.type === 'missing-file') console.log(`  ${mark} missing file: ${i.ref} (line ${i.line})`);
      else if (i.type === 'unknown-symbol') console.log(`  ${mark} unknown symbol: ${i.ref}()${i.suggestion ? ` — did you mean ${i.suggestion}()?` : ''} (line ${i.line})`);
      else if (i.type === 'high-blast-radius') console.log(`  ${mark} high blast radius: ${i.ref} → ${i.count} dependents`);
      else if (i.type === 'broad-scope') console.log(`  ${mark} broad scope: ${i.count} files (threshold ${i.threshold})`);
    }
    console.log(`\n  ${s.errors} error(s), ${s.warnings} warning(s)`);
    process.exit(s.ok ? 0 : 1);
  }

  // `sigmap verify` is the flagship alias for `verify-ai-output` — the grounding
  // guard that proves an AI answer is anchored to real signatures + line anchors.
  if (args[0] === 'verify-ai-output' || args[0] === 'verify') {
    const cmdName = args[0];
    const target = args[1];
    const jsonOut = args.includes('--json');
    const reportIdx = args.indexOf('--report');
    const reportOut = reportIdx !== -1
      ? (args[reportIdx + 1] && !args[reportIdx + 1].startsWith('--') ? args[reportIdx + 1] : 'sigmap-verify-report.html')
      : null;
    if (!target || target.startsWith('--')) {
      console.error(`[sigmap] Usage: sigmap ${cmdName} <answer.md> [--json] [--report [out.html]]`);
      process.exit(1);
    }
    const absTarget = path.resolve(cwd, target);
    if (!fs.existsSync(absTarget)) {
      console.error(`[sigmap] file not found: ${target}`);
      process.exit(1);
    }

    const answerText = fs.readFileSync(absTarget, 'utf8');
    const { verify } = requireSourceOrBundled('./src/verify/hallucination-guard');
    const { issues, summary } = verify(answerText, cwd);
    const rel = path.relative(cwd, absTarget) || target;
    const result = { file: rel, issues, summary };

    // Optional HTML report (Surface A) — written alongside any other output.
    if (reportOut) {
      const { renderReportHtml } = requireSourceOrBundled('./src/format/verify-report');
      const outAbs = path.resolve(cwd, reportOut);
      fs.writeFileSync(outAbs, renderReportHtml(result));
      if (!jsonOut) console.log(`[sigmap] report written: ${path.relative(cwd, outAbs) || reportOut}`);
    }

    if (jsonOut) {
      process.stdout.write(JSON.stringify(result) + '\n');
      process.exit(summary.total > 0 ? 1 : 0);
    }

    if (summary.total === 0) {
      console.log(`[sigmap] ✓ ${rel} — no hallucinations detected (${summary.symbolsIndexed} symbols indexed)`);
      process.exit(0);
    }

    const labels = {
      'fake-file': 'Fake file', 'fake-test-file': 'Fake test file',
      'fake-import': 'Fake import', 'fake-symbol': 'Fake symbol',
      'fake-npm-script': 'Fake npm script',
    };
    const bt = summary.byType;
    console.log(`[sigmap] ✗ ${rel} — ${summary.total} issue${summary.total === 1 ? '' : 's'} found`);
    console.log(`  fake-file: ${bt['fake-file']}  fake-test-file: ${bt['fake-test-file']}  fake-import: ${bt['fake-import']}  fake-symbol: ${bt['fake-symbol']}  fake-npm-script: ${bt['fake-npm-script']}`);
    console.log('');
    for (const issue of issues) {
      console.log(`  L${issue.line}  [${labels[issue.type] || issue.type}]  ${issue.message}`);
      if (issue.suggestion) console.log(`         ↳ ${issue.suggestion}`);
    }
    process.exit(1);
  }

  // Gap 2 (step 4): `sigmap review-pr` — diff audit (scope drift, god-nodes,
  // missing tests, security-sensitive files). Last guard stage of `create`.
  if (args[0] === 'review-pr') {
    const jsonOut = args.includes('--json');
    const staged = args.includes('--staged');
    const baseIdx = args.indexOf('--base');
    const baseArg = baseIdx !== -1 && args[baseIdx + 1] && !args[baseIdx + 1].startsWith('--') ? args[baseIdx + 1] : null;

    let nameStatus = '';
    if (staged) {
      nameStatus = __tryGit(['diff', '--cached', '--name-status'], { cwd });
    } else {
      let base = baseArg;
      if (!base) {
        for (const ref of ['main', 'develop']) {
          if (__tryGit(['rev-parse', '--verify', '--quiet', ref], { cwd })) { base = ref; break; }
        }
      }
      const mb = base ? (__tryGit(['merge-base', 'HEAD', base], { cwd }) || base) : 'HEAD~1';
      nameStatus = __tryGit(['diff', '--name-status', `${mb}..HEAD`], { cwd });
    }

    const changedFiles = nameStatus.split('\n').filter(Boolean).map((line) => {
      const parts = line.split('\t');
      const status = parts[0][0]; // M/A/D/R…
      const file = parts[parts.length - 1]; // rename → new path
      return { path: file, status };
    });

    // --markdown / --evidence: emit the branded, deterministic PR Evidence Report.
    if (args.includes('--markdown') || args.includes('--evidence')) {
      const { buildPrEvidence, formatPrEvidenceMarkdown } = requireSourceOrBundled('./src/review/pr-evidence');
      const scope = staged ? 'staged' : (baseArg ? `vs ${baseArg}` : 'branch');
      const ev = buildPrEvidence(changedFiles, cwd, { scope });
      process.stdout.write(formatPrEvidenceMarkdown(ev) + '\n');
      process.exit(ev.review.summary.ok ? 0 : 1);
    }

    const { reviewPr } = requireSourceOrBundled('./src/review/review-pr');
    const result = reviewPr(changedFiles, cwd, {});

    if (jsonOut) {
      process.stdout.write(JSON.stringify(result) + '\n');
      process.exit(result.summary.ok ? 0 : 1);
    }

    const s = result.summary;
    console.log(`[sigmap] review-pr — ${s.filesChanged} file(s) changed (${s.sourceChanged} source, ${s.testsChanged} test)`);
    if (s.findings === 0) {
      console.log('  ✓ no findings — scope, tests, blast radius, and sensitive files all clear');
      process.exit(0);
    }
    const label = { 'missing-tests': 'missing tests', 'security-file': 'security file', 'god-node': 'god node', 'scope-drift': 'scope drift' };
    for (const f of result.findings) {
      if (f.type === 'missing-tests') console.log(`  ⚠ ${label[f.type]}: ${f.file} changed with no matching test`);
      else if (f.type === 'security-file') console.log(`  ⚠ ${label[f.type]}: ${f.file}`);
      else if (f.type === 'god-node') console.log(`  ⚠ ${label[f.type]}: ${f.file} → ${f.count} dependents`);
      else if (f.type === 'scope-drift') console.log(`  ⚠ ${label[f.type]}: ${f.count} top-level dirs (${f.dirs.join(', ')})`);
    }
    console.log(`\n  ${s.findings} finding(s)`);
    process.exit(1);
  }

  // Gap 2 (capstone): `sigmap create "<task>"` — orchestrate the 4 guard stages
  // (scaffold → verify-plan → verify-ai-output → review-pr) with n/4 numbering.
  if (args[0] === 'create') {
    const jsonOut = args.includes('--json');
    const task = args[1] && !args[1].startsWith('--') ? args[1] : null;
    const flagVal = (flag) => { const i = args.indexOf(flag); return i !== -1 && args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : null; };
    const nameArg = flagVal('--name');
    const planFile = flagVal('--plan');
    const answerFile = flagVal('--answer');
    const staged = args.includes('--staged');
    const baseArg = flagVal('--base');

    const ctx = { task };

    if (nameArg) {
      ctx.name = nameArg;
      try {
        const { extractConventions } = requireSourceOrBundled('./src/conventions/extract');
        ctx.conventions = extractConventions(cwd, buildFileList(cwd, config));
      } catch (_) {}
    }
    for (const [flag, file, key] of [['--plan', planFile, 'plan'], ['--answer', answerFile, 'answer']]) {
      if (!file) continue;
      try { ctx[key] = fs.readFileSync(path.resolve(cwd, file), 'utf8'); }
      catch (e) { console.error(`[sigmap] cannot read ${flag} file: ${e.message}`); process.exit(1); }
    }

    // review-pr inputs: changed files from git (same collection as review-pr).
    let nameStatus = '';
    if (staged) {
      nameStatus = __tryGit(['diff', '--cached', '--name-status'], { cwd });
    } else {
      let base = baseArg;
      if (!base) {
        for (const ref of ['main', 'develop']) {
          if (__tryGit(['rev-parse', '--verify', '--quiet', ref], { cwd })) { base = ref; break; }
        }
      }
      const mb = base ? (__tryGit(['merge-base', 'HEAD', base], { cwd }) || base) : 'HEAD~1';
      nameStatus = __tryGit(['diff', '--name-status', `${mb}..HEAD`], { cwd });
    }
    ctx.changedFiles = nameStatus.split('\n').filter(Boolean).map((line) => {
      const parts = line.split('\t');
      return { path: parts[parts.length - 1], status: parts[0][0] };
    });

    const { orchestrate } = requireSourceOrBundled('./src/create/orchestrate');
    const result = orchestrate(ctx, cwd);

    if (jsonOut) {
      process.stdout.write(JSON.stringify(result) + '\n');
      process.exit(result.summary.ok ? 0 : 1);
    }

    console.log(`[sigmap] create${result.task ? ` "${result.task}"` : ''} — grounded-creation pipeline`);
    for (const st of result.steps) {
      const mark = st.skipped ? '–' : (st.ok ? '✓' : '✗');
      const status = st.skipped ? `skipped (${st.reason})` : (st.ok ? 'ok' : 'FAILED');
      console.log(`  ${st.n}/${st.total} ${mark} ${st.name.padEnd(16)} ${status}`);
    }
    const su = result.summary;
    console.log(`\n  ${su.ran}/${su.total} ran · ${su.passed} passed · ${su.failed} failed · ${su.skipped} skipped`);
    process.exit(su.ok ? 0 : 1);
  }

  // Feature 1: `sigmap explain <file>` — why a file is included or excluded
  if (args[0] === 'explain' || args.includes('--explain')) {
    const target = args[0] === 'explain'
      ? args[1]
      : args[args.indexOf('--explain') + 1];

    if (!target || target.startsWith('--')) {
      console.error('[sigmap] Usage: sigmap explain <file>');
      process.exit(1);
    }

    const absPath = path.resolve(cwd, target);
    const rel     = path.relative(cwd, absPath);
    const jsonOut = args.includes('--json');

    // 1. Check .contextignore
    const ignorePatterns = loadIgnorePatterns(cwd);
    const ignored = matchesIgnore(rel.replace(/\\/g, '/'), ignorePatterns);
    if (ignored) {
      if (jsonOut) {
        process.stdout.write(JSON.stringify({ status: 'excluded', reason: '.contextignore', fix: `remove the pattern or add '!${rel}' as an exception` }) + '\n');
      } else {
        console.log(`[sigmap] ${rel} — EXCLUDED`);
        console.log(`  Reason: matched .contextignore`);
        console.log(`  Fix:    remove the pattern or add '!${rel}' as an exception`);
      }
      process.exit(0);
    }

    // 2. Check srcDirs membership
    const inSrcDirs = (config.srcDirs || []).some(d => absPath.startsWith(path.resolve(cwd, d)));
    if (!inSrcDirs) {
      if (jsonOut) {
        process.stdout.write(JSON.stringify({ status: 'excluded', reason: 'not in srcDirs', srcDirs: config.srcDirs, fix: 'add the containing directory to srcDirs in gen-context.config.json' }) + '\n');
      } else {
        console.log(`[sigmap] ${rel} — EXCLUDED`);
        console.log(`  Reason: not under any srcDir (${(config.srcDirs || []).join(', ')})`);
        console.log(`  Fix:    add the containing directory to srcDirs in gen-context.config.json`);
      }
      process.exit(0);
    }

    // 3. Detect extractor and extract sigs
    const base    = path.basename(absPath);
    const ext     = path.extname(base).toLowerCase();
    let extractorName = EXT_MAP[ext] || null;
    if (!extractorName && isDockerfile(base)) extractorName = 'dockerfile';
    if (!extractorName) extractorName = 'generic';

    let sigs = [];
    if (fs.existsSync(absPath)) {
      try {
        const content = fs.readFileSync(absPath, 'utf8');
        const extractor = getExtractor(extractorName);
        if (extractor) sigs = extractor.extract(content).slice(0, config.maxSigsPerFile || 25);
      } catch (_) {}
    }

    if (!sigs.length) {
      if (jsonOut) {
        process.stdout.write(JSON.stringify({ status: 'excluded', reason: 'no signatures', extractor: extractorName, fix: 'check that the file contains function/class definitions' }) + '\n');
      } else {
        console.log(`[sigmap] ${rel} — EXCLUDED`);
        console.log(`  Reason: no extractable signatures (extractor: ${extractorName})`);
        console.log(`  Fix:    check that the file contains function/class definitions`);
      }
      process.exit(0);
    }

    // 4. Included
    if (jsonOut) {
      process.stdout.write(JSON.stringify({ status: 'included', extractor: extractorName, signatures: sigs.length, preview: sigs.slice(0, 3), sigs }) + '\n');
    } else {
      console.log(`[sigmap] ${rel} — INCLUDED`);
      console.log(`  Extractor : ${extractorName}`);
      console.log(`  Signatures: ${sigs.length}`);
      console.log(`  Preview   : ${sigs.slice(0, 3).join(' · ')}`);
    }
    process.exit(0);
  }

  if (args.includes('--init')) {
    writeInitConfig(cwd);
    // §6.2 (2f): document the grounded-creation workflow in CLAUDE.md (idempotent).
    try {
      const { renderCreationWorkflowBlock, injectCreationWorkflow } = requireSourceOrBundled('./src/init/creation-workflow');
      const claudePath = path.join(cwd, 'CLAUDE.md');
      let existing = '';
      try { if (fs.existsSync(claudePath)) existing = fs.readFileSync(claudePath, 'utf8'); } catch (_) {}
      fs.writeFileSync(claudePath, injectCreationWorkflow(existing, renderCreationWorkflowBlock()));
      console.log(`[sigmap] init → Creation workflow block in ${path.relative(cwd, claudePath) || 'CLAUDE.md'}`);
    } catch (_) {}
    process.exit(0);
  }

  if (args.includes('--health')) {
    const { score } = __require('./src/health/scorer');
    const result = score(cwd);
    // v4.0: compute live coverage score to include in health output
    let coverageResult = null;
    try {
      const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
      const { loadConfig: lc } = requireSourceOrBundled('./src/config/loader');
      const cfg = lc(cwd);
      // Use all files from srcDirs as proxies for "included" (no budget applied in health mode)
      const allFiles = buildFileList(cwd, cfg);
      const fakeEntries = allFiles.map(f => ({ filePath: f }));
      coverageResult = coverageScore(cwd, fakeEntries, cfg);
    } catch (_) {}
    // v6.7: Collect cache stats if enabled
    let cacheStats = null;
    try {
      const cachePath = path.join(cwd, '.sigmap-cache.json');
      if (fs.existsSync(cachePath)) {
        const raw = fs.readFileSync(cachePath, 'utf8');
        const data = JSON.parse(raw);
        const sizeKb = Math.round(Buffer.byteLength(raw) / 1024);
        const entries = Object.keys(data.entries || {}).length;
        const mtime = fs.statSync(cachePath).mtimeMs;
        cacheStats = { sizeKb, entries, mtimeMs: mtime };
      }
    } catch (_) {}
    if (args.includes('--json')) {
      // Feature 3 (VS Code) + Feature 5 (JetBrains): emit tokens + reduction for plugins
      const ctxPath = path.join(cwd, '.github', 'copilot-instructions.md');
      let tokens = 0;
      let reduction = result.tokenReductionPct || 0;
      if (fs.existsSync(ctxPath)) {
        try { tokens = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}
      }
      const payload = { ...result, tokens, reduction };
      if (coverageResult) {
        payload.coverage = coverageResult.score;
        payload.coverageGrade = coverageResult.grade;
        payload.coverageConfidence = coverageResult.confidence;
        payload.coverageTotalFiles = coverageResult.total;
        payload.coverageIncludedFiles = coverageResult.included;
      }
      if (cacheStats) {
        payload.cacheStats = cacheStats;
      }
      process.stdout.write(JSON.stringify(payload) + '\n');
    } else {
      console.log('[sigmap] health:');
      console.log(`  score           : ${result.score}/100 (grade ${result.grade})`);
      if (coverageResult) {
        console.log(`  file access     : ${coverageResult.grade} (${coverageResult.score}%)  — ${coverageResult.included} of ${coverageResult.total} files accessible in srcDirs`);
      }
      console.log(`  strategy        : ${result.strategy}`);
      console.log(`  token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
      console.log(`  days since regen: ${result.daysSinceRegen !== null ? result.daysSinceRegen : 'context file not found'}`);
      if (result.strategyFreshnessDays !== null) {
        console.log(`  cold freshness  : ${result.strategyFreshnessDays} day(s)`);
      }
      if (cacheStats) {
        console.log(`  sig-cache       : ${cacheStats.entries} files cached  ${cacheStats.sizeKb}KB on disk`);
      }
      console.log(`  total runs      : ${result.totalRuns}`);
      console.log(`  over-budget runs: ${result.overBudgetRuns}`);
      console.log(`  p50 token count : ${result.p50TokenCount}`);
      console.log(`  p95 token count : ${result.p95TokenCount}`);
      console.log(`  overbudget streak: ${result.overBudgetStreak}`);
      console.log(`  repo languages  : ${result.extractorCoverage}% of supported langs used here (informational — not scored)`);
      console.log(`  ${'score basis'.padEnd(16)}: staleness · token reduction · over-budget rate`);
    }
    process.exit(0);
  }

  if (args.includes('--dashboard')) {
    try {
      const { score } = __require('./src/health/scorer');
      const health = score(cwd);
      const { generateDashboardHtml } = requireSourceOrBundled('./src/format/dashboard');
      const out = generateDashboardHtml(cwd, health);
      const outPath = path.join(cwd, 'benchmarks', 'reports', 'dashboard.html');
      fs.mkdirSync(path.dirname(outPath), { recursive: true });
      fs.writeFileSync(outPath, out.html, 'utf8');
      if (args.includes('--json')) {
        process.stdout.write(JSON.stringify({ ok: true, file: path.relative(cwd, outPath), summary: out.data.summary }) + '\n');
      } else {
        console.log(`[sigmap] dashboard written: ${path.relative(cwd, outPath)}`);
      }
    } catch (err) {
      console.error(`[sigmap] dashboard error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  if (args.includes('--suggest-tool')) {
    const idx = args.indexOf('--suggest-tool');
    const taskDesc = (args[idx + 1] || '').trim();
    if (!taskDesc || taskDesc.startsWith('--')) {
      console.error('[sigmap] --suggest-tool requires a task description');
      console.error('  Example: node gen-context.js --suggest-tool "refactor the auth module"');
      process.exit(1);
    }
    const result = suggestTool(taskDesc);
    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(result) + '\n');
    } else {
      console.log('[sigmap] suggest-tool:');
      console.log(`  tier   : ${result.tier}`);
      console.log(`  label  : ${result.label}`);
      console.log(`  models : ${result.models}`);
      console.log(`  cost   : ${result.costHint}`);
    }
    process.exit(0);
  }

  if (args.includes('--benchmark') || args.includes('--eval')) {
    try {
      const { run, formatTable, formatMetrics } = __require('./src/eval/runner');
      const tasksFile = path.join(cwd, 'benchmarks', 'tasks', 'retrieval.jsonl');
      const result = run(tasksFile, cwd, { topK: 10 });
      if (args.includes('--json')) {
        // Compact output: strip ranked arrays to keep payload small
        const compact = {
          metrics: result.metrics,
          tasks: result.tasks.map((t) => ({
            id: t.id,
            query: t.query,
            hit5: t.hit5,
            rr: t.rr,
            precAt5: t.precAt5,
            tokens: t.tokens,
            top1: t.ranked[0] || null,
          })),
        };
        process.stdout.write(JSON.stringify(compact) + '\n');
      } else {
        console.log(formatMetrics(result.metrics));
        if (result.tasks.length > 0) {
          console.log('');
          console.log(formatTable(result.tasks));
        }
      }
    } catch (err) {
      console.error(`[sigmap] benchmark error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  if (args.includes('--analyze')) {
    try {
      const { analyzeFiles, formatAnalysisTable, formatAnalysisJSON } = requireSourceOrBundled('./src/eval/analyzer');
      const cfg    = config || {};
      const srcDirs = cfg.srcDirs || DEFAULTS.srcDirs;
      const exclude = cfg.exclude || DEFAULTS.exclude;
      const slow    = args.includes('--slow');

      // Collect files (reuse existing file-walker if accessible, else inline)
      const allFiles = [];
      function walkForAnalyze(dir, depth) {
        if (depth > (cfg.maxDepth || 6)) return;
        let entries;
        try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
        for (const e of entries) {
          if (exclude.some((x) => e.name === x || e.name.startsWith(x))) continue;
          const full = path.join(dir, e.name);
          if (e.isDirectory()) walkForAnalyze(full, depth + 1);
          else if (e.isFile()) allFiles.push(full);
        }
      }
      for (const sd of srcDirs) {
        const abs = path.join(cwd, sd);
        if (fs.existsSync(abs)) walkForAnalyze(abs, 0);
      }

      const stats = analyzeFiles(allFiles, cwd, { slow, maxSigs: cfg.maxSigsPerFile || 25 });

      if (args.includes('--json')) {
        const out = JSON.stringify(formatAnalysisJSON(stats)) + '\n';
        // Use the write callback to exit only after the OS has accepted all
        // bytes. Calling process.exit(0) synchronously after write() truncates
        // large outputs because the underlying pipe write is asynchronous even
        // when write() returns true.
        process.stdout.write(out, 'utf8', () => process.exit(0));
        return; // exit is handled by the callback above
      } else {
        const table = formatAnalysisTable(stats, slow);
        process.stdout.write(table);
      }
    } catch (err) {
      console.error(`[sigmap] analyze error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  if (args.includes('--diagnose-extractors')) {
    try {
      const fixturesDir = path.join(cwd, 'test', 'fixtures');
      const expectedDir = path.join(cwd, 'test', 'expected');
      if (!fs.existsSync(fixturesDir) || !fs.existsSync(expectedDir)) {
        console.error('[sigmap] test/fixtures or test/expected not found — run from the SigMap repo root');
        process.exit(1);
      }

      const EXT_TO_LANG = {
        '.ts': 'typescript', '.js': 'javascript', '.py': 'python',
        '.java': 'java', '.kt': 'kotlin', '.go': 'go', '.rs': 'rust',
        '.cs': 'csharp', '.cpp': 'cpp', '.rb': 'ruby', '.php': 'php',
        '.swift': 'swift', '.dart': 'dart', '.scala': 'scala',
        '.r': 'r', '.R': 'r',
        '.vue': 'vue', '.svelte': 'svelte', '.html': 'html',
        '.css': 'css', '.yml': 'yaml', '.sh': 'shell',
      };
      const SPECIAL = { 'Dockerfile': 'dockerfile' };

      let passed = 0; let failed = 0;
      const entries = fs.readdirSync(fixturesDir).sort();

      for (const filename of entries) {
        const ext  = path.extname(filename).toLowerCase();
        const lang = EXT_TO_LANG[ext] || SPECIAL[filename];
        if (!lang) continue;

        const fixturePath = path.join(fixturesDir, filename);
        const expectedPath = path.join(expectedDir, `${lang}.txt`);
        if (!fs.existsSync(expectedPath)) {
          console.log(`  SKIP  ${lang.padEnd(12)} (no expected file)`);
          continue;
        }

        const src      = fs.readFileSync(fixturePath, 'utf8');
        const expected = fs.readFileSync(expectedPath, 'utf8').trim();

        let mod;
        try {
          mod = requireSourceOrBundled(`./src/extractors/${lang}`);
        } catch (e) {
          console.log(`  ERROR ${lang.padEnd(12)} loader failed: ${e.message}`);
          failed++;
          continue;
        }

        let actual;
        try {
          const sigs = mod.extract(src);
          actual = sigs.join('\n').trim();
        } catch (e) {
          console.log(`  ERROR ${lang.padEnd(12)} extract() threw: ${e.message}`);
          failed++;
          continue;
        }

        if (actual === expected) {
          console.log(`  PASS  ${lang}`);
          passed++;
        } else {
          console.log(`  FAIL  ${lang}`);
          // Show first diff line
          const aLines = actual.split('\n');
          const eLines = expected.split('\n');
          const maxLen = Math.max(aLines.length, eLines.length);
          for (let i = 0; i < maxLen; i++) {
            if (aLines[i] !== eLines[i]) {
              console.log(`         expected: ${(eLines[i] || '(missing)').slice(0, 100)}`);
              console.log(`         actual  : ${(aLines[i] || '(missing)').slice(0, 100)}`);
              break;
            }
          }
          failed++;
        }
      }

      console.log(`\n${passed} passed, ${failed} failed`);
      process.exit(failed > 0 ? 1 : 0);
    } catch (err) {
      console.error(`[sigmap] diagnose error: ${err.message}`);
      process.exit(1);
    }
  }

  if (args.includes('--query')) {
    try {
      const qIdx = args.indexOf('--query');
      const query = (args[qIdx + 1] || '').trim();
      if (!query || query.startsWith('--')) {
        console.error('[sigmap] --query requires a search string');
        console.error('  Example: node gen-context.js --query "add a new language extractor"');
        process.exit(1);
      }
      const { rank, buildSigIndex, formatRankTable, formatRankJSON } = requireSourceOrBundled('./src/retrieval/ranker');

      // Resolve the context file path to query against.
      // Priority: --output flag > --adapter flag > buildSigIndex probe order
      //   (customOutput from config is handled inside buildSigIndex itself)
      let queryOpts;
      const adpIdx = args.indexOf('--adapter');

      // 1. --output <file> pins to an explicit path
      if (config.customOutput) {
        queryOpts = { contextPath: path.resolve(cwd, config.customOutput) };
      }

      // 2. --adapter <name> pins to that adapter's output path (if --output not given)
      if (!queryOpts && adpIdx >= 0) {
        const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
        const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
        if (VALID_ADAPTERS.includes(adapterName)) {
          try {
            const adapterMod = __require('./packages/adapters/' + adapterName);
            queryOpts = { contextPath: adapterMod.outputPath(cwd) };
          } catch (_) {}
        }
      }

      const index = buildSigIndex(cwd, queryOpts);
      if (index.size === 0) {
        console.error('[sigmap] no context file found. Run: node gen-context.js');
        if (adpIdx >= 0) {
          console.error('  (tried the path for --adapter ' + (args[adpIdx + 1] || '') + ')');
        }
        process.exit(1);
      }
      const topIdx = args.indexOf('--top');
      const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
                               : ((config && config.retrieval && config.retrieval.topK) || 10);
      const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
      const results = rank(query, index, { topK, recencyBoost, cwd });
      if (args.includes('--context')) {
        const miniCtx  = buildMiniContext(results, cwd);
        const ctxOut   = path.join(cwd, '.context', 'query-context.md');
        fs.mkdirSync(path.dirname(ctxOut), { recursive: true });
        fs.writeFileSync(ctxOut, miniCtx, 'utf8');
        console.log(`[sigmap] query context → ${path.relative(cwd, ctxOut)}  (${estimateTokens(miniCtx)} tokens)`);
      } else if (args.includes('--json')) {
        process.stdout.write(JSON.stringify(formatRankJSON(results, query)) + '\n');
      } else {
        process.stdout.write(formatRankTable(results, query));
      }
    } catch (err) {
      console.error(`[sigmap] query error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  // ── --each [--adapter <name>] ────────────────────────────────────────────
  // Must be checked before --adapter so that --each --adapter <name> is
  // handled here (per-repo) rather than running a single generate on the
  // parent directory.
  if (args.includes('--each')) {
    const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
    const adpIdx = args.indexOf('--adapter');
    const adapterOverride = adpIdx >= 0 ? (args[adpIdx + 1] || '').trim().toLowerCase() : null;
    if (adapterOverride && !VALID_ADAPTERS.includes(adapterOverride)) {
      console.error(`[sigmap] --each: unknown adapter "${adapterOverride}"`);
      console.error(`  Valid adapters: ${VALID_ADAPTERS.join(', ')}`);
      process.exit(1);
    }
    runEach(cwd, config, adapterOverride || null);
    process.exit(0);
  }

  // ── --adapter <name> ───────────────────────────────────────────────────────
  if (args.includes('--adapter')) {
    try {
      const adpIdx = args.indexOf('--adapter');
      const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
      const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
      if (!adapterName || adapterName.startsWith('--')) {
        console.error('[sigmap] --adapter requires an adapter name');
        console.error(`  Valid adapters: ${VALID_ADAPTERS.join(', ')}`);
        console.error('  Example: node gen-context.js --adapter copilot');
        process.exit(1);
      }
      if (!VALID_ADAPTERS.includes(adapterName)) {
        console.error(`[sigmap] unknown adapter: "${adapterName}"`);
        console.error(`  Valid adapters: ${VALID_ADAPTERS.join(', ')}`);
        process.exit(1);
      }
      if (args.includes('--json')) {
        const adapterMod = __require('./packages/adapters/' + adapterName);
        const outPath = adapterMod.outputPath(cwd);
        process.stdout.write(JSON.stringify({ adapter: adapterName, outputPath: outPath, version: VERSION }) + '\n');
        process.exit(0);
      }
      // Run generate with this adapter's output targets
      const singleAdapterConfig = Object.assign({}, config, {
        outputs: adapterName === 'claude' ? ['claude'] : [adapterName],
        adapters: [adapterName],
      });
      runGenerate(cwd, singleAdapterConfig, false);
      const adapterMod = __require('./packages/adapters/' + adapterName);
      console.warn(`[sigmap] adapter "${adapterName}" → ${path.relative(cwd, adapterMod.outputPath(cwd))}`);
    } catch (err) {
      console.error(`[sigmap] adapter error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  // ── --impact <file> ────────────────────────────────────────────────────────
  if (args.includes('--impact')) {
    try {
      const impIdx = args.indexOf('--impact');
      const targetFile = (args[impIdx + 1] || '').trim();
      if (!targetFile || targetFile.startsWith('--')) {
        console.error('[sigmap] --impact requires a file path');
        console.error('  Example: node gen-context.js --impact src/extractors/python.js');
        process.exit(1);
      }
      const { analyzeImpact, formatImpact, formatImpactJSON } = requireSourceOrBundled('./src/graph/impact');
      const depthIdx = args.indexOf('--depth');
      const depth = depthIdx >= 0 ? Math.max(0, parseInt(args[depthIdx + 1], 10) || 3) : ((config && config.impact && config.impact.depth) || 3);
      const results = analyzeImpact(targetFile, cwd, { depth });
      if (args.includes('--json')) {
        const out = results.map((r) => formatImpactJSON(r.impact));
        process.stdout.write(JSON.stringify(out.length === 1 ? out[0] : out) + '\n');
      } else {
        for (const r of results) {
          process.stdout.write(formatImpact(r.impact) + '\n');
        }
      }
    } catch (err) {
      console.error(`[sigmap] impact error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  // Method-level call-graph (D4): --callers <symbol> = method blast radius,
  // --callees <symbol> = what it calls. JS/TS + Python, deterministic.
  if (args.includes('--callers') || args.includes('--callees')) {
    try {
      const kind = args.includes('--callees') ? 'callees' : 'callers';
      const flag = kind === 'callees' ? '--callees' : '--callers';
      const symbol = (args[args.indexOf(flag) + 1] || '').trim();
      if (!symbol || symbol.startsWith('--')) {
        console.error(`[sigmap] ${flag} requires a symbol name (or file#symbol)`);
        console.error(`  Example: node gen-context.js ${flag} extractFileDeps`);
        process.exit(1);
      }
      const { methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON } = requireSourceOrBundled('./src/graph/call-graph');
      const depthIdx = args.indexOf('--depth');
      const depth = depthIdx >= 0 ? Math.max(0, parseInt(args[depthIdx + 1], 10) || 0) : 0;
      const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length) ? config.srcDirs : undefined;
      const result = kind === 'callees'
        ? methodCallees(symbol, cwd, { depth, srcDirs })
        : methodImpact(symbol, cwd, { depth, srcDirs });
      if (args.includes('--json')) {
        process.stdout.write(JSON.stringify(formatCallGraphJSON(result, kind)) + '\n');
      } else {
        process.stdout.write(formatCallGraph(result, kind) + '\n');
      }
    } catch (err) {
      console.error(`[sigmap] call-graph error: ${err.message}`);
      process.exit(1);
    }
    process.exit(0);
  }

  if (args.includes('--report')) {
    if (args.includes('--history')) {
      try {
        const { readLog, summarize } = __require('./src/tracking/logger');
        const entries = readLog(cwd);
        const summary = summarize(entries);
        let chartInfo = null;
        if (args.includes('--chart')) {
          const { score } = __require('./src/health/scorer');
          const { renderHistoryCharts } = requireSourceOrBundled('./src/format/dashboard');
          chartInfo = renderHistoryCharts(cwd, score(cwd));
        }
        if (args.includes('--json')) {
          const payload = chartInfo
            ? {
                ...summary,
                chart: {
                  tokenReductionSparkline: chartInfo.tokenReductionSparkline,
                  hitAt5Sparkline: chartInfo.hitAt5Sparkline,
                  summary: chartInfo.summary,
                  charts: chartInfo.charts,
                },
              }
            : summary;
          process.stdout.write(JSON.stringify(payload) + '\n');
        } else {
          console.log('[sigmap] usage history:');
          console.log(`  total runs      : ${summary.totalRuns}`);
          console.log(`  avg reduction   : ${summary.avgReductionPct}%`);
          console.log(`  avg tokens out  : ~${summary.avgFinalTokens}`);
          console.log(`  over-budget runs: ${summary.overBudgetRuns}`);
          if (summary.firstRun) console.log(`  first run       : ${summary.firstRun}`);
          if (summary.lastRun)  console.log(`  last run        : ${summary.lastRun}`);
          if (chartInfo) {
            console.log('');
            console.log(chartInfo.text);
          }
        }
      } catch (err) {
        console.warn(`[sigmap] tracking: ${err.message}`);
      }
      process.exit(0);
    }
    runGenerate(cwd, config, true, args.includes('--json'));
    process.exit(0);
  }

  if (args.includes('--monorepo') || config.monorepo) {
    runMonorepo(cwd, config);
    process.exit(0);
  }

  if (args.includes('--setup')) {
    runGenerate(cwd, config, false);
    installHook(cwd, scriptPath);
    registerMcp(cwd, scriptPath);
    watchMode(cwd, config);
    return; // keep process alive for watch
  }

  if (args.includes('--watch')) {
    runGenerate(cwd, config, false);
    watchMode(cwd, config);
    return; // keep process alive
  }

  if (args.includes('--diff')) {
    const idx = args.indexOf('--diff');
    const maybeBase = args[idx + 1];
    const baseRef = (maybeBase && !maybeBase.startsWith('--')) ? maybeBase : null;
    runDiff(cwd, config, args.includes('--staged'), baseRef);
    process.exit(0);
  }

  // v4.2: `--cost` — show token/$ cost estimate before and after SigMap
  if (args.includes('--cost')) {
    const rawTok = getRawTokenCount(cwd, config);
    runGenerate(cwd, config, false);

    const model = args[args.indexOf('--model') + 1] || 'gpt-4o';
    const rateK = MODEL_COSTS[model] || MODEL_COSTS['gpt-4o'];

    const ctxPath = config.customOutput
      ? path.resolve(cwd, config.customOutput)
      : path.join(cwd, '.github', 'copilot-instructions.md');
    let outTok = 0;
    try { outTok = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}

    const savings = rawTok > 0 ? Math.round((1 - outTok / rawTok) * 100) : 0;
    const costRaw = (rawTok / 1000) * rateK;
    const costCtx = (outTok / 1000) * rateK;

    const out = {
      model,
      rawTokens:     rawTok,
      contextTokens: outTok,
      costRaw:       costRaw.toFixed(4),
      costContext:   costCtx.toFixed(4),
      savingsPct:    savings,
    };

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify(out) + '\n');
    } else {
      console.log(`\n Cost estimate (${model}):`);
      console.log(` Without SigMap : ${rawTok.toLocaleString()} tok  $${out.costRaw}/query`);
      console.log(` With SigMap    : ${outTok.toLocaleString()} tok  $${out.costContext}/query`);
      console.log(` Savings        : ${savings}%  ($${(costRaw - costCtx).toFixed(4)} saved per query)\n`);
    }
    process.exit(0);
  }

  // v4.3: `--ci [--min-coverage N] [--json]` — GitHub Actions exit gate
  if (args.includes('--ci')) {
    const minCovIdx = args.indexOf('--min-coverage');
    const minCoverage = minCovIdx !== -1 ? Math.max(0, Math.min(100, parseInt(args[minCovIdx + 1], 10) || 80)) : 80;

    // Coverage = files actually in context / total source files
    const { buildSigIndex: ciBuildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
    const ciSigIndex  = ciBuildSigIndex(cwd);
    const ciTotal     = buildFileList(cwd, config).length;
    const coveragePct = ciTotal > 0 ? Math.round((ciSigIndex.size / ciTotal) * 100) : 0;

    const pass = coveragePct >= minCoverage;

    if (args.includes('--json')) {
      process.stdout.write(JSON.stringify({ pass, coverage: coveragePct, threshold: minCoverage }) + '\n');
    } else if (pass) {
      console.log(`[sigmap] ✓ CI gate passed — coverage ${coveragePct}% ≥ ${minCoverage}%`);
    } else {
      console.error(`[sigmap] ✗ CI gate FAILED — coverage ${coveragePct}% < ${minCoverage}%`);
      console.error(`  Fix: increase maxTokens or expand srcDirs in gen-context.config.json`);
    }
    process.exit(pass ? 0 : 1);
  }

  // Default: generate once
  runGenerate(cwd, config, false);
}

main();
