import { join } from "node:path"; import { backfillLazyBackgrounds, buildIR, writeIR, type IR } from "../normalize/ir.js"; import { detectSections, type Section } from "../infer/sections.js"; import { extractTokens, tokensToCss, buildTokenResolver, type Tokens } from "../infer/tokens.js"; import { buildColorPalette } from "../infer/semanticTokens.js"; import { recognizePrimitives, inventoryOf } from "../infer/primitives.js"; import { buildAssetGraph, materializeAssets, type AssetGraph } from "../infer/assets.js"; import { buildFontGraph, type FontGraph } from "../infer/fonts.js"; import { buildRecipeReport, recipeReportToMarkdown, type RecipeReport } from "../infer/recipes.js"; import { buildInteractionRecipeReport, interactionRecipeReportToMarkdown, type InteractionRecipeReport } from "../infer/interactionRecipes.js"; import { generateApp, type AppFramework } from "./app.js"; import { buildCodeQualityReport, codeQualityReportToMarkdown, type CodeQualityReport } from "./codeQuality.js"; import { interactionRejectedSet } from "./interactive.js"; import { buildManifest } from "./manifest.js"; import { buildSeoInventory, seoInventoryToMarkdown, type SeoInventory } from "./seo.js"; import { writeJSON, writeText, readJSON, fileExists } from "../util/fsx.js"; import type { CaptureResult } from "../capture/capture.js"; export type GenerateAllResult = { ir: IR; sections: Section[]; tokens: Tokens; assetGraph: AssetGraph; fontGraph: FontGraph; recipeReport: RecipeReport; interactionRecipeReport: InteractionRecipeReport; seoInventory: SeoInventory; codeQuality: CodeQualityReport; manifest: Record; assetsCopied: number; assetsMissing: string[]; }; /** * Pure deterministic generation from a frozen capture: IR → infer → generate app * → emit artifacts. Called by the CLI and twice by the determinism gate; given * the same sourceDir it must produce byte-identical output (rubric Gate 6). */ export function generateAll(opts: { sourceDir: string; capture: CaptureResult; viewports: number[]; // band/gate widths (standard breakpoints) sampleViewports?: number[]; // dense set for size inference (defaults to viewports) url: string; outDir: string; // the generated/ dir ignoreRejectedInteractions?: boolean; // validation retest mode: wire everything before pruning }): GenerateAllResult { const { sourceDir, capture, viewports, url, outDir } = opts; const sampleViewports = opts.sampleViewports ?? viewports; const appDir = join(outDir, "app"); // Stage 5: carry @keyframes only when motion capture ran (motion on by default; // `--no-motion` / the plain static benchmark leave `capture.motion` unset), so // `--no-motion` output is byte-identical to the pre-Stage-5 frozen clone. `!!capture.motion` // is stable across processes (persisted in capture-result.json) ⇒ determinism holds. const ir = buildIR(sourceDir, sampleViewports, { motion: !!capture.motion, bandViewports: viewports }); backfillLazyBackgrounds(ir); writeIR(ir, sourceDir); const sections = detectSections(ir); const tokens = extractTokens(ir); const assetGraph = buildAssetGraph(capture); const fontGraph = buildFontGraph(capture.fontFaces, assetGraph, url); const seoInventory = buildSeoInventory(ir, assetGraph, capture); // Stage 3.5: semantic color tokens (load-bearing). The palette's :root supersedes // the decorative color group; ditto.css references var(--token) for colors. const palette = buildColorPalette(ir); const tokensCss = (palette.css ? palette.css + "\n" : "") + tokensToCss(tokens, true); const tokenResolver = buildTokenResolver(tokens); const primitives = recognizePrimitives(ir); const recipeReport = buildRecipeReport(ir, sections, primitives); const interactionRecipeReport = buildInteractionRecipeReport(ir, sections, capture.interaction); // Patterns the interaction gate previously rejected (don't reproduce) → left static. const rejPath = join(sourceDir, "interaction-rejected.json"); const rejectedSpecs = !opts.ignoreRejectedInteractions && fileExists(rejPath) ? interactionRejectedSet(readJSON(rejPath)) : undefined; // Stage 4.5: component extraction is opt-in, persisted in the source dir at clone // time so every generateAll for this run (deliverable, determinism gate, prune // regen) makes the SAME choice — keeping output deterministic across processes. const optPath = join(sourceDir, "clone-options.json"); const cloneOpts = fileExists(optPath) ? readJSON<{ components?: boolean; humanizeMode?: "tailwind" | "css"; framework?: AppFramework; reflow?: boolean }>(optPath) : {}; const components = !!cloneOpts.components; const humanizeMode = cloneOpts.humanizeMode; // undefined → generateApp default ("tailwind") const gen = generateApp({ ir, assetGraph, fontGraph, appDir, sourceDir, sourceUrl: url, seoInventory, colorVar: palette.varForColor, tokenResolver, primitives, recipeReport, interaction: capture.interaction, rejectedSpecs, components, humanizeMode, framework: cloneOpts.framework, motion: capture.motion, reflow: !!cloneOpts.reflow }, tokensCss); const mat = materializeAssets(assetGraph, sourceDir, join(appDir, "public")); // Stage 4.5: record promoted components (empty when extraction is off). writeJSON(join(outDir, "extracted-components.json"), gen.components); writeJSON(join(outDir, "sections.json"), sections); writeJSON(join(outDir, "tokens.json"), tokens); writeJSON(join(outDir, "assets.json"), assetGraph.entries); writeJSON(join(outDir, "fonts.json"), fontGraph.entries); const inventory = inventoryOf(ir, primitives); writeJSON(join(outDir, "components.json"), inventory); writeJSON(join(outDir, "recipes.json"), recipeReport); writeText(join(outDir, "recipes.md"), recipeReportToMarkdown(recipeReport)); writeJSON(join(outDir, "interaction-recipes.json"), interactionRecipeReport); writeText(join(outDir, "interaction-recipes.md"), interactionRecipeReportToMarkdown(interactionRecipeReport)); writeJSON(join(outDir, "seo.json"), seoInventory); writeText(join(outDir, "seo.md"), seoInventoryToMarkdown(seoInventory)); const codeQuality = buildCodeQualityReport(appDir, recipeReport); writeJSON(join(outDir, "code-quality.json"), codeQuality); writeText(join(outDir, "code-quality.md"), codeQualityReportToMarkdown(codeQuality)); const manifest = buildManifest({ ir, sections, tokens, assetGraph, fontGraph, capture, componentCount: inventory.count }); writeJSON(join(outDir, "manifest.json"), manifest); return { ir, sections, tokens, assetGraph, fontGraph, recipeReport, interactionRecipeReport, seoInventory, codeQuality, manifest, assetsCopied: mat.copied, assetsMissing: mat.missing }; }