Initial commit

This commit is contained in:
Samraaj Bath
2026-06-29 15:11:48 -07:00
commit 66dfdcc58d
404 changed files with 45970 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
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<string, unknown>;
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<unknown>(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 };
}