/**
 * SET 17 / 37e / 43b(ii) / 45b — Superbuffer: substrate-sourced entropy as named PAGES.
 *
 * Pages under SUPERBUFFER_TOKEN_CAP, exposed per stage:
 *   fill:   substrate + spec + contract + corpus + siblings
 *   build:  spec + contract + constants + frozen + concept + corpus
 *   repair: substrate + finding-evidence + constants + contract + frozen + lessons + concept
 *
 * `spec` is the LITERAL encoded feature text from Parse (not discarded).
 * `constants` are spec literals (statuses/limits/dimensions/format strings) — intent oracle.
 * `concept` is 37f ideation output — context only; never extract code.
 * `substrate` is the SET 45b handoff (omit when no deterministic history).
 */
import type { FeatureNode, FeatureSpec, GeneratedFile } from '../core/types';
import type { IRCaseGen, IRParts } from '../core/irScaffold';
import type { Tier1Lesson } from './substrate';
import { TESTGEN_TEMPLATES } from './testgenOracle.generated';
import type { TestgenTemplate } from './testgenOracle.generated';
import { lookupRepairOracle } from './repairOracle.generated';
import {
  formatSubstrateHandoffPage,
  hasSubstrateHandoffHistory,
  type SubstrateHandoff,
} from './substrateHandoff';

/** Soft token budget for the assembled buffer (~4 chars/token heuristic). */
export const SUPERBUFFER_TOKEN_CAP = 1200;

/** SET 37e / 43b(ii) / 45b — named pages. */
export type SuperbufferPageName =
  | 'substrate'
  | 'spec'
  | 'contract'
  | 'constants'
  | 'frozen'
  | 'corpus'
  | 'siblings'
  | 'concept'
  | 'finding-evidence'
  | 'exhibit'
  | 'trace'
  | 'ledger'
  | 'union'
  | 'lessons';

export type SuperbufferStage = 'fill' | 'build' | 'repair';

export interface SuperbufferPage {
  chars: number;
  text: string;
  approxTokens: number;
}

/** Exposure policy per stage (order = pack priority). Substrate early on fill/repair. */
export const STAGE_PAGE_POLICY: Record<SuperbufferStage, SuperbufferPageName[]> = {
  // SET 75 — the union page (watcher observation) rides every stage; the
  // ledger page (this node's causal trail) is repair-stage evidence.
  // SET 82 — CONTRACT FIRST, advisory last. The union page was packing second
  // on fill and taking more budget than the spec; a small model reads the
  // first thing it is given, so priority inversion is fatal.
  fill: ['substrate', 'spec', 'contract', 'corpus', 'siblings', 'union'],
  build: ['spec', 'contract', 'constants', 'frozen', 'concept', 'corpus', 'union'],
  // SET 64c/64d — exhibit + trace pack early: small, position-bearing evidence.
  repair: ['substrate', 'finding-evidence', 'exhibit', 'trace', 'contract', 'constants', 'ledger', 'union', 'frozen', 'lessons', 'concept'],
};

/** Re-export handoff types for Integration / tests. */
export type { SubstrateHandoff };

/** Soft cap for extracted spec literals on the constants page. */
const SPEC_CONSTANTS_CAP = 24;

/**
 * SET 43b(ii) — extract literal constants from encoded feature/spec text.
 * Pulls quoted strings, ALL_CAPS tokens, status/limit/dimension numerics.
 * Deterministic: dedupe → localeCompare sort → cap.
 */
export function extractSpecConstants(specText: string): string[] {
  const found = new Set<string>();

  for (const m of specText.matchAll(/"([^"\n]{1,80})"/g)) {
    const v = m[1]!.trim();
    if (v) found.add(v);
  }
  for (const m of specText.matchAll(/'([^'\n]{1,80})'/g)) {
    const v = m[1]!.trim();
    if (v) found.add(v);
  }
  for (const m of specText.matchAll(/`([^`\n]{1,80})`/g)) {
    const v = m[1]!.trim();
    if (v) found.add(v);
  }

  // ALL_CAPS tokens (statuses, enum-like names). Skip F1-style feature ids.
  for (const m of specText.matchAll(/\b[A-Z][A-Z0-9_]{1,}\b/g)) {
    const tok = m[0]!;
    if (/^[A-Z]\d+$/.test(tok)) continue;
    found.add(tok);
  }

  // Numerics tied to status/limit/dimension language.
  for (const m of specText.matchAll(
    /\b(?:status(?:es)?|code|limit|max(?:imum)?|min(?:imum)?|size|width|height|depth|timeout|retries?|port|capacity|threshold|ttl|len(?:gth)?|dim(?:ension)?s?)\s*[:=]?\s*(-?\d+(?:\.\d+)?)\b/gi,
  )) {
    found.add(m[1]!);
  }
  // Dimension pairs: 640x480
  for (const m of specText.matchAll(/\b(\d{2,})\s*[x×]\s*(\d{2,})\b/gi)) {
    found.add(`${m[1]}x${m[2]}`);
  }

  return [...found]
    .filter(s => s.length > 0)
    .sort((a, b) => a.localeCompare(b))
    .slice(0, SPEC_CONSTANTS_CAP);
}

export interface SuperbufferContext {
  spec: FeatureSpec;
  feature: FeatureNode;
  parts: IRParts | null;
  stubs: GeneratedFile[];
  lessons: Tier1Lesson[];
  /** Sibling features' frozen IR parts (same run). */
  siblingParts: Map<string, IRParts>;
  /**
   * SET 36d — sibling implementations already in the artifact (build stage).
   * Packed under the same token cap as case siblings.
   */
  siblingImpls?: GeneratedFile[];
  /** Case name or classKey the buffer is for. */
  target: string;
  /** Optional signature for similarity scoring. */
  signature?: { name: string; params: string[]; returnType: string } | null;
  /** SET 37e — stage selects which pages are exposed. Default fill. */
  stage?: SuperbufferStage;
  /** SET 37f — concept page text (ideation draw); context only. */
  conceptText?: string | null;
  /** SET 37e repair — finding evidence page body. */
  findingEvidence?: string | null;
  /**
   * SET 45b — substrate handoff page. Packed only when history exists;
   * null/empty omits the page entirely (no empty-page noise).
   */
  substrateHandoff?: SubstrateHandoff | null;
  /** SET 64c — flawed-exhibit page text (already headed); repair stage only. */
  exhibitText?: string | null;
  /** SET 64d — runtime trace page text (already headed); repair stage only. */
  traceText?: string | null;
  /** SET 75 — supergit lineage for THIS node (already headed): the causal
   * trail that produced the finding, read back out of the ledger. */
  ledgerText?: string | null;
  /** SET 75 — watcher state-of-the-union page (already headed), advisory. */
  unionText?: string | null;
}

export interface SuperbufferAssembly {
  templates: { id: string; family: string; body: string; score: number }[];
  lessons: Tier1Lesson[];
  siblingCases: { featureId: string; name: string; body: string; score: number }[];
  /** SET 36d — sibling impl snippets packed into the buffer. */
  siblingImpls: { path: string; body: string }[];
  frozenCases: { name: string; body: string }[];
  stubSignatures: string[];
  /** Count of stub signatures actually packed into promptBlock (≤ stubSignatures.length). */
  packedStubCount: number;
  /** Approximate tokens under SUPERBUFFER_TOKEN_CAP. */
  approxTokens: number;
  promptBlock: string;
  logLine: string;
  /** SET 37e — named pages actually packed (chars + text). */
  pages: Record<string, SuperbufferPage>;
  /** SET 37e — stage used for exposure policy. */
  stage: SuperbufferStage;
  /** SET 37e — literal encoded feature text (spec page source). */
  specText: string;
}

export interface CladePromotionProposal {
  family: string;
  patternKey: string;
  /** In-run: superbuffer wins for this pattern. Cross-run: consecutive failed runs. */
  wins: number;
  /** Auto-drafted fixture pair (body under test + case body). */
  draft: { stubSketch: string; caseBody: string };
  featureId: string;
  ts: string;
  /**
   * SET 31d — provenance of the signal: an in-run superbuffer win-streak
   * (SET 17 original) vs a cross-run recurrence scan over stored run
   * records (SET 31d). Optional/omitted for pre-31d callers.
   */
  source?: 'in-run-win' | 'cross-run-failure';
  /** SET 31d — run ids contributing to a cross-run failure streak. */
  runIds?: string[];
}

/** Deterministic name-token overlap (Jaccard on alphanumeric tokens). */
export function nameTokenOverlap(a: string, b: string): number {
  const tok = (s: string) => new Set(
    s.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).filter(Boolean),
  );
  const A = tok(a);
  const B = tok(b);
  if (!A.size || !B.size) return 0;
  let inter = 0;
  for (const t of A) if (B.has(t)) inter++;
  return inter / (A.size + B.size - inter);
}

/** Signature-shape similarity: arity match + return-type token overlap + name overlap. */
export function signatureSimilarity(
  sig: { name: string; params: string[]; returnType: string } | null | undefined,
  template: TestgenTemplate,
  symbolHint?: string,
): number {
  if (!sig) {
    return symbolHint ? nameTokenOverlap(symbolHint, template.family) * 0.3 : 0;
  }
  const arity = sig.params.length === template.paramCount ? 1
    : 1 - Math.min(1, Math.abs(sig.params.length - template.paramCount) / 3);
  let typeScore = 0;
  const n = Math.min(sig.params.length, template.paramTypes.length);
  if (n > 0) {
    let hits = 0;
    for (let i = 0; i < n; i++) {
      const a = sig.params[i]!.toLowerCase().replace(/\s+/g, '');
      const b = template.paramTypes[i]!.toLowerCase().replace(/\s+/g, '');
      if (a === b || a.includes(b) || b.includes(a)) hits++;
    }
    typeScore = hits / Math.max(sig.params.length, template.paramTypes.length);
  } else if (sig.params.length === 0 && template.paramCount === 0) {
    typeScore = 1;
  }
  const retA = sig.returnType.toLowerCase().replace(/\s+/g, '');
  const retB = template.returnType.toLowerCase().replace(/\s+/g, '');
  const retScore = retA === retB || retA.includes(retB) || retB.includes(retA) ? 1 : 0;
  const nameScore = Math.max(
    nameTokenOverlap(sig.name, template.family),
    nameTokenOverlap(sig.name, template.id),
    symbolHint ? nameTokenOverlap(symbolHint, template.family) : 0,
  );
  return 0.35 * arity + 0.3 * typeScore + 0.15 * retScore + 0.2 * nameScore;
}

function approxTokens(text: string): number {
  return Math.ceil(text.length / 4);
}

function pageOf(text: string): SuperbufferPage {
  const t = text.trimEnd();
  return { chars: t.length, text: t, approxTokens: approxTokens(t) };
}

/**
 * SET 37e — LITERAL encoded feature text from Parse.
 * This is the denotation page: the feature's own words riding every draw.
 */
export function encodeFeatureSpecText(feature: FeatureNode, spec?: FeatureSpec): string {
  const lines = [
    `Feature ${feature.id}: ${feature.name}`,
    `Description: ${feature.description}`,
    `Edge cases: ${feature.edgeCases.join(' | ') || '(none)'}`,
    `Exports: ${feature.exports.map(e => `${e.symbol} from ${e.file}`).join('; ') || '(none)'}`,
    `Files: ${feature.files.join(', ')}`,
    `Deps: ${feature.deps.join(', ') || '(none)'}`,
    `Test file: ${feature.testFile}`,
  ];
  if (spec?.title) lines.unshift(`Product: ${spec.title}`);
  if (spec?.prompt) lines.push(`Prompt span: ${spec.prompt.slice(0, 400)}`);
  return lines.join('\n');
}

/** Alias — SET 37e naming. */
export const encodeFeatureSpecPage = encodeFeatureSpecText;

/** True when the classKey has no enumerated repair-oracle entry (optional oracle). */
export function isNoHandlerClass(classKey: string | undefined | null): boolean {
  if (!classKey) return true;
  try {
    return lookupRepairOracle(classKey) == null;
  } catch {
    return true;
  }
}

/**
 * SET 37f — strip code fences / export blocks from concept ideation.
 * Concept page is CONTEXT ONLY — never extract code for the artifact.
 */
export function sanitizeConceptPage(raw: string): string {
  let s = raw.trim();
  // Drop fenced code blocks entirely (never trust as code).
  s = s.replace(/```[\s\S]*?```/g, '[code omitted — concept is prose context only]');
  // Soften bare export statements.
  s = s.replace(/^export\s+.+$/gm, '[export omitted]');
  return s.slice(0, 1600);
}

/** SET 37f — high-temp ideation default. */
export const CONCEPT_DRAW_TEMP = 1.1;

/** SET 37f — event when a concept page is stored (Integration emits). */
export function formatConceptDrawEvent(featureId: string, chars: number): string {
  return `concept draw: ${featureId} (${chars} chars, context only)`;
}

/**
 * Assemble a capped superbuffer for a freeform / no-handler target.
 * Deterministic; 0 tokens (aside from optional pre-drawn concept page).
 */
export function assembleSuperbuffer(ctx: SuperbufferContext): SuperbufferAssembly {
  const stage: SuperbufferStage = ctx.stage ?? 'fill';
  const policy = STAGE_PAGE_POLICY[stage];

  const specText = encodeFeatureSpecText(ctx.feature, ctx.spec);

  const frozenCases = (ctx.parts?.cases ?? []).map(c => ({
    name: c.name,
    body: c.body.slice(0, 240),
  }));

  // SET 28c — stub scaffolding always exists by this point.
  // SET 73 — extraction widened (Andy: "there should be more stubs"): the old
  // function regex demanded a return-type annotation, and class stubs yielded
  // one bare `class X` line — untyped functions, arrow-const exports, and
  // every class METHOD were silently dropped from the contract page, so
  // draws flew without the very signatures conformance later rejects against.
  const stubSignatures: string[] = [];
  const stubBodies: string[] = [];
  const seenSigSyms = new Set<string>();
  const pushSig = (sym: string, sig: string): void => {
    if (seenSigSyms.has(sym)) return;
    seenSigSyms.add(sym);
    stubSignatures.push(sig);
  };
  for (const stub of ctx.stubs) {
    stubBodies.push(`--- ${stub.path} ---\n${stub.content.slice(0, 400)}`);
    // Typed-return functions first (richest form wins the dedupe).
    for (const m of stub.content.matchAll(
      /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*:\s*([^{;]+)/g,
    )) {
      pushSig(m[1]!, `${m[1]}(${m[2]}): ${m[3]!.trim()}`);
    }
    // Untyped functions (JS-style stubs, inference-typed TS).
    for (const m of stub.content.matchAll(
      /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/g,
    )) {
      pushSig(m[1]!, `${m[1]}(${m[2]})`);
    }
    // Arrow-const exports: `export const f = (a, b) => …` (typed or not).
    for (const m of stub.content.matchAll(
      /export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::\s*[^=]+)?=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*[^=]+)?=>/g,
    )) {
      pushSig(m[1]!, `${m[1]}(${m[2]}) =>`);
    }
    // Exported classes: name + METHOD signatures from the brace-matched body.
    for (const m of stub.content.matchAll(/export\s+class\s+([A-Za-z_$][\w$]*)/g)) {
      const cls = m[1]!;
      pushSig(cls, `class ${cls}`);
      const braceAt = stub.content.indexOf('{', m.index!);
      if (braceAt < 0) continue;
      let depth = 0;
      let end = stub.content.length;
      for (let i = braceAt; i < stub.content.length; i++) {
        if (stub.content[i] === '{') depth++;
        else if (stub.content[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
      }
      const body = stub.content.slice(braceAt + 1, end);
      for (const mm of body.matchAll(
        /^\s*(?:public\s+|private\s+|protected\s+|static\s+)*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)(?:\s*:\s*([^{\n]+))?\s*\{/gm,
      )) {
        const name = mm[1]!;
        if (['if', 'for', 'while', 'switch', 'catch', 'return'].includes(name)) continue;
        pushSig(`${cls}.${name}`, `${cls}.${name}(${mm[2]})${mm[3] ? `: ${mm[3].trim()}` : ''}`);
      }
    }
    for (const m of stub.content.matchAll(/export\s+interface\s+([A-Za-z_$][\w$]*)/g)) {
      pushSig(`interface ${m[1]}`, `interface ${m[1]}`);
    }
    for (const m of stub.content.matchAll(/export\s+type\s+([A-Za-z_$][\w$]*)\s*=\s*([^;]+);/g)) {
      pushSig(`type ${m[1]}`, `type ${m[1]} = ${m[2]!.trim()}`);
    }
    for (const m of stub.content.matchAll(/export\s+const\s+([A-Za-z_$][\w$]*)\s*:\s*([^=]+)=/g)) {
      pushSig(m[1]!, `${m[1]}: ${m[2]!.trim()}`);
    }
  }

  const symbolHint = ctx.signature?.name
    ?? ctx.feature.exports[0]?.symbol
    ?? ctx.target;

  const scoredTemplates = TESTGEN_TEMPLATES
    .map(t => ({
      id: t.id,
      family: t.family,
      body: t.body.replace(/SYM/g, symbolHint).slice(0, 200),
      score: signatureSimilarity(ctx.signature, t, symbolHint),
    }))
    .filter(t => t.score > 0.15)
    .sort((a, b) => b.score - a.score)
    .slice(0, 6);

  const targetPrefix = ctx.target.split('|')[0] ?? ctx.target;
  const scoredLessons = ctx.lessons
    .filter(l => {
      const p = l.classKey.split('|')[0] ?? '';
      return p === targetPrefix
        || l.classKey.startsWith(targetPrefix)
        || targetPrefix.startsWith(p)
        || nameTokenOverlap(l.classKey, ctx.target) > 0.2;
    })
    .slice(0, 8);

  const siblingCases: SuperbufferAssembly['siblingCases'] = [];
  for (const [fid, parts] of ctx.siblingParts) {
    if (fid === ctx.feature.id) continue;
    for (const c of parts.cases) {
      const score = Math.max(
        nameTokenOverlap(c.name, ctx.target),
        nameTokenOverlap(c.clade?.family ?? '', ctx.target),
        c.clade?.branch === 'freeform' ? 0.1 : 0.05,
      );
      if (score < 0.12) continue;
      siblingCases.push({
        featureId: fid,
        name: c.name,
        body: c.body.slice(0, 200),
        score,
      });
    }
  }
  siblingCases.sort((a, b) => b.score - a.score);
  const topSiblings = siblingCases.slice(0, 6);

  const scoredImpls = (ctx.siblingImpls ?? [])
    .filter(f => !ctx.feature.files.includes(f.path))
    .map(f => ({
      path: f.path,
      body: f.content.slice(0, 280),
      score: Math.max(
        nameTokenOverlap(f.path, ctx.target),
        nameTokenOverlap(f.path, symbolHint),
        nameTokenOverlap(f.content.slice(0, 120), symbolHint),
      ),
    }))
    .filter(f => f.score > 0.05 || (ctx.siblingImpls?.length ?? 0) <= 4)
    .sort((a, b) => b.score - a.score)
    .slice(0, 4);

  // ---- Build full page texts (pre-cap), then pack by stage policy ----
  const contractText = [
    'Page contract — stubs + signatures:',
    stubSignatures.length
      ? stubSignatures.slice(0, 12).map(s => `- ${s}`).join('\n')
      : '(no signatures extracted)',
    stubBodies.slice(0, 4).join('\n'),
  ].join('\n');

  const frozenText = [
    'Page frozen — own cases:',
    ...frozenCases.map(c => `"${c.name}":\n${c.body}`),
  ].join('\n');

  const corpusText = [
    'Page corpus — templates + lessons:',
    ...scoredTemplates.map(t =>
      `Clade template ${t.id} (${t.family}, score=${t.score.toFixed(2)}):\n${t.body}`),
    ...scoredLessons.map(l => `Lesson [${l.classKey}]: ${l.idiom} → ${l.resolution}`),
  ].join('\n');

  const lessonsOnlyText = [
    'Page lessons:',
    ...scoredLessons.map(l => `Lesson [${l.classKey}]: ${l.idiom} → ${l.resolution}`),
  ].join('\n');

  const siblingsText = [
    'Page siblings:',
    ...topSiblings.map(s => `Sibling ${s.featureId} "${s.name}":\n${s.body}`),
    ...scoredImpls.map(impl => `Sibling impl ${impl.path}:\n${impl.body}`),
  ].join('\n');

  const conceptRaw = (ctx.conceptText ?? '').trim();
  const conceptText = conceptRaw
    ? `Page concept — ideation (CONTEXT ONLY, never extract code):\n${sanitizeConceptPage(conceptRaw)}`
    : '';

  const findingText = (ctx.findingEvidence ?? '').trim()
    ? `Page finding-evidence:\n${(ctx.findingEvidence ?? '').trim().slice(0, 600)}`
    : '';

  // SET 43b(ii) — spec literals as intent-oracle page (build/repair).
  const constantItems = extractSpecConstants(specText);
  const constantsText = constantItems.length
    ? [
      'Page constants — spec literals (intent oracle):',
      ...constantItems.map(c => `- ${c}`),
    ].join('\n')
    : '';

  // SET 45b — substrate handoff (omit when no history / empty string).
  const substrateText = hasSubstrateHandoffHistory(ctx.substrateHandoff)
    ? formatSubstrateHandoffPage(ctx.substrateHandoff)
    : '';

  const pageSources: Partial<Record<SuperbufferPageName, string>> = {
    substrate: substrateText,
    spec: `Page spec — encoded feature text:\n${specText}`,
    contract: contractText,
    constants: constantsText,
    frozen: frozenText,
    corpus: corpusText,
    siblings: siblingsText,
    concept: conceptText,
    'finding-evidence': findingText,
    exhibit: (ctx.exhibitText ?? '').trim(),
    trace: (ctx.traceText ?? '').trim(),
    ledger: (ctx.ledgerText ?? '').trim(),
    union: (ctx.unionText ?? '').trim(),
    lessons: lessonsOnlyText,
  };

  // Pack under token cap in policy order.
  const header = [
    'Superbuffer — verified correctness algorithms for similar structures.',
    `Stage: ${stage}. Enumerate / apply the analogous strategy to THIS node.`,
    '',
  ].join('\n');
  let tokens = approxTokens(header);
  const lines: string[] = [header.trimEnd()];
  const pages: Record<string, SuperbufferPage> = {};

  const keptTemplates: typeof scoredTemplates = [];
  const keptLessons: Tier1Lesson[] = [];
  const keptFrozen: typeof frozenCases = [];
  const keptSiblings: typeof topSiblings = [];
  const keptImpls: { path: string; body: string }[] = [];
  let keptStubCount = 0;

  const take = (block: string): boolean => {
    const t = approxTokens(block);
    if (tokens + t > SUPERBUFFER_TOKEN_CAP) {
      // Try a truncated slice to keep the page named even under pressure.
      const budget = Math.max(0, (SUPERBUFFER_TOKEN_CAP - tokens) * 4 - 8);
      if (budget < 40) return false;
      const clipped = block.slice(0, budget) + '\n…';
      lines.push(clipped);
      tokens += approxTokens(clipped);
      return true;
    }
    lines.push(block);
    tokens += t;
    return true;
  };

  for (const name of policy) {
    const src = pageSources[name];
    if (!src || !src.trim()) continue;
    if (!take(src)) continue;
    // Record what was packed (may be truncated in lines; store full page stats from src budget).
    const packed = pageOf(src.slice(0, Math.min(src.length, SUPERBUFFER_TOKEN_CAP * 4)));
    pages[name] = packed;

    if (name === 'contract') {
      keptStubCount = Math.min(stubSignatures.length, 12);
    }
    if (name === 'frozen') {
      keptFrozen.push(...frozenCases);
    }
    if (name === 'corpus') {
      keptTemplates.push(...scoredTemplates);
      keptLessons.push(...scoredLessons);
    }
    if (name === 'lessons') {
      keptLessons.push(...scoredLessons);
    }
    if (name === 'siblings') {
      keptSiblings.push(...topSiblings);
      keptImpls.push(...scoredImpls.map(i => ({ path: i.path, body: i.body })));
    }
  }

  const promptBlock = lines.join('\n');
  const tplNames = [...new Set(keptTemplates.map(t => t.family))];
  const lessonNames = [...new Set(keptLessons.map(l => {
    const head = l.classKey.split('|')[0] ?? l.classKey;
    return head.slice(0, 48);
  }))];
  const pageSizes = Object.entries(pages)
    .map(([n, p]) => `${n}:${p.chars}`)
    .join(', ');
  const tplProv = keptTemplates.length
    ? `${keptTemplates.length} templates [${tplNames.join(', ')}]`
    : `${keptTemplates.length} templates`;
  const lessonProv = keptLessons.length
    ? `${keptLessons.length} lessons [${lessonNames.join(', ')}]`
    : `${keptLessons.length} lessons`;
  const implProv = keptImpls.length ? `, ${keptImpls.length} impls` : '';
  // SET 34f backward-compat: stubs/frozen/templates… immediately after `assembled (`.
  // SET 37e: pages + stage named in the same event.
  const logLine =
    `superbuffer: assembled (`
    + `${keptStubCount} stubs, ${keptFrozen.length} frozen, `
    + `${tplProv}, ${lessonProv}, `
    + `${keptSiblings.length} siblings${implProv}`
    + `; pages [${pageSizes || 'none'}]`
    + `) stage=${stage} for "${ctx.target}"`;

  return {
    templates: keptTemplates,
    lessons: keptLessons,
    siblingCases: keptSiblings,
    siblingImpls: keptImpls,
    frozenCases: keptFrozen,
    stubSignatures,
    packedStubCount: keptStubCount,
    approxTokens: tokens,
    promptBlock,
    logLine,
    pages,
    stage,
    specText,
  };
}

/** Prompt fragment for fixTestCases / repair failureOutput. */
export function formatSuperbufferForPrompt(assembly: SuperbufferAssembly): string {
  return assembly.promptBlock;
}

/** Stable key for freeform pattern compounding (≥3 wins → clade promotion). */
export function freeformPatternKey(opts: {
  classKey?: string;
  caseName?: string;
  cladeFamily?: string;
}): string {
  if (opts.cladeFamily && opts.cladeFamily !== 'freeform') {
    return `family:${opts.cladeFamily}`;
  }
  if (opts.classKey) return `class:${opts.classKey.split('|').slice(0, 2).join('|')}`;
  if (opts.caseName) return `case:${opts.caseName.toLowerCase().replace(/\s+/g, '-')}`;
  return 'freeform:unknown';
}

export function shouldProposeCladePromotion(wins: number): boolean {
  return wins >= 3;
}

export function draftCladePromotion(opts: {
  patternKey: string;
  featureId: string;
  wins: number;
  caseBody?: string;
  stubSketch?: string;
  source?: CladePromotionProposal['source'];
  runIds?: string[];
}): CladePromotionProposal {
  const family = opts.patternKey.replace(/^(family|class|case):/, '').slice(0, 48) || 'freeform-promoted';
  const proposal: CladePromotionProposal = {
    family,
    patternKey: opts.patternKey,
    wins: opts.wins,
    draft: {
      stubSketch: (opts.stubSketch ?? 'export function SYM(...args: unknown[]): unknown { throw new Error("stub"); }').slice(0, 400),
      caseBody: (opts.caseBody ?? 'expect(SYM()).toBeDefined();').slice(0, 400),
    },
    featureId: opts.featureId,
    ts: new Date().toISOString(),
  };
  if (opts.source) proposal.source = opts.source;
  if (opts.runIds) proposal.runIds = opts.runIds;
  return proposal;
}

/** SET 31d — the honesty line for a promotion candidate (in-run win-streak or cross-run loss-streak). */
export function formatCladeCandidateMessage(proposal: CladePromotionProposal): string {
  return proposal.source === 'in-run-win'
    ? `clade-candidate: "${proposal.family}" (won ${proposal.wins} times this run)`
    : `clade-candidate: "${proposal.family}" (failed ${proposal.wins} consecutive runs)`;
}

/** Pick an IRCaseGen-shaped sibling/frozen body as a freeform fill hint (0 tok). */
export function nearestFreeformBodies(parts: IRParts | null, limit = 3): IRCaseGen[] {
  if (!parts) return [];
  return parts.cases
    .filter(c => (c.clade?.branch ?? 'freeform') === 'freeform')
    .slice(0, limit);
}
