From 5b99df6463fffc3ee1f645ce341d4321af76b44a Mon Sep 17 00:00:00 2001 From: "Matt (via Claude Code)" Date: Mon, 20 Apr 2026 01:00:24 -0500 Subject: [PATCH 1/4] refactor(frontend): decompose PackageExplorer.tsx (553 -> 197 LOC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PackageExplorer was a 553-LOC god-component mixing file-list building, tree rendering, file viewer, checklist rubric, and the master-detail layout. Split along the obvious seams: packageExplorer/types.ts shared VirtualFile + path helpers packageExplorer/buildMeta.ts pure PACKAGE.md template builder packageExplorer/buildFiles.ts pure partition into installable + metadata (fully unit-testable) packageExplorer/FileTree.tsx collapsible directory widget packageExplorer/GoldStandardChecklist.tsx CLAUDE.md rubric comparison PackageExplorer.tsx (197 LOC) orchestrator: master-detail layout, ExplorerHeader + FileViewer sub-components kept inline Largest submodule is 132 LOC, under the 400-LOC TSX ceiling in docs/clean-code.md §2. Tests ----- - packageExplorer/buildFiles.test.ts (5 tests) — pure tree-shaping logic: SKILL.md first, supporting_files grouping, PACKAGE.md counts, integration_report extraction, _meta/parents/*.md per winner. - PackageExplorer.test.tsx (1 test, jsdom) — smoke test: renders with fixture data, asserts installable + metadata counts + download link. QA: frontend build, lint, format:check, 41 vitest tests (+6) all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/PackageExplorer.test.tsx | 72 +++ frontend/src/components/PackageExplorer.tsx | 536 +++--------------- .../components/packageExplorer/FileTree.tsx | 132 +++++ .../packageExplorer/GoldStandardChecklist.tsx | 102 ++++ .../packageExplorer/buildFiles.test.ts | 129 +++++ .../components/packageExplorer/buildFiles.ts | 117 ++++ .../components/packageExplorer/buildMeta.ts | 82 +++ .../src/components/packageExplorer/types.ts | 26 + 8 files changed, 748 insertions(+), 448 deletions(-) create mode 100644 frontend/src/components/PackageExplorer.test.tsx create mode 100644 frontend/src/components/packageExplorer/FileTree.tsx create mode 100644 frontend/src/components/packageExplorer/GoldStandardChecklist.tsx create mode 100644 frontend/src/components/packageExplorer/buildFiles.test.ts create mode 100644 frontend/src/components/packageExplorer/buildFiles.ts create mode 100644 frontend/src/components/packageExplorer/buildMeta.ts create mode 100644 frontend/src/components/packageExplorer/types.ts diff --git a/frontend/src/components/PackageExplorer.test.tsx b/frontend/src/components/PackageExplorer.test.tsx new file mode 100644 index 0000000..2f5a5ff --- /dev/null +++ b/frontend/src/components/PackageExplorer.test.tsx @@ -0,0 +1,72 @@ +// @vitest-environment jsdom +// +// Smoke test for PackageExplorer — renders with realistic fixture data, +// asserts the master-detail layout reaches the DOM (installable section, +// metadata section, download link). Detailed pure-logic assertions live +// in packageExplorer/buildFiles.test.ts. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import type { RunReportChallenge, RunReportGenome } from "@/types"; + +import PackageExplorer from "./PackageExplorer"; + +const compositeGenome: RunReportGenome = { + id: "composite_abc", + generation: 1, + skill_md_content: "---\nname: composite\n---\n# composite", + frontmatter: { name: "composite", description: "x" }, + supporting_files: { + "scripts/validate.sh": "#!/bin/bash\necho ok", + "references/guide.md": "# guide", + }, + traits: [], + meta_strategy: "engineer_composite", + parent_ids: [], + mutations: [], + mutation_rationale: "", + maturity: "draft", + pareto_objectives: {}, + deterministic_scores: {}, +} as unknown as RunReportGenome; + +const winner: RunReportGenome = { + ...compositeGenome, + id: "gen_seed_streams_winner", + meta_strategy: "seed_pipeline_winner", +}; + +const challenge: RunReportChallenge = { + id: "easy-01", + prompt: "stub", + difficulty: "easy", + evaluation_criteria: {}, + verification_method: "judge_review", + setup_files: {}, + gold_standard_hints: "", +} as unknown as RunReportChallenge; + +describe("PackageExplorer", () => { + it("renders the installable section, metadata section, and download link", () => { + render( + , + ); + + expect(screen.getByText(/Skill package · Pre-download view/)).toBeTruthy(); + // Installable section: SKILL.md + the two supporting files = 3 + expect(screen.getByText(/Installable · 3/)).toBeTruthy(); + // Metadata: PACKAGE.md + REPORT.md + 1 parent + 1 challenge = 4 + expect(screen.getByText(/Evolution metadata · 4/)).toBeTruthy(); + const download = screen.getByRole("link", { name: /Download \.zip/ }); + expect(download.getAttribute("href")).toBe("/api/runs/run-smoke-1/export?format=skill_dir"); + expect(screen.getByText(/Gold Standard Checklist/)).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/PackageExplorer.tsx b/frontend/src/components/PackageExplorer.tsx index e838ac2..bb67c08 100644 --- a/frontend/src/components/PackageExplorer.tsx +++ b/frontend/src/components/PackageExplorer.tsx @@ -2,8 +2,12 @@ import { useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import type { RunReportChallenge, RunReportGenome } from "@/types"; + import CodeViewer from "./CodeViewer"; -import type { RunReportChallenge, RunReportGenome } from "../types"; +import FileTree from "./packageExplorer/FileTree"; +import GoldStandardChecklist from "./packageExplorer/GoldStandardChecklist"; +import { buildFiles } from "./packageExplorer/buildFiles"; interface PackageExplorerProps { compositeSkillMd: string | null; @@ -14,21 +18,6 @@ interface PackageExplorerProps { familyLabel: string; } -type VirtualFile = { - path: string; - content: string; - // "markdown" = render via ReactMarkdown. "code" = render via CodeViewer - // which picks syntax based on file extension. - language: "markdown" | "code"; - // "installable" = part of the downloadable .zip, loaded by Claude. - // "metadata" = evolution artifact, NOT loaded at runtime, kept for audit. - kind: "installable" | "metadata"; -}; - -function detectLanguage(path: string): "markdown" | "code" { - return path.endsWith(".md") ? "markdown" : "code"; -} - /** * Package explorer — separates the real installable skill package from * evolution metadata so a user can see exactly what they'd download and @@ -41,12 +30,11 @@ function detectLanguage(path: string): "markdown" | "code" { * - Metadata section: parents/, challenges/, PACKAGE.md, REPORT.md. * Preserved for audit + browsing but NOT in the deployable zip. * - Gold Standard Checklist: transparent comparison against the Skill - * Authoring Constraints in CLAUDE.md. Shows which files are present - * and which would be produced by a richer evolution pipeline. + * Authoring Constraints in CLAUDE.md. * - * The tree is rendered as a custom flat list grouped by section so we can - * force SKILL.md at the very top without fighting the shared FileTree's - * directories-first sort. + * File list building lives in packageExplorer/buildFiles.ts; the tree + * widget is packageExplorer/FileTree; the checklist is its own component. + * This file is the master-detail orchestrator. */ export default function PackageExplorer({ compositeSkillMd, @@ -56,91 +44,18 @@ export default function PackageExplorer({ runId, familyLabel, }: PackageExplorerProps) { - const { installable, metadata } = useMemo(() => { - const inst: VirtualFile[] = []; - const meta: VirtualFile[] = []; - - // 1. SKILL.md — the one real file that always ships. - inst.push({ - path: "SKILL.md", - content: compositeSkillMd ?? "# (composite SKILL.md not loaded)", - language: "markdown", - kind: "installable", - }); - - // 2. Real supporting_files from the composite genome. These are the - // rich-package artifacts (scripts/*, references/*, test_fixtures/*, - // assets/*) produced by post-assembly enrichment OR by a production - // engine that natively generates rich directory packages. Sorted so - // directories group together visually. - const composite = genomes.find((g) => g.meta_strategy === "engineer_composite"); - const supportingFiles = composite?.supporting_files ?? {}; - const sortedPaths = Object.keys(supportingFiles).sort((a, b) => { - // Group by top-level directory, then alphabetical within. - const dirA = a.split("/")[0]; - const dirB = b.split("/")[0]; - if (dirA !== dirB) return dirA.localeCompare(dirB); - return a.localeCompare(b); - }); - for (const path of sortedPaths) { - inst.push({ - path, - content: supportingFiles[path], - language: detectLanguage(path), - kind: "installable", - }); - } - - // 3. PACKAGE.md — metadata, synthesized. - const winnerGenomes = genomes.filter((g) => g.meta_strategy === "seed_pipeline_winner"); - meta.push({ - path: "_meta/PACKAGE.md", - content: buildPackageMd({ + const { installable, metadata } = useMemo( + () => + buildFiles({ + compositeSkillMd, + genomes, + challenges, + learningLog, runId, familyLabel, - winnerCount: winnerGenomes.length, - challengeCount: challenges.length, - genomeCount: genomes.length, - installableCount: inst.length, }), - language: "markdown", - kind: "metadata", - }); - - // 4. REPORT.md — integration report from learning_log - const reportEntry = learningLog.find((e) => e.startsWith("[integration_report] ")); - if (reportEntry) { - meta.push({ - path: "_meta/REPORT.md", - content: reportEntry.replace("[integration_report] ", ""), - language: "markdown", - kind: "metadata", - }); - } - - // 5. parents/ — 12 winning variant SKILL.md files - for (const g of winnerGenomes) { - const dim = deriveDimensionFromId(g.id); - meta.push({ - path: `_meta/parents/${dim}.md`, - content: g.skill_md_content, - language: "markdown", - kind: "metadata", - }); - } - - // 6. challenges/ — all 24 JSON specs - for (const c of challenges) { - meta.push({ - path: `_meta/challenges/${c.id}.json`, - content: JSON.stringify(c, null, 2), - language: "code", - kind: "metadata", - }); - } - - return { installable: inst, metadata: meta }; - }, [compositeSkillMd, genomes, challenges, learningLog, runId, familyLabel]); + [compositeSkillMd, genomes, challenges, learningLog, runId, familyLabel], + ); const allFiles = useMemo(() => [...installable, ...metadata], [installable, metadata]); @@ -148,110 +63,43 @@ export default function PackageExplorer({ const [selectedPath, setSelectedPath] = useState("SKILL.md"); const selectedFile = allFiles.find((f) => f.path === selectedPath); - // Gold Standard Checklist — compare against the CLAUDE.md Skill Authoring - // Constraints. For each recommended file, is it present in the - // installable set? - const checklist = useMemo(() => { - const present = new Set(installable.map((f) => f.path)); - return [ - { - path: "SKILL.md", - label: "SKILL.md with frontmatter + body", - kind: "required", - present: present.has("SKILL.md"), - }, - { - path: "scripts/validate.sh", - label: "scripts/validate.sh (structural self-check)", - kind: "recommended", - present: present.has("scripts/validate.sh"), - }, - { - path: "scripts/main_helper.py", - label: "scripts/main_helper.py (deterministic helper)", - kind: "recommended", - present: present.has("scripts/main_helper.py"), - }, - { - path: "references/guide.md", - label: "references/guide.md (domain reference)", - kind: "recommended", - present: present.has("references/guide.md"), - }, - { - path: "test_fixtures/", - label: "test_fixtures/ (sample input files)", - kind: "optional", - present: Array.from(present).some((p) => p.startsWith("test_fixtures/")), - }, - { - path: "assets/", - label: "assets/ (templates, configs)", - kind: "optional", - present: Array.from(present).some((p) => p.startsWith("assets/")), - }, - ]; - }, [installable]); - const isMarkdown = selectedFile?.language === "markdown"; - const bodyOnly = useMemo(() => { + const markdownBody = useMemo(() => { if (!selectedFile || !isMarkdown) return ""; + // Strip YAML frontmatter from the rendered view — the browser shows + // the body only, since the frontmatter is already surfaced in the + // header/badge row above. const m = selectedFile.content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); return m?.[1] ?? selectedFile.content; }, [selectedFile, isMarkdown]); return (
- {/* Header with honest framing */} -
-
-
-

- Skill package · Pre-download view -

-

- - {installable.length} installable {installable.length === 1 ? "file" : "files"} - {" "} - ship in the downloadable zip and are loaded by Claude at runtime. {metadata.length}{" "} - additional metadata files are preserved for auditing the evolution process but are NOT - part of the deployable package. -

-
- - ↓ Download .zip - -
-
+ - {/* Master-detail: sectioned file list on left, viewer on right */}
- {/* Gold Standard Checklist */} -
-

- Gold Standard Checklist -

-

- What this package contains vs. the Skill Authoring Constraints in{" "} - CLAUDE.md. -

-
- {checklist.map((item) => ( -
- - {item.present ? "✓" : "○"} - -
-

{item.label}

-

- {item.kind} ·{" "} - {item.present ? "present in this package" : "not generated by this run"} -

-
-
- ))} -
-
+ ); } -interface FileRowProps { - file: VirtualFile; - selected: boolean; - onSelect: () => void; - indent?: number; +interface ExplorerHeaderProps { + installableCount: number; + metadataCount: number; + runId: string; } -function FileRow({ file, selected, onSelect, indent = 0 }: FileRowProps) { - // Show only the basename in the list. - const basename = file.path.split("/").pop() ?? file.path; - const icon = basename.endsWith(".json") ? "{ }" : "📄"; +function ExplorerHeader({ installableCount, metadataCount, runId }: ExplorerHeaderProps) { return ( - +
+
+
+

+ Skill package · Pre-download view +

+

+ + {installableCount} installable {installableCount === 1 ? "file" : "files"} + {" "} + ship in the downloadable zip and are loaded by Claude at runtime. {metadataCount}{" "} + additional metadata files are preserved for auditing the evolution process but are NOT + part of the deployable package. +

+
+ + ↓ Download .zip + +
+
); } -interface FileTreeSectionProps { - files: VirtualFile[]; - selectedPath: string; - onSelect: (path: string) => void; - stripPrefix?: string; - defaultOpen?: boolean; +interface FileViewerProps { + file: { path: string; content: string; kind: "installable" | "metadata" }; + isMarkdown: boolean; + markdownBody: string; } -/** - * Collapsible tree for a section of files. Groups files under their - * top-level subdirectory so ``scripts/*`` and ``references/*`` render as - * collapsible directory groups. Standalone top-level files render above - * the directory groups. - * - * Props: - * - ``stripPrefix``: an optional path prefix to strip from each file - * (e.g. ``"_meta/"`` for the metadata section) before grouping. - * - ``defaultOpen``: whether directories start expanded (true) or - * collapsed (false). - */ -function FileTreeSection({ - files, - selectedPath, - onSelect, - stripPrefix = "", - defaultOpen = true, -}: FileTreeSectionProps) { - const { topLevel, dirs, allDirNames } = useMemo(() => { - const top: VirtualFile[] = []; - const grouped = new Map(); - const dirNames: string[] = []; - for (const f of files) { - const rest = stripPrefix ? f.path.replace(new RegExp(`^${stripPrefix}`), "") : f.path; - if (!rest.includes("/")) { - top.push(f); - } else { - const dirName = rest.split("/")[0]; - if (!grouped.has(dirName)) { - grouped.set(dirName, []); - dirNames.push(dirName); - } - grouped.get(dirName)!.push(f); - } - } - return { topLevel: top, dirs: grouped, allDirNames: dirNames }; - }, [files, stripPrefix]); - - const [openDirs, setOpenDirs] = useState>(() => - defaultOpen ? new Set(allDirNames) : new Set(), - ); - - const toggle = (dir: string) => { - setOpenDirs((prev) => { - const next = new Set(prev); - if (next.has(dir)) next.delete(dir); - else next.add(dir); - return next; - }); - }; - - // The indent for nested items depends on whether we're stripping a prefix. - // Files inside a stripped-prefix path look as if they live at the root so - // directory rows get no extra indent. - const indent = 0; - +function FileViewer({ file, isMarkdown, markdownBody }: FileViewerProps) { return ( -
- {topLevel.map((f) => ( - onSelect(f.path)} - indent={indent} - /> - ))} - {Array.from(dirs.entries()).map(([dir, dirFiles]) => { - const isOpen = openDirs.has(dir); - return ( -
- - {isOpen && - dirFiles.map((f) => ( - onSelect(f.path)} - indent={indent + 1} - /> - ))} + <> +
+

+ {file.path} +

+ {file.kind === "installable" ? ( + + Installable + + ) : ( + + Metadata + + )} +
+
+ {isMarkdown ? ( +
+ {markdownBody}
- ); - })} -
+ ) : ( + + )} +
+ ); } - -function deriveDimensionFromId(id: string): string { - let slug = id - .replace(/^gen_seed_/, "") - .replace(/_winner$/, "") - .replace(/^elixir_phoenix_liveview_?/, ""); - slug = slug.replace(/_/g, "-"); - return slug || id; -} - -interface PackageMdInput { - runId: string; - familyLabel: string; - winnerCount: number; - challengeCount: number; - genomeCount: number; - installableCount: number; -} - -function buildPackageMd({ - runId, - familyLabel, - winnerCount, - challengeCount, - genomeCount, - installableCount, -}: PackageMdInput): string { - return `# ${familyLabel} — Package Metadata - -**Run ID**: \`${runId}\` -**Evolution mode**: atomic -**Generation**: 1 - -## What's actually in the .zip - -${installableCount} installable file${installableCount === 1 ? "" : "s"} (SKILL.md + scripts/, references/, test_fixtures/, assets/) ship in the downloadable package. Everything under -\`_meta/\` — including this file — is preserved for auditing the evolution -process but is NOT loaded by Claude at runtime. - -| Location | Role | -|-----------------|------------------------------------------| -| \`SKILL.md\` | The composite skill — deploy this | -| \`_meta/PACKAGE.md\` | This manifest (metadata only) | -| \`_meta/REPORT.md\` | Engineer integration report | -| \`_meta/parents/*.md\` | ${winnerCount} winning variant sources | -| \`_meta/challenges/*.json\` | ${challengeCount} L1 test specs | - -## Totals - -- **${genomeCount}** SkillGenomes (seeds + winners + composite) -- **${winnerCount}** winning variants merged into the composite -- **${challengeCount}** L1 test challenges sampled - -## How to deploy - -1. Extract the .zip into your project. -2. Place \`SKILL.md\` at \`.claude/skills//SKILL.md\`. -3. Delete the \`_meta/\` directory if present — Claude ignores it, but - removing it keeps your deployment clean. -4. Done. Claude Code will pick up the skill on next restart. - -## What's NOT in this package - -A richer skill would also include: - -- \`scripts/validate.sh\` — structural self-check -- \`scripts/main_helper.py\` — deterministic helper (parser, formatter, generator) -- \`references/guide.md\` — domain reference doc Claude reads on demand -- \`test_fixtures/\` — sample inputs -- \`assets/\` — templates and static resources - -The evolution pipeline that produced this skill only generated prose rules, -not helper scripts or reference files. A production pipeline could add a -\`.claude/skills/scripter/\` agent per dimension to produce those. - -## Generation lineage - -This is **Generation 1** — produced by evolving a pre-existing seed plus -spawned alternatives across 12 dimensions. A re-evolution would produce -Generation 2 with this composite as the seed input. - -For full context on assembly decisions, see \`_meta/REPORT.md\`. -`; -} diff --git a/frontend/src/components/packageExplorer/FileTree.tsx b/frontend/src/components/packageExplorer/FileTree.tsx new file mode 100644 index 0000000..2ce39e2 --- /dev/null +++ b/frontend/src/components/packageExplorer/FileTree.tsx @@ -0,0 +1,132 @@ +/** + * Collapsible file tree for a PackageExplorer section. + * + * Groups files under their top-level subdirectory so ``scripts/*`` and + * ``references/*`` render as collapsible directory groups; standalone + * top-level files render above the directory groups. Pure presentational + * component — no fetches, no side effects beyond the open/closed state. + */ +import { useMemo, useState } from "react"; + +import type { VirtualFile } from "./types"; + +interface FileRowProps { + file: VirtualFile; + selected: boolean; + onSelect: () => void; + indent?: number; +} + +function FileRow({ file, selected, onSelect, indent = 0 }: FileRowProps) { + const basename = file.path.split("/").pop() ?? file.path; + const icon = basename.endsWith(".json") ? "{ }" : "📄"; + return ( + + ); +} + +interface FileTreeProps { + files: VirtualFile[]; + selectedPath: string; + onSelect: (path: string) => void; + /** Optional path prefix to strip before grouping (e.g. ``"_meta/"``). */ + stripPrefix?: string; + /** Whether directories start expanded (true) or collapsed (false). */ + defaultOpen?: boolean; +} + +export default function FileTree({ + files, + selectedPath, + onSelect, + stripPrefix = "", + defaultOpen = true, +}: FileTreeProps) { + const { topLevel, dirs, allDirNames } = useMemo(() => { + const top: VirtualFile[] = []; + const grouped = new Map(); + const dirNames: string[] = []; + for (const f of files) { + const rest = stripPrefix ? f.path.replace(new RegExp(`^${stripPrefix}`), "") : f.path; + if (!rest.includes("/")) { + top.push(f); + } else { + const dirName = rest.split("/")[0]; + if (!grouped.has(dirName)) { + grouped.set(dirName, []); + dirNames.push(dirName); + } + grouped.get(dirName)!.push(f); + } + } + return { topLevel: top, dirs: grouped, allDirNames: dirNames }; + }, [files, stripPrefix]); + + const [openDirs, setOpenDirs] = useState>(() => + defaultOpen ? new Set(allDirNames) : new Set(), + ); + + const toggle = (dir: string) => { + setOpenDirs((prev) => { + const next = new Set(prev); + if (next.has(dir)) next.delete(dir); + else next.add(dir); + return next; + }); + }; + + return ( +
+ {topLevel.map((f) => ( + onSelect(f.path)} + /> + ))} + {Array.from(dirs.entries()).map(([dir, dirFiles]) => { + const isOpen = openDirs.has(dir); + return ( +
+ + {isOpen && + dirFiles.map((f) => ( + onSelect(f.path)} + indent={1} + /> + ))} +
+ ); + })} +
+ ); +} diff --git a/frontend/src/components/packageExplorer/GoldStandardChecklist.tsx b/frontend/src/components/packageExplorer/GoldStandardChecklist.tsx new file mode 100644 index 0000000..7ac9e75 --- /dev/null +++ b/frontend/src/components/packageExplorer/GoldStandardChecklist.tsx @@ -0,0 +1,102 @@ +/** + * Transparent rubric comparing the generated skill package against the + * Skill Authoring Constraints in CLAUDE.md. Pure presentational — + * takes the installable file list and computes presence. + */ +import { useMemo } from "react"; + +import type { VirtualFile } from "./types"; + +interface GoldStandardChecklistProps { + installable: VirtualFile[]; +} + +type ChecklistItem = { + path: string; + label: string; + kind: "required" | "recommended" | "optional"; + present: boolean; +}; + +function buildChecklist(installable: VirtualFile[]): ChecklistItem[] { + const present = new Set(installable.map((f) => f.path)); + const pathStartsWith = (prefix: string) => Array.from(present).some((p) => p.startsWith(prefix)); + + return [ + { + path: "SKILL.md", + label: "SKILL.md with frontmatter + body", + kind: "required", + present: present.has("SKILL.md"), + }, + { + path: "scripts/validate.sh", + label: "scripts/validate.sh (structural self-check)", + kind: "recommended", + present: present.has("scripts/validate.sh"), + }, + { + path: "scripts/main_helper.py", + label: "scripts/main_helper.py (deterministic helper)", + kind: "recommended", + present: present.has("scripts/main_helper.py"), + }, + { + path: "references/guide.md", + label: "references/guide.md (domain reference)", + kind: "recommended", + present: present.has("references/guide.md"), + }, + { + path: "test_fixtures/", + label: "test_fixtures/ (sample input files)", + kind: "optional", + present: pathStartsWith("test_fixtures/"), + }, + { + path: "assets/", + label: "assets/ (templates, configs)", + kind: "optional", + present: pathStartsWith("assets/"), + }, + ]; +} + +export default function GoldStandardChecklist({ installable }: GoldStandardChecklistProps) { + const checklist = useMemo(() => buildChecklist(installable), [installable]); + + return ( +
+

+ Gold Standard Checklist +

+

+ What this package contains vs. the Skill Authoring Constraints in{" "} + CLAUDE.md. +

+
+ {checklist.map((item) => ( +
+ + {item.present ? "✓" : "○"} + +
+

{item.label}

+

+ {item.kind} ·{" "} + {item.present ? "present in this package" : "not generated by this run"} +

+
+
+ ))} +
+
+ ); +} diff --git a/frontend/src/components/packageExplorer/buildFiles.test.ts b/frontend/src/components/packageExplorer/buildFiles.test.ts new file mode 100644 index 0000000..65bf69f --- /dev/null +++ b/frontend/src/components/packageExplorer/buildFiles.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it } from "vitest"; + +import type { RunReportChallenge, RunReportGenome } from "@/types"; + +import { buildFiles } from "./buildFiles"; + +const genome = (over: Partial): RunReportGenome => + ({ + id: "g1", + generation: 0, + skill_md_content: "# stub\n\n## Examples\n**Example 1**\n**Example 2**\n", + frontmatter: {}, + supporting_files: {}, + traits: [], + meta_strategy: "", + parent_ids: [], + mutations: [], + mutation_rationale: "", + maturity: "draft", + pareto_objectives: {}, + deterministic_scores: {}, + ...over, + }) as unknown as RunReportGenome; + +const challenge = (id: string): RunReportChallenge => + ({ + id, + prompt: "stub", + difficulty: "easy", + evaluation_criteria: {}, + verification_method: "judge_review", + setup_files: {}, + gold_standard_hints: "", + }) as unknown as RunReportChallenge; + +describe("buildFiles", () => { + it("puts SKILL.md first in installable and falls back to placeholder when null", () => { + const { installable } = buildFiles({ + compositeSkillMd: null, + genomes: [], + challenges: [], + learningLog: [], + runId: "run-x", + familyLabel: "Fam", + }); + expect(installable[0].path).toBe("SKILL.md"); + expect(installable[0].content).toContain("not loaded"); + }); + + it("adds composite's supporting_files to installable, grouped by directory", () => { + const composite = genome({ + id: "c", + meta_strategy: "engineer_composite", + supporting_files: { + "scripts/validate.sh": "#!/bin/bash\necho ok", + "references/guide.md": "# guide", + "scripts/main.py": "print(1)", + }, + }); + const { installable } = buildFiles({ + compositeSkillMd: "---\nname: x\n---\n# x", + genomes: [composite], + challenges: [], + learningLog: [], + runId: "run-x", + familyLabel: "Fam", + }); + const paths = installable.map((f) => f.path); + expect(paths[0]).toBe("SKILL.md"); + // Directories stay grouped (alphabetical by top-level dir, then by full path) + expect(paths.slice(1)).toEqual([ + "references/guide.md", + "scripts/main.py", + "scripts/validate.sh", + ]); + }); + + it("emits PACKAGE.md into metadata with the right counts", () => { + const winner = genome({ id: "w1", meta_strategy: "seed_pipeline_winner" }); + const composite = genome({ id: "c", meta_strategy: "engineer_composite" }); + const { metadata } = buildFiles({ + compositeSkillMd: "# x", + genomes: [winner, composite], + challenges: [challenge("easy-01"), challenge("easy-02")], + learningLog: [], + runId: "run-x", + familyLabel: "Phoenix LiveView", + }); + const pkg = metadata.find((f) => f.path === "_meta/PACKAGE.md"); + expect(pkg).toBeDefined(); + expect(pkg!.content).toContain("Phoenix LiveView — Package Metadata"); + expect(pkg!.content).toContain("**1** winning variants"); + expect(pkg!.content).toContain("**2** L1 test challenges"); + }); + + it("includes integration_report from learning_log when present", () => { + const { metadata } = buildFiles({ + compositeSkillMd: null, + genomes: [], + challenges: [], + learningLog: ["[integration_report] merged cleanly"], + runId: "run-x", + familyLabel: "Fam", + }); + const report = metadata.find((f) => f.path === "_meta/REPORT.md"); + expect(report).toBeDefined(); + expect(report!.content).toBe("merged cleanly"); + }); + + it("emits one _meta/parents/.md per winning variant", () => { + const winners = [ + genome({ id: "gen_seed_streams_winner", meta_strategy: "seed_pipeline_winner" }), + genome({ id: "gen_seed_routes_winner", meta_strategy: "seed_pipeline_winner" }), + ]; + const { metadata } = buildFiles({ + compositeSkillMd: null, + genomes: winners, + challenges: [], + learningLog: [], + runId: "run-x", + familyLabel: "Fam", + }); + const parents = metadata.filter((f) => f.path.startsWith("_meta/parents/")); + expect(parents.map((p) => p.path)).toEqual([ + "_meta/parents/streams.md", + "_meta/parents/routes.md", + ]); + }); +}); diff --git a/frontend/src/components/packageExplorer/buildFiles.ts b/frontend/src/components/packageExplorer/buildFiles.ts new file mode 100644 index 0000000..db52c1b --- /dev/null +++ b/frontend/src/components/packageExplorer/buildFiles.ts @@ -0,0 +1,117 @@ +/** + * Pure function that synthesizes the virtual file list for a run's + * package browser. Partitions into installable (ships in zip) vs. + * metadata (audit-only, under _meta/). + * + * Extracted from the main component so the tree-shaping logic can be + * tested in isolation and so the component file stays focused on + * rendering. + */ +import type { RunReportChallenge, RunReportGenome } from "@/types"; + +import { buildPackageMd } from "./buildMeta"; +import { deriveDimensionFromId, detectLanguage } from "./types"; +import type { VirtualFile } from "./types"; + +export interface BuildFilesInput { + compositeSkillMd: string | null; + genomes: RunReportGenome[]; + challenges: RunReportChallenge[]; + learningLog: string[]; + runId: string; + familyLabel: string; +} + +export interface BuildFilesOutput { + installable: VirtualFile[]; + metadata: VirtualFile[]; +} + +export function buildFiles({ + compositeSkillMd, + genomes, + challenges, + learningLog, + runId, + familyLabel, +}: BuildFilesInput): BuildFilesOutput { + const installable: VirtualFile[] = []; + const metadata: VirtualFile[] = []; + + // 1. SKILL.md — the one real file that always ships. + installable.push({ + path: "SKILL.md", + content: compositeSkillMd ?? "# (composite SKILL.md not loaded)", + language: "markdown", + kind: "installable", + }); + + // 2. Real supporting_files from the composite genome. Sorted so + // directories group together visually. + const composite = genomes.find((g) => g.meta_strategy === "engineer_composite"); + const supportingFiles = composite?.supporting_files ?? {}; + const sortedPaths = Object.keys(supportingFiles).sort((a, b) => { + const dirA = a.split("/")[0]; + const dirB = b.split("/")[0]; + if (dirA !== dirB) return dirA.localeCompare(dirB); + return a.localeCompare(b); + }); + for (const path of sortedPaths) { + installable.push({ + path, + content: supportingFiles[path], + language: detectLanguage(path), + kind: "installable", + }); + } + + // 3. PACKAGE.md — metadata, synthesized. + const winnerGenomes = genomes.filter((g) => g.meta_strategy === "seed_pipeline_winner"); + metadata.push({ + path: "_meta/PACKAGE.md", + content: buildPackageMd({ + runId, + familyLabel, + winnerCount: winnerGenomes.length, + challengeCount: challenges.length, + genomeCount: genomes.length, + installableCount: installable.length, + }), + language: "markdown", + kind: "metadata", + }); + + // 4. REPORT.md — integration report from learning_log (if present). + const reportEntry = learningLog.find((e) => e.startsWith("[integration_report] ")); + if (reportEntry) { + metadata.push({ + path: "_meta/REPORT.md", + content: reportEntry.replace("[integration_report] ", ""), + language: "markdown", + kind: "metadata", + }); + } + + // 5. parents/ — 12 winning variant SKILL.md files. + for (const g of winnerGenomes) { + const dim = deriveDimensionFromId(g.id); + metadata.push({ + path: `_meta/parents/${dim}.md`, + content: g.skill_md_content, + language: "markdown", + kind: "metadata", + }); + } + + // 6. challenges/ — JSON specs for audit. + for (const c of challenges) { + metadata.push({ + path: `_meta/challenges/${c.id}.json`, + content: JSON.stringify(c, null, 2), + language: "code", + kind: "metadata", + }); + } + + return { installable, metadata }; +} diff --git a/frontend/src/components/packageExplorer/buildMeta.ts b/frontend/src/components/packageExplorer/buildMeta.ts new file mode 100644 index 0000000..2010f12 --- /dev/null +++ b/frontend/src/components/packageExplorer/buildMeta.ts @@ -0,0 +1,82 @@ +/** + * Builds the synthesized PACKAGE.md metadata file that lives under _meta/. + * + * Extracted from the inline template in PackageExplorer so the main + * component stays focused on master-detail rendering. Pure function — + * deterministic output for a given input, no I/O, no side effects. + */ + +export interface PackageMdInput { + runId: string; + familyLabel: string; + winnerCount: number; + challengeCount: number; + genomeCount: number; + installableCount: number; +} + +export function buildPackageMd({ + runId, + familyLabel, + winnerCount, + challengeCount, + genomeCount, + installableCount, +}: PackageMdInput): string { + return `# ${familyLabel} — Package Metadata + +**Run ID**: \`${runId}\` +**Evolution mode**: atomic +**Generation**: 1 + +## What's actually in the .zip + +${installableCount} installable file${installableCount === 1 ? "" : "s"} (SKILL.md + scripts/, references/, test_fixtures/, assets/) ship in the downloadable package. Everything under +\`_meta/\` — including this file — is preserved for auditing the evolution +process but is NOT loaded by Claude at runtime. + +| Location | Role | +|-----------------|------------------------------------------| +| \`SKILL.md\` | The composite skill — deploy this | +| \`_meta/PACKAGE.md\` | This manifest (metadata only) | +| \`_meta/REPORT.md\` | Engineer integration report | +| \`_meta/parents/*.md\` | ${winnerCount} winning variant sources | +| \`_meta/challenges/*.json\` | ${challengeCount} L1 test specs | + +## Totals + +- **${genomeCount}** SkillGenomes (seeds + winners + composite) +- **${winnerCount}** winning variants merged into the composite +- **${challengeCount}** L1 test challenges sampled + +## How to deploy + +1. Extract the .zip into your project. +2. Place \`SKILL.md\` at \`.claude/skills//SKILL.md\`. +3. Delete the \`_meta/\` directory if present — Claude ignores it, but + removing it keeps your deployment clean. +4. Done. Claude Code will pick up the skill on next restart. + +## What's NOT in this package + +A richer skill would also include: + +- \`scripts/validate.sh\` — structural self-check +- \`scripts/main_helper.py\` — deterministic helper (parser, formatter, generator) +- \`references/guide.md\` — domain reference doc Claude reads on demand +- \`test_fixtures/\` — sample inputs +- \`assets/\` — templates and static resources + +The evolution pipeline that produced this skill only generated prose rules, +not helper scripts or reference files. A production pipeline could add a +\`.claude/skills/scripter/\` agent per dimension to produce those. + +## Generation lineage + +This is **Generation 1** — produced by evolving a pre-existing seed plus +spawned alternatives across 12 dimensions. A re-evolution would produce +Generation 2 with this composite as the seed input. + +For full context on assembly decisions, see \`_meta/REPORT.md\`. +`; +} diff --git a/frontend/src/components/packageExplorer/types.ts b/frontend/src/components/packageExplorer/types.ts new file mode 100644 index 0000000..ffa52b1 --- /dev/null +++ b/frontend/src/components/packageExplorer/types.ts @@ -0,0 +1,26 @@ +/** + * Shared types for the PackageExplorer view. + * + * A VirtualFile is a synthesized file entry rendered in the package browser. + * "installable" files end up in the downloadable zip and are loaded by Claude + * at runtime; "metadata" files are evolution artifacts kept for audit only. + */ +export type VirtualFile = { + path: string; + content: string; + language: "markdown" | "code"; + kind: "installable" | "metadata"; +}; + +export function detectLanguage(path: string): "markdown" | "code" { + return path.endsWith(".md") ? "markdown" : "code"; +} + +export function deriveDimensionFromId(id: string): string { + let slug = id + .replace(/^gen_seed_/, "") + .replace(/_winner$/, "") + .replace(/^elixir_phoenix_liveview_?/, ""); + slug = slug.replace(/_/g, "-"); + return slug || id; +} From 51747a58e61d78b6736dbb8ce55aa2f816f33e37 Mon Sep 17 00:00:00 2001 From: "Matt (via Claude Code)" Date: Mon, 20 Apr 2026 01:05:22 -0500 Subject: [PATCH 2/4] refactor(frontend): decompose SpecializationInput.tsx (561 -> 344 LOC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Start an Evolution Run" form was 561 LOC with 12 useState, 5 raw fetch() calls (one useEffect + one submit branching to 3 endpoints), and every layout concern inline. Decomposed along obvious seams: specializationInput/types.ts SourceMode / EvolutionMode / UploadResponse / SeedSummary specializationInput/estimateCost.ts pure time+cost estimator (calibrated from live runs) specializationInput/startEvolution.ts pure API dispatcher — handles all 3 source modes + 4 request shapes through one typed signature specializationInput/SourceModePicker Starting-Point radio grid specializationInput/EvolutionModePicker Auto/Atomic/Classic radio specializationInput/SeedPicker category-filter + seed grid specializationInput/RunEstimateCard footer estimate + submit btn api/hooks/seeds.ts typed useSeeds hook replacing the raw /api/seeds fetch SpecializationInput.tsx orchestrator — state + compose Also inlined small ScratchBody / UploadBody / ForkBody / GeneratedPackageBanner helpers in the main file; they're too parent-coupled to merit their own module. Tests ----- - estimateCost.test.ts: 4 tests pinning the calibration points (5x3 = 53min/$7.50; 2x1 = 9min/$2) and the hrs-vs-min rollover. QA: frontend build, lint, format:check, 45 vitest tests (+4) all green. Largest submodule is 96 LOC, under the 400-LOC TSX ceiling. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/api/hooks/seeds.ts | 18 + .../src/components/SpecializationInput.tsx | 699 ++++++------------ .../EvolutionModePicker.tsx | 56 ++ .../specializationInput/RunEstimateCard.tsx | 69 ++ .../specializationInput/SeedPicker.tsx | 77 ++ .../specializationInput/SourceModePicker.tsx | 66 ++ .../specializationInput/estimateCost.test.ts | 33 + .../specializationInput/estimateCost.ts | 50 ++ .../specializationInput/startEvolution.ts | 96 +++ .../components/specializationInput/types.ts | 35 + 10 files changed, 740 insertions(+), 459 deletions(-) create mode 100644 frontend/src/api/hooks/seeds.ts create mode 100644 frontend/src/components/specializationInput/EvolutionModePicker.tsx create mode 100644 frontend/src/components/specializationInput/RunEstimateCard.tsx create mode 100644 frontend/src/components/specializationInput/SeedPicker.tsx create mode 100644 frontend/src/components/specializationInput/SourceModePicker.tsx create mode 100644 frontend/src/components/specializationInput/estimateCost.test.ts create mode 100644 frontend/src/components/specializationInput/estimateCost.ts create mode 100644 frontend/src/components/specializationInput/startEvolution.ts create mode 100644 frontend/src/components/specializationInput/types.ts diff --git a/frontend/src/api/hooks/seeds.ts b/frontend/src/api/hooks/seeds.ts new file mode 100644 index 0000000..e6347ec --- /dev/null +++ b/frontend/src/api/hooks/seeds.ts @@ -0,0 +1,18 @@ +/** + * React Query hooks for /api/seeds (the curated seed-library registry). + * + * See docs/clean-code.md §8 — no raw fetch() in components. + */ + +import { useQuery } from "@tanstack/react-query"; + +import { apiClient } from "@/api/client"; + +import type { SeedSummary } from "@/components/specializationInput/types"; + +export function useSeeds() { + return useQuery({ + queryKey: ["seeds"] as const, + queryFn: () => apiClient.get("/api/seeds"), + }); +} diff --git a/frontend/src/components/SpecializationInput.tsx b/frontend/src/components/SpecializationInput.tsx index 1f2e8cb..1fdd214 100644 --- a/frontend/src/components/SpecializationInput.tsx +++ b/frontend/src/components/SpecializationInput.tsx @@ -1,57 +1,35 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { useSeeds } from "@/api/hooks/seeds"; + import InviteGate from "./InviteGate"; import ParameterInput from "./ParameterInput"; -import PrimaryButton from "./PrimaryButton"; import SkillUploader from "./SkillUploader"; import SpecAssistantChat from "./SpecAssistantChat"; -import type { EvolveRequest, EvolveResponse } from "../types"; - -type SourceMode = "scratch" | "upload" | "fork"; - -interface UploadResponse { - upload_id: string | null; - filename: string; - valid: boolean; - frontmatter?: Record; - skill_md_content?: string; - supporting_files?: string[]; - errors?: string[]; -} - -interface SeedSummary { - id: string; - title: string; - description: string; - category: string; - difficulty: "easy" | "medium" | "hard"; -} - -const DIFFICULTY_COLOR: Record = { - easy: "text-tertiary", - medium: "text-warning", - hard: "text-error", -}; - -const SOURCE_MODES: { value: SourceMode; label: string; hint: string }[] = [ - { - value: "scratch", - label: "From Scratch", - hint: "Describe a domain and evolve a new Skill from the golden template.", - }, - { - value: "upload", - label: "Upload Existing", - hint: "Bring your own SKILL.md (or zipped Skill dir) and evolve it forward.", - }, - { - value: "fork", - label: "Fork from Registry", - hint: "Pick a curated Gen 0 Skill from the library as your starting point.", - }, -]; +import EvolutionModePicker from "./specializationInput/EvolutionModePicker"; +import RunEstimateCard from "./specializationInput/RunEstimateCard"; +import SeedPicker from "./specializationInput/SeedPicker"; +import SourceModePicker from "./specializationInput/SourceModePicker"; +import { estimateCost } from "./specializationInput/estimateCost"; +import { startEvolution } from "./specializationInput/startEvolution"; +import type { + EvolutionMode, + GeneratedPackage, + SeedSummary, + SourceMode, + UploadResponse, +} from "./specializationInput/types"; +import { DIFFICULTY_COLOR } from "./specializationInput/types"; +/** + * "Start an Evolution Run" form. + * + * Orchestrates three source modes (from scratch / upload / fork registry), + * four parameter knobs, and the run-estimate footer. The fetch layer, form + * sub-views, and pure estimators live in ``specializationInput/*`` so this + * file stays focused on state composition and the submit flow. + */ export default function SpecializationInput() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); @@ -62,147 +40,58 @@ export default function SpecializationInput() { const [populationSize, setPopulationSize] = useState(5); const [numGenerations, setNumGenerations] = useState(3); const [budget, setBudget] = useState(10); - // v2.0 — Auto lets the Taxonomist decide; Atomic and Classic force the mode. - const [evolutionMode, setEvolutionMode] = useState<"auto" | "atomic" | "molecular">("auto"); + const [evolutionMode, setEvolutionMode] = useState("auto"); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [upload, setUpload] = useState(null); const [forkedSeed, setForkedSeed] = useState(null); - const [allSeeds, setAllSeeds] = useState(null); - const [seedCategoryFilter, setSeedCategoryFilter] = useState("all"); const [inviteCode, setInviteCode] = useState(null); - const [generatedPackage, setGeneratedPackage] = useState<{ - skillMdContent: string; - supportingFiles: Record; - specialization: string; - } | null>(null); + const [generatedPackage, setGeneratedPackage] = useState(null); - const handleValidated = useCallback((code: string) => { - setInviteCode(code); - }, []); + const { data: allSeeds = null } = useSeeds(); - // Fetch all seeds once — used by both the ?seed= auto-select path - // AND the inline picker grid when fork mode is active. + // Auto-select the seed named in ?seed= as soon as the library loads. useEffect(() => { - fetch("/api/seeds") - .then((r) => r.json() as Promise) - .then((seeds) => { - setAllSeeds(seeds); - if (seedParam) { - const match = seeds.find((s) => s.id === seedParam); - if (match) { - setForkedSeed(match); - } - } - }) - .catch(() => setAllSeeds([])); - }, [seedParam]); + if (!seedParam || !allSeeds) return; + const match = allSeeds.find((s) => s.id === seedParam); + if (match) setForkedSeed(match); + }, [seedParam, allSeeds]); - const seedCategories = allSeeds - ? ["all", ...Array.from(new Set(allSeeds.map((s) => s.category)))] - : ["all"]; + const handleValidated = useCallback((code: string) => { + setInviteCode(code); + }, []); - const visibleSeeds = - allSeeds?.filter((s) => seedCategoryFilter === "all" || s.category === seedCategoryFilter) ?? - []; + const estimate = useMemo( + () => estimateCost({ populationSize, numGenerations }), + [populationSize, numGenerations], + ); const submit = async () => { setSubmitting(true); setError(null); try { - let res: Response; - if (sourceMode === "scratch") { - if (!specialization.trim() && !generatedPackage) { - throw new Error("Specialization is required"); - } - if (generatedPackage) { - // Use the AI-generated skill package as parent - res = await fetch("/api/evolve/from-parent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - parent_source: "generated", - skill_md_content: generatedPackage.skillMdContent, - supporting_files: generatedPackage.supportingFiles, - specialization: generatedPackage.specialization || specialization, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - }), - }); - } else { - const body: EvolveRequest & { - invite_code?: string; - evolution_mode?: string; - } = { - mode: "domain", - specialization, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - // "auto" maps to undefined so the Taxonomist decides server-side - evolution_mode: evolutionMode === "auto" ? undefined : evolutionMode, - }; - res = await fetch("/api/evolve", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - } - } else if (sourceMode === "upload") { - if (!upload?.upload_id) { - throw new Error("Upload a valid SKILL.md or zip first"); - } - res = await fetch("/api/evolve/from-parent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - parent_source: "upload", - parent_id: upload.upload_id, - specialization: specialization || undefined, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - }), - }); - } else { - // fork - if (!forkedSeed) { - throw new Error("No seed selected to fork"); - } - res = await fetch("/api/evolve/from-parent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - parent_source: "registry", - parent_id: forkedSeed.id, - specialization: specialization || undefined, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - }), - }); - } - - if (!res.ok) { - const text = await res.text(); - throw new Error(`HTTP ${res.status}: ${text}`); - } - const data = (await res.json()) as EvolveResponse; - navigate(`/runs/${data.run_id}`); + const runId = await startEvolution({ + sourceMode, + specialization, + populationSize, + numGenerations, + budget, + evolutionMode, + inviteCode, + upload, + forkedSeedId: forkedSeed?.id ?? null, + generatedPackage, + }); + navigate(`/runs/${runId}`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setSubmitting(false); } }; - // Invite-gated: if no code has been validated yet, render the gate instead - // of the form. The gate handles its own "already validated" fast-path via - // localStorage, so returning users with a saved code see the form directly. + // Invite gate: no code yet => show gate. The gate handles its own + // "already validated" fast-path via localStorage, so returning users + // with a saved code see the form directly. if (inviteCode === null) { return ; } @@ -214,251 +103,52 @@ export default function SpecializationInput() {

Start an Evolution Run

- {/* Source mode toggle */}
-

- Starting Point -

-
- {SOURCE_MODES.map((m) => { - const selected = sourceMode === m.value; - return ( - - ); - })} -
+
- {/* Source-specific body */} {sourceMode === "scratch" && ( -
-

- Specialization Blueprint -

-