import type { EssenceFile, EssenceV4 } from '@decantr/essence-spec';
import { isV4 } from '@decantr/essence-spec';
import type { ContentResolver } from '@decantr/registry';
import { walkIR } from './ir-helpers.js';
import { runPipeline } from './pipeline.js';
import type { IRAppNode, IRNode, IRPageNode, IRPatternNode } from './types.js';

export type ExecutionPackType = 'scaffold' | 'section' | 'page' | 'mutation' | 'review';

export const EXECUTION_PACK_SCHEMA_URLS = {
  scaffold: 'https://decantr.ai/schemas/scaffold-pack.v1.json',
  section: 'https://decantr.ai/schemas/section-pack.v1.json',
  page: 'https://decantr.ai/schemas/page-pack.v1.json',
  mutation: 'https://decantr.ai/schemas/mutation-pack.v1.json',
  review: 'https://decantr.ai/schemas/review-pack.v1.json',
} as const;

export const PACK_MANIFEST_SCHEMA_URL = 'https://decantr.ai/schemas/pack-manifest.v1.json';
export const EXECUTION_PACK_BUNDLE_SCHEMA_URL =
  'https://decantr.ai/schemas/execution-pack-bundle.v1.json';
export const SELECTED_EXECUTION_PACK_SCHEMA_URL =
  'https://decantr.ai/schemas/selected-execution-pack.v1.json';

export interface ExecutionPackTarget {
  platform: 'web';
  framework: string | null;
  runtime: string | null;
  adapter: string;
}

export interface ExecutionPackScope {
  appId: string;
  pageIds: string[];
  patternIds: string[];
}

export interface ExecutionPackExample {
  id: string;
  label: string;
  language: string;
  snippet: string;
}

export interface ExecutionPackAntiPattern {
  id: string;
  summary: string;
  guidance: string;
}

export interface ExecutionPackSuccessCheck {
  id: string;
  label: string;
  severity: 'error' | 'warn' | 'info';
}

export interface ExecutionPackTokenBudget {
  target: number;
  max: number;
  strategy: string[];
}

export interface ExecutionPackBase<TData> {
  $schema: string;
  packVersion: '1.0.0';
  packType: ExecutionPackType;
  objective: string;
  target: ExecutionPackTarget;
  preset: string | null;
  scope: ExecutionPackScope;
  requiredSetup: string[];
  allowedVocabulary: string[];
  examples: ExecutionPackExample[];
  antiPatterns: ExecutionPackAntiPattern[];
  successChecks: ExecutionPackSuccessCheck[];
  tokenBudget: ExecutionPackTokenBudget;
  data: TData;
  renderedMarkdown: string;
}

export interface PackManifestEntry {
  id: string;
  markdown: string;
  json: string;
}

export interface PackManifestSectionEntry extends PackManifestEntry {
  pageIds: string[];
}

export interface PackManifestPageEntry extends PackManifestEntry {
  sectionId: string | null;
  sectionRole: string | null;
}

export interface PackManifestMutationEntry extends PackManifestEntry {
  mutationType: MutationPackKind;
}

export interface ExecutionPackManifest {
  $schema: string;
  version: '1.0.0';
  generatedAt: string;
  scaffold: PackManifestEntry | null;
  review: PackManifestEntry | null;
  sections: PackManifestSectionEntry[];
  pages: PackManifestPageEntry[];
  mutations: PackManifestMutationEntry[];
}

export interface ScaffoldPackRoute {
  pageId: string;
  sectionId?: string;
  path: string;
  patternIds: string[];
  shell?: string;
}

export interface ScaffoldPackData {
  shell: string;
  theme: {
    id: string;
    mode: string;
    shape: string | null;
  };
  routing: 'hash' | 'history' | 'pathname';
  features: string[];
  navigation?: {
    /**
     * true  = palette enabled with implicit defaults.
     * false = not enabled.
     * object = explicit structured contract (see CommandPaletteContract).
     */
    commandPalette: boolean | CommandPaletteContract;
    hotkeys: Array<{
      key: string;
      route?: string;
      label?: string;
      semantics?: HotkeySemantics;
    }>;
    /** Default hotkey semantics applied unless per-hotkey semantics override. */
    hotkeySemantics?: HotkeySemantics;
  };
  routes: ScaffoldPackRoute[];
  /**
   * Required theme decorator contract — surfaced ONCE at the project level
   * here, then referenced (not duplicated) in each section pack. Cold-LLM
   * runs reported the table was duplicated 7-10× across context files
   * (~270-540 redundant lines per scaffold) when each section pack carried
   * its own copy. Centralizing in scaffold-pack — which the cold prompt
   * tells AI to read first — keeps the contract authoritative without the
   * per-section repetition.
   */
  themeDecorators?: ThemeDecoratorEntry[];
}

export interface CommandPaletteContract {
  trigger?: string;
  placeholder?: string;
  width?: string;
  styling?: 'modal' | 'sheet' | 'inline' | 'fullscreen';
  commands?: Array<{
    id: string;
    label: string;
    section?: string;
    hotkey?: string;
    action?: string;
    route?: string;
  }>;
}

export interface HotkeySemantics {
  chord_window_ms?: number;
  input_guard?: boolean;
  modifier_suppression?: boolean;
  match_case?: boolean;
}

export interface ScaffoldExecutionPack extends ExecutionPackBase<ScaffoldPackData> {
  packType: 'scaffold';
}

export interface SectionPackInput {
  id: string;
  role: string;
  shell: string;
  description: string;
  features: string[];
  pageIds: string[];
  navigationItems?: SectionNavigationItemPack[];
  /** Execution-level directives — short imperative rules for this section. */
  directives?: string[];
}

/** Navigation item contract for a section's primary navigation. */
export interface SectionNavigationItemPack {
  label: string;
  route: string;
  icon?: string;
  hotkey?: string;
  active_match?: string;
  badge?: string;
}

export interface SectionPackRoute {
  pageId: string;
  sectionId?: string;
  path: string;
  patternIds: string[];
  shell?: string;
}

export interface SectionPackData {
  sectionId: string;
  role: string;
  shell: string;
  description: string;
  features: string[];
  theme: {
    id: string;
    mode: string;
    shape: string | null;
  };
  routes: SectionPackRoute[];
  /** Contract for items rendered in this section's primary navigation. */
  navigationItems?: SectionNavigationItemPack[];
  /** Execution-level directives emitted into the section-pack rendering. */
  directives?: string[];
  /**
   * Required theme decorator contract surfaced into the compact pack.
   * Mirrors the long-form section-context "Required Theme Decorators" table.
   * Cold prompts instruct AI assistants to read packs first, so the strong
   * decorator contract has to live HERE, not just in the long-form file.
   */
  themeDecorators?: ThemeDecoratorEntry[];
}

/** Compact, render-ready decorator entry for pack contracts. */
export interface ThemeDecoratorEntry {
  /** Decorator class name without leading dot, e.g. "lum-glass". */
  class: string;
  /** Authored intent describing what the decorator does. */
  intent: string;
  /** Authored slot hints listing where to apply it (joined from usage[]). */
  applyTo: string;
}

export interface SectionExecutionPack extends ExecutionPackBase<SectionPackData> {
  packType: 'section';
}

export interface PagePackInput {
  pageId: string;
  shell: string;
  sectionId: string | null;
  sectionRole: string | null;
  features: string[];
  /** Execution-level directives — short imperative rules for this page. */
  directives?: string[];
}

export interface PagePackPattern {
  id: string;
  alias: string;
  preset: string;
  layout: string;
  /**
   * Per-preset prose from the pattern's preset.description. When present,
   * the page-pack renderer emits it as a short description line so cold
   * LLMs read preset-specific guidance instead of having to look up the
   * pattern's root description.
   */
  presetDescription?: string;
  /**
   * Declared interactions from the pattern JSON. Rendered
   * as a checkbox checklist so cold LLMs in generation mode see a
   * hard-edged list they can't categorize as "philosophy". Enforced by
   * decantr check --strict (C5 guard rule).
   */
  interactions?: string[];
}

export interface PagePackData {
  pageId: string;
  path: string;
  shell: string;
  sectionId: string | null;
  sectionRole: string | null;
  features: string[];
  surface: string;
  theme: {
    id: string;
    mode: string;
    shape: string | null;
  };
  wiringSignals: string[];
  patterns: PagePackPattern[];
  /** Execution-level directives emitted into the page-pack rendering. */
  directives?: string[];
}

export interface PageExecutionPack extends ExecutionPackBase<PagePackData> {
  packType: 'page';
}

export type MutationPackKind = 'add-page' | 'modify';

export interface MutationPackData {
  mutationType: MutationPackKind;
  shell: string;
  theme: {
    id: string;
    mode: string;
    shape: string | null;
  };
  routing: 'hash' | 'history' | 'pathname';
  features: string[];
  routes: ScaffoldPackRoute[];
  workflow: string[];
}

export interface MutationExecutionPack extends ExecutionPackBase<MutationPackData> {
  packType: 'mutation';
}

export type ReviewPackKind = 'app';

export interface ReviewPackData {
  reviewType: ReviewPackKind;
  shell: string;
  theme: {
    id: string;
    mode: string;
    shape: string | null;
  };
  routing: 'hash' | 'history' | 'pathname';
  features: string[];
  routes: ScaffoldPackRoute[];
  focusAreas: string[];
  workflow: string[];
}

export interface ReviewExecutionPack extends ExecutionPackBase<ReviewPackData> {
  packType: 'review';
}

export interface ExecutionPackBundle {
  $schema: string;
  generatedAt: string;
  sourceEssenceVersion: string;
  manifest: ExecutionPackManifest;
  scaffold: ScaffoldExecutionPack;
  review: ReviewExecutionPack;
  sections: SectionExecutionPack[];
  pages: PageExecutionPack[];
  mutations: MutationExecutionPack[];
}

export type SelectedExecutionPack =
  | ScaffoldExecutionPack
  | ReviewExecutionPack
  | SectionExecutionPack
  | PageExecutionPack
  | MutationExecutionPack;

export interface ExecutionPackSelector {
  packType: ExecutionPackType;
  id?: string | null;
}

export interface SelectedExecutionPackResponse {
  $schema: string;
  generatedAt: string;
  sourceEssenceVersion: string;
  manifest: ExecutionPackManifest;
  selector: {
    packType: ExecutionPackType;
    id: string | null;
  };
  pack: SelectedExecutionPack;
}

export interface ScaffoldPackBuilderOptions {
  objective?: string;
  target?: Partial<ExecutionPackTarget>;
  preset?: string | null;
  requiredSetup?: string[];
  examples?: ExecutionPackExample[];
  antiPatterns?: ExecutionPackAntiPattern[];
  successChecks?: ExecutionPackSuccessCheck[];
  tokenBudget?: Partial<ExecutionPackTokenBudget>;
  navigation?: {
    commandPalette: boolean | CommandPaletteContract;
    hotkeys: Array<{
      key: string;
      route?: string;
      label?: string;
      semantics?: HotkeySemantics;
    }>;
    hotkeySemantics?: HotkeySemantics;
  };
  /**
   * Required theme decorator contract surfaced into the scaffold pack.
   * The scaffold pack is read first per cold-prompt rules, so the canonical
   * decorator contract belongs here. Section packs reference this without
   * duplicating to avoid the 7-10× repetition cold-LLM runs reported.
   */
  themeDecorators?: ThemeDecoratorEntry[];
}

export interface SectionPackBuilderOptions extends ScaffoldPackBuilderOptions {
  // themeDecorators inherited from base — section packs render a pointer
  // to scaffold-pack rather than re-emitting the full table (1.7.22 dedup).
}
export interface PagePackBuilderOptions extends ScaffoldPackBuilderOptions {}
export interface MutationPackBuilderOptions extends ScaffoldPackBuilderOptions {
  mutationType: MutationPackKind;
  workflow?: string[];
}
export interface ReviewPackBuilderOptions extends ScaffoldPackBuilderOptions {
  reviewType?: ReviewPackKind;
  focusAreas?: string[];
  workflow?: string[];
}

export interface CompileExecutionPackBundleOptions {
  contentRoot?: string;
  overridePaths?: string[];
  resolver?: ContentResolver;
}

const DEFAULT_TARGET: ExecutionPackTarget = {
  platform: 'web',
  framework: null,
  runtime: null,
  adapter: 'generic-web',
};

const DEFAULT_TOKEN_BUDGET: ExecutionPackTokenBudget = {
  target: 1400,
  max: 2200,
  strategy: [
    'Prefer route summaries over repeated prose.',
    'Use compact vocabulary lists instead of large reference tables.',
    'Include only task-relevant examples and checks.',
  ],
};

const DEFAULT_SUCCESS_CHECKS: ExecutionPackSuccessCheck[] = [
  {
    id: 'route-topology',
    label: 'Routes and page IDs match the compiled topology.',
    severity: 'error',
  },
  {
    id: 'shell-consistency',
    label: 'The declared shell contract is preserved unless the task explicitly mutates it.',
    severity: 'error',
  },
  {
    id: 'theme-consistency',
    label: 'Theme identity and mode remain consistent across scaffolded routes.',
    severity: 'warn',
  },
];

const DEFAULT_SECTION_SUCCESS_CHECKS: ExecutionPackSuccessCheck[] = [
  {
    id: 'section-route-coherence',
    label: 'Section pages and routes remain coherent with the compiled topology.',
    severity: 'error',
  },
  {
    id: 'section-shell-consistency',
    label: 'The section shell contract stays consistent across its routes.',
    severity: 'error',
  },
  {
    id: 'section-pattern-coverage',
    label: 'Primary section patterns are represented without adding off-contract filler sections.',
    severity: 'warn',
  },
];

const DEFAULT_PAGE_SUCCESS_CHECKS: ExecutionPackSuccessCheck[] = [
  {
    id: 'page-route-contract',
    label: 'The page keeps the compiled route, shell, and section contract intact.',
    severity: 'error',
  },
  {
    id: 'page-pattern-contract',
    label:
      'The page preserves its primary compiled patterns instead of drifting into unrelated layouts.',
    severity: 'error',
  },
  {
    id: 'page-state-contract',
    label: 'Any declared wiring signals remain coherent with the rendered page structure.',
    severity: 'warn',
  },
];

const DEFAULT_MUTATION_SUCCESS_CHECKS: Record<MutationPackKind, ExecutionPackSuccessCheck[]> = {
  'add-page': [
    {
      id: 'mutation-essence-first',
      label: 'New pages are declared in the essence before any code generation begins.',
      severity: 'error',
    },
    {
      id: 'mutation-shell-contract',
      label:
        'New routes inherit an existing shell and section contract unless the essence changes first.',
      severity: 'error',
    },
    {
      id: 'mutation-refresh',
      label: 'Refresh compiled packs after the mutation so downstream tasks read current topology.',
      severity: 'warn',
    },
  ],
  modify: [
    {
      id: 'mutation-existing-topology',
      label:
        'Modified routes remain coherent with the compiled topology unless the essence changes first.',
      severity: 'error',
    },
    {
      id: 'mutation-theme-contract',
      label: 'Theme, shell, and page identity stay aligned with the current contract during edits.',
      severity: 'error',
    },
    {
      id: 'mutation-page-pack-first',
      label:
        'Route-local edits should start from the compiled page pack rather than improvised structure.',
      severity: 'warn',
    },
  ],
};

const DEFAULT_REVIEW_SUCCESS_CHECKS: ExecutionPackSuccessCheck[] = [
  {
    id: 'review-contract-baseline',
    label:
      'Review findings should use the compiled route, shell, and theme contract as the baseline.',
    severity: 'error',
  },
  {
    id: 'review-evidence',
    label: 'Each critique finding should cite concrete evidence from the generated workspace.',
    severity: 'error',
  },
  {
    id: 'review-remediation',
    label:
      'Suggested fixes should point back to code changes or essence updates when contract drift exists.',
    severity: 'warn',
  },
];

const DEFAULT_REVIEW_ANTI_PATTERNS: ExecutionPackAntiPattern[] = [
  {
    id: 'inline-styles',
    summary: 'Avoid inline style literals as the primary styling path.',
    guidance:
      'Move visual styling into tokens.css and treatments.css instead of component-local style objects.',
  },
  {
    id: 'hardcoded-colors',
    summary: 'Avoid hardcoded color literals.',
    guidance: 'Use CSS variables and theme decorators instead of hex, rgb, or hsl values.',
  },
  {
    id: 'utility-framework-leakage',
    summary: 'Avoid utility-framework leakage as the primary design language.',
    guidance:
      'Prefer compiled Decantr treatments and contract vocabulary over ad hoc utility class stacks.',
  },
];

function collectPatternIds(page: IRPageNode): string[] {
  const patternIds: string[] = [];
  walkIR(page, (node: IRNode) => {
    if (node.type !== 'pattern') return;
    const patternNode = node as IRPatternNode;
    patternIds.push(patternNode.pattern.patternId);
  });
  return [...new Set(patternIds)];
}

function pageIdentity(pageId: string, sectionId?: string | null): string {
  return sectionId ? `${sectionId}/${pageId}` : pageId;
}

function pageFileId(pageId: string, sectionId?: string | null): string {
  return pageIdentity(pageId, sectionId).replace(/[^a-zA-Z0-9._-]+/g, '-');
}

function routePageLabel(route: Pick<ScaffoldPackRoute, 'pageId' | 'sectionId'>): string {
  return route.sectionId ? `${route.sectionId}/${route.pageId}` : route.pageId;
}

function summarizeRoutes(appNode: IRAppNode): ScaffoldPackRoute[] {
  return appNode.routes.flatMap((route) => {
    const pageNode = findPageNode(appNode, route.pageId, route.sectionId);
    if (!pageNode) return [];
    return [
      {
        pageId: pageNode.pageId,
        ...(route.sectionId ? { sectionId: route.sectionId } : {}),
        path: route.path,
        patternIds: collectPatternIds(pageNode),
        shell: route.shell,
      },
    ];
  });
}

function summarizeSectionRoutes(appNode: IRAppNode, input: SectionPackInput): SectionPackRoute[] {
  return summarizeRoutes(appNode).filter(
    (route) =>
      input.pageIds.includes(route.pageId) && (!route.sectionId || route.sectionId === input.id),
  );
}

function summarizePageRoute(
  appNode: IRAppNode,
  pageId: string,
  sectionId?: string | null,
): ScaffoldPackRoute | null {
  return (
    summarizeRoutes(appNode).find(
      (route) =>
        route.pageId === pageId &&
        (!sectionId || !route.sectionId || route.sectionId === sectionId),
    ) ?? null
  );
}

function findPageNode(
  appNode: IRAppNode,
  pageId: string,
  sectionId?: string | null,
): IRPageNode | null {
  const page = appNode.children.find((node) => {
    const pageNode = node as IRPageNode;
    return (
      pageNode.pageId === pageId &&
      (!sectionId || !pageNode.sectionId || pageNode.sectionId === sectionId)
    );
  });
  return page ? (page as IRPageNode) : null;
}

function collectPagePatterns(page: IRPageNode): PagePackPattern[] {
  const patterns: PagePackPattern[] = [];
  walkIR(page, (node: IRNode) => {
    if (node.type !== 'pattern') return;
    const patternNode = node as IRPatternNode;
    patterns.push({
      id: patternNode.pattern.patternId,
      alias: patternNode.pattern.alias,
      preset: patternNode.pattern.preset,
      layout: patternNode.pattern.layout,
      ...(patternNode.pattern.presetDescription
        ? { presetDescription: patternNode.pattern.presetDescription }
        : {}),
      ...(patternNode.pattern.interactions && patternNode.pattern.interactions.length > 0
        ? { interactions: patternNode.pattern.interactions }
        : {}),
    });
  });
  return patterns;
}

function mergeTokenBudget(overrides?: Partial<ExecutionPackTokenBudget>): ExecutionPackTokenBudget {
  return {
    target: overrides?.target ?? DEFAULT_TOKEN_BUDGET.target,
    max: overrides?.max ?? DEFAULT_TOKEN_BUDGET.max,
    strategy: overrides?.strategy ?? DEFAULT_TOKEN_BUDGET.strategy,
  };
}

function renderList(title: string, entries: string[]): string[] {
  if (entries.length === 0) return [];
  return [title, ...entries.map((entry) => `- ${entry}`), ''];
}

// Compact description of hotkey semantics for pack markdown. Returns empty
// string when no semantics are declared so the line can be omitted entirely.
function formatHotkeySemantics(semantics: HotkeySemantics | undefined): string {
  if (!semantics) return '';
  const parts: string[] = [];
  if (typeof semantics.chord_window_ms === 'number') {
    parts.push(`chord window ${semantics.chord_window_ms}ms`);
  }
  if (semantics.input_guard === true) parts.push('suppress during text-input focus');
  if (semantics.input_guard === false) parts.push('fire even while typing (explicit)');
  if (semantics.modifier_suppression === true) parts.push('ignore when modifier held');
  if (semantics.modifier_suppression === false) parts.push('allow modifier passthrough (explicit)');
  if (semantics.match_case === true) parts.push('case-sensitive');
  return parts.join('; ');
}

// Short, mechanical router-implementation hint so the LLM picks the right
// imports without having to consult a separate narrative file. Keyed off the
// same string enum as the schema.
function routingImplementationHint(routing: 'hash' | 'history' | 'pathname'): string {
  switch (routing) {
    case 'hash':
      return 'HashRouter from react-router-dom; URLs prefixed with /# (e.g. /#/login). Only for static-only hosts without SPA fallback.';
    case 'history':
      return 'BrowserRouter from react-router-dom; regular URLs (e.g. /login). Works on Vite dev, Vercel, Netlify, Cloudflare Pages.';
    case 'pathname':
      return 'pathname-based routing (Next.js App Router file conventions).';
    default:
      return String(routing);
  }
}

export function renderExecutionPackMarkdown(pack: ExecutionPackBase<unknown>): string {
  const lines: string[] = [];
  const escapeMarkdownCell = (value: string): string => {
    let escaped = '';
    for (const char of value) {
      if (char === '\\') escaped += '\\\\';
      else if (char === '|') escaped += '\\|';
      else if (char === '\n' || char === '\r') escaped += '<br>';
      else escaped += char;
    }
    return escaped;
  };

  lines.push(`# ${pack.packType.charAt(0).toUpperCase()}${pack.packType.slice(1)} Pack`);
  lines.push('');
  lines.push(`**Objective:** ${pack.objective}`);
  lines.push(
    `**Target:** ${pack.target.adapter}${pack.target.framework ? ` (${pack.target.framework})` : ''}`,
  );
  lines.push(
    `**Scope:** pages=${pack.scope.pageIds.join(', ') || 'none'} | patterns=${pack.scope.patternIds.join(', ') || 'none'}`,
  );
  lines.push('');

  if (pack.packType === 'scaffold') {
    const scaffoldPack = pack as ScaffoldExecutionPack;
    const scaffoldShells = [
      ...new Set(scaffoldPack.data.routes.map((route) => route.shell).filter(Boolean)),
    ];
    lines.push('## Scaffold Contract');
    lines.push(`- Shell: ${scaffoldPack.data.shell}`);
    if (scaffoldShells.length > 1) {
      const secondaryShells = scaffoldShells.filter((shell) => shell !== scaffoldPack.data.shell);
      lines.push(
        `- Shells: ${[`${scaffoldPack.data.shell} (primary)`, ...secondaryShells].join(', ')}`,
      );
    }
    lines.push(`- Theme: ${scaffoldPack.data.theme.id} (${scaffoldPack.data.theme.mode})`);
    lines.push(
      `- Routing: ${scaffoldPack.data.routing} → ${routingImplementationHint(scaffoldPack.data.routing)}`,
    );
    if (scaffoldPack.data.features.length > 0) {
      lines.push(`- Features: ${scaffoldPack.data.features.join(', ')}`);
    }
    if (
      scaffoldPack.data.navigation?.commandPalette ||
      scaffoldPack.data.navigation?.hotkeys.length
    ) {
      lines.push('- Navigation:');
      const cp = scaffoldPack.data.navigation.commandPalette;
      if (cp) {
        if (typeof cp === 'object') {
          lines.push('  - Command palette:');
          if (cp.trigger) lines.push(`    - Trigger: ${cp.trigger}`);
          if (cp.styling)
            lines.push(`    - Styling: ${cp.styling}${cp.width ? ` (width ${cp.width})` : ''}`);
          else if (cp.width) lines.push(`    - Width: ${cp.width}`);
          if (cp.placeholder) lines.push(`    - Placeholder: "${cp.placeholder}"`);
          if (cp.commands && cp.commands.length > 0) {
            lines.push(`    - Commands (${cp.commands.length}):`);
            for (const command of cp.commands) {
              const parts: string[] = [];
              if (command.section) parts.push(command.section);
              parts.push(command.label);
              if (command.hotkey) parts.push(`[${command.hotkey}]`);
              if (command.route) parts.push(`→ ${command.route}`);
              lines.push(`      - ${command.id}: ${parts.join(' · ')}`);
            }
          }
        } else {
          lines.push('  - command palette required');
        }
      }
      const globalSemantics = scaffoldPack.data.navigation.hotkeySemantics;
      if (scaffoldPack.data.navigation.hotkeys.length > 0) {
        const semanticsSummary = formatHotkeySemantics(globalSemantics);
        lines.push(`  - Hotkeys${semanticsSummary ? ` (${semanticsSummary})` : ''}:`);
        for (const hotkey of scaffoldPack.data.navigation.hotkeys) {
          const target = [hotkey.label, hotkey.route].filter(Boolean).join(' — ');
          const perKeySemantics = formatHotkeySemantics(hotkey.semantics);
          const suffix = perKeySemantics ? ` _(${perKeySemantics})_` : '';
          lines.push(`    - ${hotkey.key}${target ? `: ${target}` : ''}${suffix}`);
        }
      }
    }
    lines.push('');

    lines.push('## Route Plan');
    for (const route of scaffoldPack.data.routes) {
      const patterns = route.patternIds.length > 0 ? route.patternIds.join(', ') : 'none';
      lines.push(
        `- ${route.path} -> ${routePageLabel(route)}${route.shell ? ` @ ${route.shell}` : ''} [${patterns}]`,
      );
    }
    lines.push('');

    // Required Theme Decorators — canonical project-level decorator contract
    // (1.7.22). Lives ONCE here in scaffold-pack rather than duplicated across
    // section packs. Cold prompts read scaffold-pack first, so this is the
    // earliest authoritative read of the theme's visual identity contract.
    if (scaffoldPack.data.themeDecorators && scaffoldPack.data.themeDecorators.length > 0) {
      lines.push(`## Required Theme Decorators (${scaffoldPack.data.theme.id})`);
      lines.push('');
      lines.push(
        'These classes carry the active theme\'s visual identity. Tokens alone give bones; decorators give personality. Generated source MUST apply these across all sections — without them, every page reads as "themed colors only" with no theme character. Section packs reference this table; the contract is project-wide.',
      );
      lines.push('');
      lines.push('| Class | Intent | Apply to |');
      lines.push('|-------|--------|----------|');
      for (const entry of scaffoldPack.data.themeDecorators) {
        lines.push(
          `| \`.${entry.class}\` | ${escapeMarkdownCell(entry.intent)} | ${escapeMarkdownCell(entry.applyTo)} |`,
        );
      }
      lines.push('');
    }
  }

  if (pack.packType === 'section') {
    const sectionPack = pack as SectionExecutionPack;
    lines.push('## Section Contract');
    lines.push(`- Section: ${sectionPack.data.sectionId}`);
    lines.push(`- Role: ${sectionPack.data.role}`);
    lines.push(`- Shell: ${sectionPack.data.shell}`);
    lines.push(`- Theme: ${sectionPack.data.theme.id} (${sectionPack.data.theme.mode})`);
    if (sectionPack.data.features.length > 0) {
      lines.push(`- Features: ${sectionPack.data.features.join(', ')}`);
    }
    if (sectionPack.data.description) {
      lines.push(`- Description: ${sectionPack.data.description}`);
    }
    lines.push('');

    lines.push('## Section Routes');
    for (const route of sectionPack.data.routes) {
      const patterns = route.patternIds.length > 0 ? route.patternIds.join(', ') : 'none';
      lines.push(
        `- ${route.path} -> ${routePageLabel(route)}${route.shell ? ` @ ${route.shell}` : ''} [${patterns}]`,
      );
    }
    lines.push('');

    if (sectionPack.data.navigationItems && sectionPack.data.navigationItems.length > 0) {
      lines.push('## Section Navigation');
      lines.push('');
      lines.push(
        "Render these items in the shell's primary navigation. Exact match on label, route, and icon.",
      );
      lines.push('');
      for (const item of sectionPack.data.navigationItems) {
        const parts: string[] = [`${item.label} → ${item.route}`];
        if (item.icon) parts.push(`icon: ${item.icon}`);
        if (item.hotkey) parts.push(`hotkey: ${item.hotkey}`);
        if (item.badge) parts.push(`badge: ${item.badge}`);
        if (item.active_match) parts.push(`active match: \`${item.active_match}\``);
        lines.push(`- ${parts.join(' · ')}`);
      }
      lines.push('');
    }

    if (sectionPack.data.directives && sectionPack.data.directives.length > 0) {
      lines.push('## Section Directives');
      lines.push('');
      lines.push(
        'Execution-level rules every page in this section must obey. Follow exactly — these live in the pack contract, not narrative prose.',
      );
      lines.push('');
      for (const directive of sectionPack.data.directives) {
        lines.push(`- ${directive}`);
      }
      lines.push('');
    }

    // Theme decorators pointer — full table lives in scaffold-pack.md (1.7.22
    // dedup). F2 Phase 1 cold-LLM runs flagged 7-10× duplication of the same
    // table across section packs (~270-540 redundant lines per scaffold). The
    // canonical contract now lives ONCE in scaffold-pack; section packs cite
    // the theme id and point readers there. Section-pack data still carries
    // themeDecorators if explicitly set (legacy support), but the renderer
    // prefers the pointer form unless a section-specific override is needed.
    if (sectionPack.data.themeDecorators && sectionPack.data.themeDecorators.length > 0) {
      // Legacy/override path — render the full table. Kept for back-compat
      // with consumers that still pass themeDecorators directly.
      lines.push(`## Required Theme Decorators (${sectionPack.data.theme.id})`);
      lines.push('');
      lines.push(
        'These classes carry the active theme\'s visual identity. Tokens alone give bones; decorators give personality. Generated source MUST apply these — without them, the page reads as "themed colors only" with no theme character.',
      );
      lines.push('');
      lines.push('| Class | Intent | Apply to |');
      lines.push('|-------|--------|----------|');
      for (const entry of sectionPack.data.themeDecorators) {
        lines.push(
          `| \`.${entry.class}\` | ${escapeMarkdownCell(entry.intent)} | ${escapeMarkdownCell(entry.applyTo)} |`,
        );
      }
      lines.push('');
    } else {
      // Default path (1.7.22+) — pointer to scaffold-pack's canonical table.
      lines.push('## Theme Decorators');
      lines.push('');
      lines.push(
        `Theme \`${sectionPack.data.theme.id}\` decorators are documented ONCE in \`scaffold-pack.md\` under "Required Theme Decorators". Apply them across this section's pages — the contract is the same project-wide. See also DECANTR.md "Decorator Quick Reference" for the same table.`,
      );
      lines.push('');
    }
  }

  if (pack.packType === 'page') {
    const pagePack = pack as PageExecutionPack;
    lines.push('## Page Contract');
    lines.push(`- Page: ${pagePack.data.pageId}`);
    lines.push(`- Path: ${pagePack.data.path}`);
    lines.push(`- Shell: ${pagePack.data.shell}`);
    if (pagePack.data.sectionId) {
      const role = pagePack.data.sectionRole ? ` (${pagePack.data.sectionRole})` : '';
      lines.push(`- Section: ${pagePack.data.sectionId}${role}`);
    }
    lines.push(`- Theme: ${pagePack.data.theme.id} (${pagePack.data.theme.mode})`);
    if (pagePack.data.features.length > 0) {
      lines.push(`- Features: ${pagePack.data.features.join(', ')}`);
    }
    if (pagePack.data.surface) {
      lines.push(`- Surface: ${pagePack.data.surface}`);
    }
    lines.push('');

    lines.push('## Page Patterns');
    for (const pattern of pagePack.data.patterns) {
      lines.push(
        `- ${pattern.alias} -> ${pattern.id} [${pattern.layout}${pattern.preset ? ` | ${pattern.preset}` : ''}]`,
      );
      // Emit preset-specific description on the next line (indented) when
      // the pattern's preset carries its own prose. This stops cold LLMs
      // from falling back to the blueprint-generic root description.
      if (pattern.presetDescription) {
        lines.push(`  > ${pattern.presetDescription}`);
      }
      // Surface declared interactions[] as a checkbox checklist.
      // Hard-edged format — LLMs in generation mode cannot categorize a
      // checkbox as philosophy. Enforced by decantr check --strict (C5).
      if (pattern.interactions && pattern.interactions.length > 0) {
        lines.push(
          `  **Interactions (MUST implement each — see DECANTR.md "Interaction Requirements"):**`,
        );
        for (const interaction of pattern.interactions) {
          lines.push(`  - [ ] ${interaction}`);
        }
      }
    }
    lines.push('');

    if (pagePack.data.wiringSignals.length > 0) {
      lines.push('## Wiring Signals');
      for (const signal of pagePack.data.wiringSignals) {
        lines.push(`- ${signal}`);
      }
      lines.push('');
    }

    if (pagePack.data.directives && pagePack.data.directives.length > 0) {
      lines.push('## Page Directives');
      lines.push('');
      lines.push('Execution-level rules for this route. Follow exactly.');
      lines.push('');
      for (const directive of pagePack.data.directives) {
        lines.push(`- ${directive}`);
      }
      lines.push('');
    }
  }

  if (pack.packType === 'mutation') {
    const mutationPack = pack as MutationExecutionPack;
    lines.push('## Mutation Contract');
    lines.push(`- Operation: ${mutationPack.data.mutationType}`);
    lines.push(`- Shell: ${mutationPack.data.shell}`);
    lines.push(`- Theme: ${mutationPack.data.theme.id} (${mutationPack.data.theme.mode})`);
    lines.push(
      `- Routing: ${mutationPack.data.routing} → ${routingImplementationHint(mutationPack.data.routing)}`,
    );
    if (mutationPack.data.features.length > 0) {
      lines.push(`- Features: ${mutationPack.data.features.join(', ')}`);
    }
    lines.push('');

    lines.push('## Route Topology');
    for (const route of mutationPack.data.routes) {
      const patterns = route.patternIds.length > 0 ? route.patternIds.join(', ') : 'none';
      lines.push(
        `- ${route.path} -> ${routePageLabel(route)}${route.shell ? ` @ ${route.shell}` : ''} [${patterns}]`,
      );
    }
    lines.push('');

    if (mutationPack.data.workflow.length > 0) {
      lines.push('## Workflow');
      for (const step of mutationPack.data.workflow) {
        lines.push(`- ${step}`);
      }
      lines.push('');
    }
  }

  if (pack.packType === 'review') {
    const reviewPack = pack as ReviewExecutionPack;
    lines.push('## Review Contract');
    lines.push(`- Review Type: ${reviewPack.data.reviewType}`);
    lines.push(`- Shell: ${reviewPack.data.shell}`);
    lines.push(`- Theme: ${reviewPack.data.theme.id} (${reviewPack.data.theme.mode})`);
    lines.push(`- Routing: ${reviewPack.data.routing}`);
    if (reviewPack.data.features.length > 0) {
      lines.push(`- Features: ${reviewPack.data.features.join(', ')}`);
    }
    lines.push('');

    lines.push('## Review Topology');
    for (const route of reviewPack.data.routes) {
      const patterns = route.patternIds.length > 0 ? route.patternIds.join(', ') : 'none';
      lines.push(
        `- ${route.path} -> ${routePageLabel(route)}${route.shell ? ` @ ${route.shell}` : ''} [${patterns}]`,
      );
    }
    lines.push('');

    if (reviewPack.data.focusAreas.length > 0) {
      lines.push('## Focus Areas');
      for (const focusArea of reviewPack.data.focusAreas) {
        lines.push(`- ${focusArea}`);
      }
      lines.push('');
    }

    if (reviewPack.data.workflow.length > 0) {
      lines.push('## Review Workflow');
      for (const step of reviewPack.data.workflow) {
        lines.push(`- ${step}`);
      }
      lines.push('');
    }
  }

  // P1-1 — Page packs skip the universal footer (required setup, allowed
  // vocabulary, success checks, anti-patterns, examples, token budget).
  // These blocks are identical across every page in a 16-page scaffold
  // (~35 lines × N pages of pure boilerplate) and their JSON still carries
  // all fields for any consumer that needs the raw data. Non-page packs
  // keep the full footer — each one (scaffold, section, mutation, review)
  // is a singleton per scaffold so there's no duplication problem.
  if (pack.packType === 'page') {
    lines.push('## Shared Contract');
    lines.push(
      'Required setup, allowed vocabulary, success checks, anti-patterns, and token budget are shared across every page pack. The full list lives in the pack JSON sidecar (`page-<id>-pack.json`) and in the pack-manifest. Refer there instead of re-reading the same boilerplate 16 times.',
    );
    lines.push('');
    return lines.join('\n').trimEnd() + '\n';
  }

  lines.push('## Required Setup');
  if (pack.requiredSetup.length === 0) {
    lines.push('- None declared.');
  } else {
    lines.push(...pack.requiredSetup.map((entry) => `- ${entry}`));
  }
  lines.push('');

  lines.push('## Allowed Vocabulary');
  if (pack.allowedVocabulary.length === 0) {
    lines.push('- None declared.');
  } else {
    lines.push(...pack.allowedVocabulary.map((entry) => `- ${entry}`));
  }
  lines.push('');

  lines.push(
    ...renderList(
      '## Success Checks',
      pack.successChecks.map((entry) => `${entry.label} [${entry.severity}]`),
    ),
  );
  lines.push(
    ...renderList(
      '## Anti-Patterns',
      pack.antiPatterns.map((entry) => `${entry.summary}: ${entry.guidance}`),
    ),
  );
  lines.push(
    ...renderList(
      '## Examples',
      pack.examples.map((entry) => `${entry.label} (${entry.language})`),
    ),
  );

  lines.push('## Token Budget');
  lines.push(`- Target: ${pack.tokenBudget.target}`);
  lines.push(`- Max: ${pack.tokenBudget.max}`);
  lines.push(...pack.tokenBudget.strategy.map((entry) => `- ${entry}`));
  lines.push('');

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

export function resolvePackAdapter(
  target: string | undefined,
  platformType: string | undefined,
): string {
  if (target === 'html' || target === 'vanilla' || target === 'javascript' || target === 'js') {
    return 'vanilla-vite';
  }
  if (target === 'react' && platformType === 'spa') return 'react-vite';
  if (target === 'react') return 'react-web';
  if (target === 'vue' && platformType === 'spa') return 'vue-vite';
  if (target === 'svelte' && platformType === 'spa') return 'sveltekit';
  if (target === 'sveltekit') return 'sveltekit';
  if (target === 'angular') return 'angular';
  if (target === 'solid' || target === 'solidjs') return 'solid-vite';
  if (target) return target;
  return 'generic-web';
}

export function listPackSections(essence: EssenceV4): SectionPackInput[] {
  const declaredSections = essence.blueprint.sections;
  const routedSectionPages = new Set(
    Object.values(essence.blueprint.routes ?? {}).map((entry) => `${entry.section}:${entry.page}`),
  );
  return declaredSections
    .map((section) => {
      const pageIds = section.pages
        .filter(
          (page) =>
            routedSectionPages.size === 0 || routedSectionPages.has(`${section.id}:${page.id}`),
        )
        .map((page) => page.id);

      return {
        id: section.id,
        role: section.role,
        shell: section.shell as string,
        description: section.description,
        features: section.features,
        pageIds,
        ...(Array.isArray(section.navigation_items) && section.navigation_items.length > 0
          ? { navigationItems: section.navigation_items as SectionNavigationItemPack[] }
          : {}),
        ...(Array.isArray(section.directives) && section.directives.length > 0
          ? { directives: section.directives }
          : {}),
      };
    })
    .filter((section) => section.pageIds.length > 0);
}

export function listPackPages(essence: EssenceV4): PagePackInput[] {
  const declaredSections = essence.blueprint.sections;
  const routedSectionPages = new Set(
    Object.values(essence.blueprint.routes ?? {}).map((entry) => `${entry.section}:${entry.page}`),
  );
  return declaredSections.flatMap((section) =>
    section.pages
      .map((page) => ({
        pageId: page.id,
        shell: (page.shell_override ?? section.shell) as string,
        sectionId: section.id,
        sectionRole: section.role,
        features: section.features,
        ...(Array.isArray(page.directives) && page.directives.length > 0
          ? { directives: page.directives }
          : {}),
      }))
      .filter(
        (page) =>
          routedSectionPages.size === 0 ||
          routedSectionPages.has(`${page.sectionId}:${page.pageId}`),
      ),
  );
}

function buildPageManifestEntries(pages: PagePackInput[]): PackManifestPageEntry[] {
  const counts = new Map<string, number>();
  for (const page of pages) {
    counts.set(page.pageId, (counts.get(page.pageId) ?? 0) + 1);
  }

  return pages.map((page) => {
    const hasDuplicatePageId = (counts.get(page.pageId) ?? 0) > 1;
    const id = hasDuplicatePageId ? pageIdentity(page.pageId, page.sectionId) : page.pageId;
    const fileId = hasDuplicatePageId ? pageFileId(page.pageId, page.sectionId) : page.pageId;
    return {
      id,
      markdown: `page-${fileId}-pack.md`,
      json: `page-${fileId}-pack.json`,
      sectionId: page.sectionId,
      sectionRole: page.sectionRole,
    };
  });
}

export function buildScaffoldPack(
  appNode: IRAppNode,
  options: ScaffoldPackBuilderOptions = {},
): ScaffoldExecutionPack {
  const routes = summarizeRoutes(appNode);
  const scopePatternIds = [...new Set(routes.flatMap((route) => route.patternIds))];

  const pack: ScaffoldExecutionPack = {
    $schema: EXECUTION_PACK_SCHEMA_URLS.scaffold,
    packVersion: '1.0.0',
    packType: 'scaffold',
    objective:
      options.objective ?? `Scaffold the ${appNode.theme.id} app shell and declared routes.`,
    target: {
      ...DEFAULT_TARGET,
      ...options.target,
    },
    preset: options.preset ?? null,
    scope: {
      appId: appNode.id,
      pageIds: routes.map((route) => route.pageId),
      patternIds: scopePatternIds,
    },
    requiredSetup: options.requiredSetup ?? [
      'Treat the declared routes as the topology source of truth.',
      'Preserve the resolved theme and shell contract unless the task explicitly mutates them.',
    ],
    allowedVocabulary: [
      ...new Set([
        appNode.shell.config.type,
        appNode.theme.id,
        appNode.theme.mode,
        ...appNode.features,
        ...scopePatternIds,
      ]),
    ],
    examples: options.examples ?? [],
    antiPatterns: options.antiPatterns ?? [],
    successChecks: options.successChecks ?? DEFAULT_SUCCESS_CHECKS,
    tokenBudget: mergeTokenBudget(options.tokenBudget),
    data: {
      shell: appNode.shell.config.type,
      theme: {
        id: appNode.theme.id,
        mode: appNode.theme.mode,
        shape: appNode.theme.shape,
      },
      routing: appNode.routing,
      features: appNode.features,
      navigation: options.navigation,
      routes,
      ...(options.themeDecorators && options.themeDecorators.length > 0
        ? { themeDecorators: options.themeDecorators }
        : {}),
    },
    renderedMarkdown: '',
  };

  pack.renderedMarkdown = renderExecutionPackMarkdown(pack);
  return pack;
}

export function buildSectionPack(
  appNode: IRAppNode,
  input: SectionPackInput,
  options: SectionPackBuilderOptions = {},
): SectionExecutionPack {
  const routes = summarizeSectionRoutes(appNode, input);
  const scopePatternIds = [...new Set(routes.flatMap((route) => route.patternIds))];

  const pack: SectionExecutionPack = {
    $schema: EXECUTION_PACK_SCHEMA_URLS.section,
    packVersion: '1.0.0',
    packType: 'section',
    objective:
      options.objective ??
      `Implement the ${input.id} section using the compiled ${input.shell} shell contract.`,
    target: {
      ...DEFAULT_TARGET,
      ...options.target,
    },
    preset: options.preset ?? null,
    scope: {
      appId: appNode.id,
      pageIds: routes.map((route) => route.pageId),
      patternIds: scopePatternIds,
    },
    requiredSetup: options.requiredSetup ?? [
      'Use the declared section routes as the source of truth for this slice of the app.',
      'Keep the section shell consistent unless the task explicitly changes the shell contract.',
    ],
    allowedVocabulary: [
      ...new Set([
        input.id,
        input.role,
        input.shell,
        appNode.theme.id,
        appNode.theme.mode,
        ...input.features,
        ...scopePatternIds,
      ]),
    ],
    examples: options.examples ?? [],
    antiPatterns: options.antiPatterns ?? [],
    successChecks: options.successChecks ?? DEFAULT_SECTION_SUCCESS_CHECKS,
    tokenBudget: mergeTokenBudget(options.tokenBudget),
    data: {
      sectionId: input.id,
      role: input.role,
      shell: input.shell,
      description: input.description,
      features: input.features,
      theme: {
        id: appNode.theme.id,
        mode: appNode.theme.mode,
        shape: appNode.theme.shape,
      },
      routes,
      ...(input.navigationItems && input.navigationItems.length > 0
        ? { navigationItems: input.navigationItems }
        : {}),
      ...(input.directives && input.directives.length > 0 ? { directives: input.directives } : {}),
      ...(options.themeDecorators && options.themeDecorators.length > 0
        ? { themeDecorators: options.themeDecorators }
        : {}),
    },
    renderedMarkdown: '',
  };

  pack.renderedMarkdown = renderExecutionPackMarkdown(pack);
  return pack;
}

export function buildPagePack(
  appNode: IRAppNode,
  input: PagePackInput,
  options: PagePackBuilderOptions = {},
): PageExecutionPack {
  const pageNode = findPageNode(appNode, input.pageId, input.sectionId);
  const route = summarizePageRoute(appNode, input.pageId, input.sectionId);

  if (!pageNode || !route) {
    throw new Error(`Unknown page for page pack: ${input.pageId}`);
  }

  const patterns = collectPagePatterns(pageNode);

  const pack: PageExecutionPack = {
    $schema: EXECUTION_PACK_SCHEMA_URLS.page,
    packVersion: '1.0.0',
    packType: 'page',
    objective:
      options.objective ?? `Implement the ${input.pageId} route using the compiled page contract.`,
    target: {
      ...DEFAULT_TARGET,
      ...options.target,
    },
    preset: options.preset ?? null,
    scope: {
      appId: appNode.id,
      pageIds: [route.pageId],
      patternIds: [...new Set(patterns.map((pattern) => pattern.id))],
    },
    requiredSetup: options.requiredSetup ?? [
      'Keep the compiled route and shell contract stable for this page.',
      'Treat the listed page patterns as the primary structure for this route.',
    ],
    allowedVocabulary: [
      ...new Set(
        [
          input.pageId,
          input.shell,
          input.sectionId ?? '',
          input.sectionRole ?? '',
          appNode.theme.id,
          appNode.theme.mode,
          ...input.features,
          ...patterns.flatMap((pattern) => [pattern.id, pattern.alias, pattern.layout]),
        ].filter(Boolean),
      ),
    ],
    examples: options.examples ?? [],
    antiPatterns: options.antiPatterns ?? [],
    successChecks: options.successChecks ?? DEFAULT_PAGE_SUCCESS_CHECKS,
    tokenBudget: mergeTokenBudget(options.tokenBudget),
    data: {
      pageId: route.pageId,
      path: route.path,
      shell: input.shell,
      sectionId: input.sectionId,
      sectionRole: input.sectionRole,
      features: input.features,
      surface: pageNode.surface,
      theme: {
        id: appNode.theme.id,
        mode: appNode.theme.mode,
        shape: appNode.theme.shape,
      },
      wiringSignals: pageNode.wiring?.signals.map((signal) => signal.name) ?? [],
      patterns,
      ...(input.directives && input.directives.length > 0 ? { directives: input.directives } : {}),
    },
    renderedMarkdown: '',
  };

  pack.renderedMarkdown = renderExecutionPackMarkdown(pack);
  return pack;
}

export function buildMutationPack(
  appNode: IRAppNode,
  options: MutationPackBuilderOptions,
): MutationExecutionPack {
  const routes = summarizeRoutes(appNode);
  const scopePatternIds = [...new Set(routes.flatMap((route) => route.patternIds))];
  const defaultWorkflow =
    options.mutationType === 'add-page'
      ? [
          'Declare the new page in the essence before generating code.',
          'Refresh Decantr context so section and page packs include the new route.',
          'Read the relevant section pack and new page pack before implementation.',
        ]
      : [
          'Read the page pack for the route you are modifying first.',
          'Stop and update the essence before changing route, shell, or pattern contracts.',
          'Validate and check drift after code changes complete.',
        ];

  const pack: MutationExecutionPack = {
    $schema: EXECUTION_PACK_SCHEMA_URLS.mutation,
    packVersion: '1.0.0',
    packType: 'mutation',
    objective:
      options.objective ??
      `Execute the ${options.mutationType} workflow against the compiled app contract.`,
    target: {
      ...DEFAULT_TARGET,
      ...options.target,
    },
    preset: options.preset ?? null,
    scope: {
      appId: appNode.id,
      pageIds: routes.map((route) => route.pageId),
      patternIds: scopePatternIds,
    },
    requiredSetup: options.requiredSetup ?? [
      'Treat the compiled topology as the source of truth until the essence changes.',
      'Refresh Decantr context after structural mutations so downstream tasks read current packs.',
    ],
    allowedVocabulary: [
      ...new Set([
        options.mutationType,
        appNode.shell.config.type,
        appNode.theme.id,
        appNode.theme.mode,
        ...appNode.features,
        ...scopePatternIds,
      ]),
    ],
    examples: options.examples ?? [],
    antiPatterns: options.antiPatterns ?? [],
    successChecks: options.successChecks ?? DEFAULT_MUTATION_SUCCESS_CHECKS[options.mutationType],
    tokenBudget: mergeTokenBudget(options.tokenBudget),
    data: {
      mutationType: options.mutationType,
      shell: appNode.shell.config.type,
      theme: {
        id: appNode.theme.id,
        mode: appNode.theme.mode,
        shape: appNode.theme.shape,
      },
      routing: appNode.routing,
      features: appNode.features,
      routes,
      workflow: options.workflow ?? defaultWorkflow,
    },
    renderedMarkdown: '',
  };

  pack.renderedMarkdown = renderExecutionPackMarkdown(pack);
  return pack;
}

export function buildReviewPack(
  appNode: IRAppNode,
  options: ReviewPackBuilderOptions = {},
): ReviewExecutionPack {
  const routes = summarizeRoutes(appNode);
  const scopePatternIds = [...new Set(routes.flatMap((route) => route.patternIds))];
  const reviewType = options.reviewType ?? 'app';
  const focusAreas = options.focusAreas ?? [
    'route-topology',
    'theme-consistency',
    'treatment-usage',
    'accessibility',
    'responsive-design',
  ];
  const workflow = options.workflow ?? [
    'Read the scaffold pack and page packs before evaluating generated code.',
    'Compare findings against the compiled route, shell, and theme contract first.',
    'Escalate contract drift into essence updates when the requested output intentionally changes topology or theme identity.',
  ];

  const pack: ReviewExecutionPack = {
    $schema: EXECUTION_PACK_SCHEMA_URLS.review,
    packVersion: '1.0.0',
    packType: 'review',
    objective:
      options.objective ?? 'Review generated output against the compiled Decantr contract.',
    target: {
      ...DEFAULT_TARGET,
      ...options.target,
    },
    preset: options.preset ?? null,
    scope: {
      appId: appNode.id,
      pageIds: routes.map((route) => route.pageId),
      patternIds: scopePatternIds,
    },
    requiredSetup: options.requiredSetup ?? [
      'Read the compiled scaffold and route packs before reviewing code.',
      'Use concrete evidence from the workspace instead of purely stylistic intuition.',
    ],
    allowedVocabulary: [
      ...new Set([
        reviewType,
        appNode.shell.config.type,
        appNode.theme.id,
        appNode.theme.mode,
        ...appNode.features,
        ...scopePatternIds,
        ...focusAreas,
      ]),
    ],
    examples: options.examples ?? [],
    antiPatterns: options.antiPatterns ?? DEFAULT_REVIEW_ANTI_PATTERNS,
    successChecks: options.successChecks ?? DEFAULT_REVIEW_SUCCESS_CHECKS,
    tokenBudget: mergeTokenBudget(options.tokenBudget),
    data: {
      reviewType,
      shell: appNode.shell.config.type,
      theme: {
        id: appNode.theme.id,
        mode: appNode.theme.mode,
        shape: appNode.theme.shape,
      },
      routing: appNode.routing,
      features: appNode.features,
      routes,
      focusAreas,
      workflow,
    },
    renderedMarkdown: '',
  };

  pack.renderedMarkdown = renderExecutionPackMarkdown(pack);
  return pack;
}

export async function compileExecutionPackBundle(
  essence: EssenceFile,
  options: CompileExecutionPackBundleOptions = {},
): Promise<ExecutionPackBundle> {
  if (!isV4(essence)) {
    throw new Error(
      'Active Decantr V2 workflows require Essence v4.0.0. Run `decantr migrate --to v4` for older essence files.',
    );
  }

  const generatedAt = new Date().toISOString();
  const sharedTarget = {
    framework: essence.meta.target || null,
    runtime: essence.meta.platform.type || null,
    adapter: resolvePackAdapter(essence.meta.target, essence.meta.platform.type),
  };

  const pipeline = await runPipeline(essence, {
    contentRoot: options.contentRoot,
    overridePaths: options.overridePaths,
    resolver: options.resolver,
  });

  const navMeta = essence.meta.navigation;
  // Preserve the full command_palette value when it's a structured contract;
  // coerce to boolean only when it's the legacy flag form.
  const commandPaletteValue: boolean | CommandPaletteContract =
    typeof navMeta?.command_palette === 'object' && navMeta.command_palette !== null
      ? (navMeta.command_palette as CommandPaletteContract)
      : Boolean(navMeta?.command_palette);

  // Compose theme decorator contract from registry data once and reuse for
  // both scaffold pack (canonical) and section packs (reference). Filter to
  // entries with the minimum data needed to render a useful row.
  const themeDecorators: ThemeDecoratorEntry[] | undefined = (() => {
    const defs = pipeline.registryTheme?.decorator_definitions;
    if (!defs) return undefined;
    const entries: ThemeDecoratorEntry[] = [];
    for (const [name, def] of Object.entries(defs)) {
      const intent = def.intent || def.description || '';
      const applyTo = (def.usage || []).join(', ');
      if (!intent && !applyTo) continue;
      entries.push({ class: name, intent, applyTo });
    }
    return entries.length > 0 ? entries : undefined;
  })();

  const scaffold = buildScaffoldPack(pipeline.ir, {
    target: sharedTarget,
    navigation: {
      commandPalette: commandPaletteValue,
      hotkeys: Array.isArray(navMeta?.hotkeys)
        ? navMeta.hotkeys
            .filter(
              (
                hotkey,
              ): hotkey is {
                key: string;
                route?: string;
                action?: string;
                label: string;
                semantics?: HotkeySemantics;
              } =>
                Boolean(
                  hotkey && typeof hotkey.key === 'string' && typeof hotkey.label === 'string',
                ),
            )
            .map((hotkey) => ({
              key: hotkey.key,
              route: hotkey.route,
              label: hotkey.label,
              ...(hotkey.semantics ? { semantics: hotkey.semantics as HotkeySemantics } : {}),
            }))
        : [],
      ...(navMeta?.hotkey_semantics
        ? { hotkeySemantics: navMeta.hotkey_semantics as HotkeySemantics }
        : {}),
    },
    ...(themeDecorators ? { themeDecorators } : {}),
  });
  const review = buildReviewPack(pipeline.ir, {
    target: sharedTarget,
  });
  const sectionInputs = listPackSections(essence);
  const pageInputs = listPackPages(essence);

  const sections = sectionInputs.map((section) =>
    buildSectionPack(pipeline.ir, section, {
      target: sharedTarget,
      // themeDecorators intentionally NOT passed to section packs as of 1.7.22.
      // Section packs render a one-line pointer to scaffold-pack instead of
      // duplicating the full decorator table — F2 Phase 1 reports flagged
      // 7-10× duplication across context files. The decorators are still
      // available on each section pack as section-pack.theme.id allows the
      // renderer to compose the pointer with the correct theme name.
    }),
  );
  const pages = pageInputs.map((page) =>
    buildPagePack(pipeline.ir, page, {
      target: sharedTarget,
    }),
  );
  const mutations = (['add-page', 'modify'] as const).map((mutationType) =>
    buildMutationPack(pipeline.ir, {
      mutationType,
      target: sharedTarget,
    }),
  );

  const manifest: ExecutionPackManifest = {
    $schema: PACK_MANIFEST_SCHEMA_URL,
    version: '1.0.0',
    generatedAt,
    scaffold: {
      id: 'scaffold',
      markdown: 'scaffold-pack.md',
      json: 'scaffold-pack.json',
    },
    review: {
      id: 'review',
      markdown: 'review-pack.md',
      json: 'review-pack.json',
    },
    sections: sectionInputs.map((section) => ({
      id: section.id,
      markdown: `section-${section.id}-pack.md`,
      json: `section-${section.id}-pack.json`,
      pageIds: section.pageIds,
    })),
    pages: buildPageManifestEntries(pageInputs),
    mutations: (['add-page', 'modify'] as const).map((mutationType) => ({
      id: mutationType,
      markdown: `mutation-${mutationType}-pack.md`,
      json: `mutation-${mutationType}-pack.json`,
      mutationType,
    })),
  };

  return {
    $schema: EXECUTION_PACK_BUNDLE_SCHEMA_URL,
    generatedAt,
    sourceEssenceVersion: essence.version,
    manifest,
    scaffold,
    review,
    sections,
    pages,
    mutations,
  };
}

export function selectExecutionPackFromBundle(
  bundle: ExecutionPackBundle,
  selector: ExecutionPackSelector,
): SelectedExecutionPackResponse | null {
  const id = selector.id ?? null;
  let pack: SelectedExecutionPack | null = null;

  switch (selector.packType) {
    case 'scaffold':
      pack = bundle.scaffold;
      break;
    case 'review':
      pack = bundle.review;
      break;
    case 'section':
      pack = id ? (bundle.sections.find((entry) => entry.data.sectionId === id) ?? null) : null;
      break;
    case 'page':
      pack = id
        ? (bundle.pages.find(
            (entry) =>
              entry.data.pageId === id ||
              pageIdentity(entry.data.pageId, entry.data.sectionId) === id ||
              pageFileId(entry.data.pageId, entry.data.sectionId) === id,
          ) ?? null)
        : null;
      break;
    case 'mutation':
      pack = id ? (bundle.mutations.find((entry) => entry.data.mutationType === id) ?? null) : null;
      break;
    default:
      pack = null;
      break;
  }

  if (!pack) {
    return null;
  }

  return {
    $schema: SELECTED_EXECUTION_PACK_SCHEMA_URL,
    generatedAt: bundle.generatedAt,
    sourceEssenceVersion: bundle.sourceEssenceVersion,
    manifest: bundle.manifest,
    selector: {
      packType: selector.packType,
      id,
    },
    pack,
  };
}

export async function compileSelectedExecutionPack(
  essence: EssenceFile,
  selector: ExecutionPackSelector,
  options: CompileExecutionPackBundleOptions = {},
): Promise<SelectedExecutionPackResponse | null> {
  const bundle = await compileExecutionPackBundle(essence, options);
  return selectExecutionPackFromBundle(bundle, selector);
}
