From 99d81b7dadf0874c600134aa918fd741e30402c9 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 21:03:11 -0700 Subject: [PATCH] Complete Step 478: scaffold file generation --- editor/CMakeLists.txt | 9 + editor/src/ArchitectScaffoldGenerator.h | 358 ++++++++++++++++++++++++ editor/tests/step478_test.cpp | 224 +++++++++++++++ progress.md | 41 +++ 4 files changed, 632 insertions(+) create mode 100644 editor/src/ArchitectScaffoldGenerator.h create mode 100644 editor/tests/step478_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index a93f659..3a144ea 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3181,4 +3181,13 @@ target_link_libraries(step477_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step478_test tests/step478_test.cpp) +target_include_directories(step478_test PRIVATE src) +target_link_libraries(step478_test PRIVATE + nlohmann_json::nlohmann_json + unofficial::tree-sitter::tree-sitter + tree_sitter_python tree_sitter_cpp tree_sitter_elisp + tree_sitter_javascript tree_sitter_typescript + tree_sitter_java tree_sitter_rust tree_sitter_go) + # Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies) diff --git a/editor/src/ArchitectScaffoldGenerator.h b/editor/src/ArchitectScaffoldGenerator.h new file mode 100644 index 0000000..18cf5aa --- /dev/null +++ b/editor/src/ArchitectScaffoldGenerator.h @@ -0,0 +1,358 @@ +#pragma once + +// Step 478: Scaffold File Generation +// Materializes an approved skeleton into concrete files and file operations. + +#include "ArchitectSkeletonGenerator.h" +#include "FileOperations.h" + +#include +#include +#include +#include +#include +#include + +struct ScaffoldFileSpec { + std::string relativePath; + std::string language; + std::string content; +}; + +struct ScaffoldOperation { + std::string method; // fileCreate | fileWrite + std::string path; // workspace-relative path + std::string language; // for fileCreate + std::string content; // for fileWrite +}; + +struct ScaffoldPlan { + std::string workspaceRoot; + std::string projectName; + std::vector directories; + std::vector files; + std::vector operations; + std::vector notes; + + bool hasFile(const std::string& relPath) const { + for (const auto& f : files) { + if (f.relativePath == relPath) return true; + } + return false; + } +}; + +struct ScaffoldInput { + std::string workspaceRoot; // target workspace root + std::string projectName; + SkeletonProjectSpec skeleton; + bool includeWhetstoneState = true; +}; + +class ArchitectScaffoldGenerator { +public: + static ScaffoldPlan buildPlan(const ScaffoldInput& in) { + ScaffoldPlan out; + out.workspaceRoot = in.workspaceRoot.empty() ? "." : in.workspaceRoot; + out.projectName = sanitizeSegment(in.projectName.empty() ? "project" : in.projectName); + const std::string base = out.projectName; + + std::vector languages; + for (const auto& m : in.skeleton.modules) { + languages.push_back(lower(m.language)); + const std::string rel = base + "/" + modulePath(m); + out.files.push_back({rel, lower(m.language), renderModuleFile(m)}); + } + + if (containsAny(languages, {"typescript", "javascript"})) { + out.files.push_back({base + "/package.json", "json", renderPackageJson(out.projectName)}); + } + if (containsAny(languages, {"python"})) { + out.files.push_back({base + "/pyproject.toml", "toml", renderPyproject(out.projectName)}); + } + if (containsAny(languages, {"rust"})) { + out.files.push_back({base + "/Cargo.toml", "toml", renderCargo(out.projectName)}); + } + if (containsAny(languages, {"cpp", "c"})) { + out.files.push_back({base + "/CMakeLists.txt", "cmake", renderCmake(out.projectName)}); + } + + out.files.push_back({base + "/.gitignore", "text", renderGitignore(languages)}); + + if (in.includeWhetstoneState) { + out.files.push_back({base + "/.whetstone/workflow_state.json", + "json", renderWorkflowState(in.skeleton)}); + for (const auto& m : in.skeleton.modules) { + const std::string sidecar = + base + "/.whetstone/sidecars/" + sanitizeSegment(m.moduleName) + ".ast.json"; + out.files.push_back({sidecar, "json", renderSidecar(m)}); + } + } + + collectDirectories(out); + buildOperations(out); + out.notes.push_back("Scaffold plan generated from approved skeleton"); + out.notes.push_back("Operations are compatible with fileCreate/fileWrite MCP semantics"); + return out; + } + + static bool applyPlan(const ScaffoldPlan& plan, std::string* error = nullptr) { + for (const auto& op : plan.operations) { + auto [okPath, resolved] = fileOpsResolvePath(plan.workspaceRoot, op.path); + if (!okPath) { + if (error) *error = resolved; + return false; + } + + if (op.method == "fileCreate") { + if (std::filesystem::exists(resolved)) continue; + auto [ok, msg] = fileOpsCreate(resolved, op.language, "empty"); + if (!ok) { + if (error) *error = msg; + return false; + } + continue; + } + + if (op.method == "fileWrite") { + auto [ok, msg, bytes] = fileOpsWrite(resolved, op.content); + (void)bytes; + if (!ok) { + if (error) *error = msg; + return false; + } + continue; + } + + if (error) *error = "Unknown operation method: " + op.method; + return false; + } + return true; + } + +private: + static std::string lower(const std::string& s) { + std::string out = s; + std::transform(out.begin(), out.end(), out.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return out; + } + + static std::string sanitizeSegment(const std::string& s) { + std::string out; + for (char c : s) { + unsigned char uc = static_cast(c); + if (std::isalnum(uc) || c == '_' || c == '-') out.push_back(c); + else if (std::isspace(uc)) out.push_back('_'); + } + if (out.empty()) return "project"; + return out; + } + + static bool containsAny(const std::vector& values, + std::initializer_list expected) { + for (const auto& e : expected) { + for (const auto& v : values) { + if (v == e) return true; + } + } + return false; + } + + static std::string modulePath(const SkeletonModuleSpec& m) { + const std::string mod = sanitizeSegment(m.moduleName); + const std::string lang = lower(m.language); + if (lang == "typescript") return "src/" + mod + "/" + mod + ".ts"; + if (lang == "javascript") return "src/" + mod + "/" + mod + ".js"; + if (lang == "python") return "src/" + mod + ".py"; + if (lang == "rust") return "src/" + mod + ".rs"; + if (lang == "cpp" || lang == "c") return "src/" + mod + ".h"; + if (lang == "go") return "internal/" + mod + "/" + mod + ".go"; + if (lang == "sql") return "db/" + mod + ".sql"; + if (lang == "yaml" || lang == "yml") return "deploy/" + mod + ".yaml"; + if (lang == "markdown" || lang == "md") return "docs/" + mod + ".md"; + return "src/" + mod + ".txt"; + } + + static std::string annotationPrefix(const std::string& lang) { + if (lang == "sql") return "-- "; + if (lang == "markdown" || lang == "md") return ""; + return ""; + } + + static std::string toIdentifier(const std::string& name) { + std::string out; + for (char c : name) { + unsigned char uc = static_cast(c); + if (std::isalnum(uc) || c == '_') out.push_back(c); + else out.push_back('_'); + } + if (out.empty()) return "generated_fn"; + if (std::isdigit(static_cast(out[0]))) out = "fn_" + out; + return out; + } + + static std::string renderFunction(const SkeletonFunctionSpec& fn, const std::string& lang) { + std::ostringstream o; + const std::string prefix = annotationPrefix(lang); + const std::string suffix = annotationSuffix(lang); + for (const auto& a : fn.annotations) { + o << prefix << a.key << ": " << a.value << suffix << "\n"; + } + + const std::string id = toIdentifier(fn.name); + if (lang == "python") { + o << "def " << id << "():\n pass\n\n"; + } else if (lang == "typescript") { + o << "export function " << id << "(): void {\n}\n\n"; + } else if (lang == "javascript") { + o << "export function " << id << "() {\n}\n\n"; + } else if (lang == "rust") { + o << "pub fn " << id << "() {\n}\n\n"; + } else if (lang == "go") { + o << "func " << id << "() {\n}\n\n"; + } else if (lang == "cpp" || lang == "c") { + o << "inline void " << id << "() {\n}\n\n"; + } else if (lang == "sql") { + o << "-- STUB: " << id << "\n\n"; + } else if (lang == "yaml" || lang == "yml") { + o << id << ": stub\n\n"; + } else if (lang == "markdown" || lang == "md") { + o << "### " << id << "\n\n"; + } else { + o << id << "\n\n"; + } + return o.str(); + } + + static std::string renderModuleFile(const SkeletonModuleSpec& m) { + std::ostringstream o; + const std::string lang = lower(m.language); + if (lang == "python") o << "# Auto-generated scaffold module: " << m.moduleName << "\n\n"; + else if (lang == "sql") o << "-- Auto-generated scaffold module: " << m.moduleName << "\n\n"; + else if (lang == "markdown" || lang == "md") + o << "# Auto-generated scaffold module: " << m.moduleName << "\n\n"; + else o << "// Auto-generated scaffold module: " << m.moduleName << "\n\n"; + + if (!m.dependencies.empty()) { + if (lang == "python") o << "# dependencies: "; + else if (lang == "sql") o << "-- dependencies: "; + else if (lang == "markdown" || lang == "md") o << "Dependencies: "; + else o << "// dependencies: "; + for (size_t i = 0; i < m.dependencies.size(); ++i) { + if (i) o << ", "; + o << m.dependencies[i]; + } + o << "\n\n"; + } + + for (const auto& fn : m.functions) o << renderFunction(fn, lang); + if (m.functions.empty()) o << renderFunction({"execute", "void execute()", {}}, lang); + return o.str(); + } + + static std::string renderPackageJson(const std::string& projectName) { + std::ostringstream o; + o << "{\n" + << " \"name\": \"" << projectName << "\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"private\": true,\n" + << " \"scripts\": {\n" + << " \"build\": \"echo build\",\n" + << " \"test\": \"echo test\"\n" + << " }\n" + << "}\n"; + return o.str(); + } + + static std::string renderPyproject(const std::string& projectName) { + std::ostringstream o; + o << "[project]\n" + << "name = \"" << projectName << "\"\n" + << "version = \"0.1.0\"\n" + << "description = \"Generated by ArchitectScaffoldGenerator\"\n"; + return o.str(); + } + + static std::string renderCargo(const std::string& projectName) { + std::ostringstream o; + o << "[package]\n" + << "name = \"" << projectName << "\"\n" + << "version = \"0.1.0\"\n" + << "edition = \"2021\"\n"; + return o.str(); + } + + static std::string renderCmake(const std::string& projectName) { + std::ostringstream o; + o << "cmake_minimum_required(VERSION 3.20)\n" + << "project(" << projectName << " LANGUAGES CXX)\n\n" + << "add_library(" << projectName << "_scaffold INTERFACE)\n"; + return o.str(); + } + + static std::string renderGitignore(const std::vector& languages) { + std::ostringstream o; + o << ".DS_Store\n.idea/\n.vscode/\n"; + if (containsAny(languages, {"typescript", "javascript"})) o << "node_modules/\ndist/\n"; + if (containsAny(languages, {"python"})) o << "__pycache__/\n*.pyc\n.venv/\n"; + if (containsAny(languages, {"rust"})) o << "target/\n"; + if (containsAny(languages, {"cpp", "c"})) o << "build/\n*.o\n"; + o << ".whetstone/\n"; + return o.str(); + } + + static std::string renderWorkflowState(const SkeletonProjectSpec& sk) { + std::ostringstream o; + o << "{\n" + << " \"status\": \"planned\",\n" + << " \"moduleCount\": " << sk.modules.size() << ",\n" + << " \"modules\": ["; + for (size_t i = 0; i < sk.modules.size(); ++i) { + if (i) o << ", "; + o << "\"" << sanitizeSegment(sk.modules[i].moduleName) << "\""; + } + o << "]\n}\n"; + return o.str(); + } + + static std::string renderSidecar(const SkeletonModuleSpec& mod) { + std::ostringstream o; + o << "{\n" + << " \"module\": \"" << sanitizeSegment(mod.moduleName) << "\",\n" + << " \"language\": \"" << lower(mod.language) << "\",\n" + << " \"functions\": ["; + for (size_t i = 0; i < mod.functions.size(); ++i) { + if (i) o << ", "; + o << "{ \"name\": \"" << toIdentifier(mod.functions[i].name) << "\" }"; + } + o << "]\n}\n"; + return o.str(); + } + + static void collectDirectories(ScaffoldPlan& plan) { + std::vector uniqueDirs; + for (const auto& f : plan.files) { + std::filesystem::path p(f.relativePath); + auto dir = p.parent_path().string(); + if (dir.empty()) continue; + if (std::find(uniqueDirs.begin(), uniqueDirs.end(), dir) == uniqueDirs.end()) { + uniqueDirs.push_back(dir); + } + } + plan.directories = std::move(uniqueDirs); + } + + static void buildOperations(ScaffoldPlan& plan) { + plan.operations.clear(); + for (const auto& f : plan.files) { + plan.operations.push_back({"fileCreate", f.relativePath, f.language, ""}); + plan.operations.push_back({"fileWrite", f.relativePath, "", f.content}); + } + } +}; diff --git a/editor/tests/step478_test.cpp b/editor/tests/step478_test.cpp new file mode 100644 index 0000000..23864b5 --- /dev/null +++ b/editor/tests/step478_test.cpp @@ -0,0 +1,224 @@ +// Step 478: Scaffold File Generation Tests (12 tests) + +#include "ArchitectScaffoldGenerator.h" + +#include +#include +#include +#include +#include + +static int passed = 0, failed = 0; +#define TEST(name) { std::cout << " " << #name << "... "; } +#define PASS() { std::cout << "PASS\n"; ++passed; } +#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; } +#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {} + +namespace fs = std::filesystem; + +static bool endsWith(const std::string& v, const std::string& suffix) { + return v.size() >= suffix.size() && + v.compare(v.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +static const ScaffoldFileSpec* findBySuffix(const ScaffoldPlan& plan, + const std::string& suffix) { + for (const auto& f : plan.files) { + if (endsWith(f.relativePath, suffix)) return &f; + } + return nullptr; +} + +static SkeletonFunctionSpec fn(const std::string& name) { + SkeletonFunctionSpec f; + f.name = name; + f.signature = "void " + name + "()"; + f.annotations = { + {"@Intent", "intent " + name}, + {"@Complexity", "medium"}, + {"@ContextWidth", "module"}, + {"@Automatability", "llm"}, + {"@Contract", "pre: x; post: y"} + }; + return f; +} + +static SkeletonModuleSpec mod(const std::string& name, const std::string& lang, + std::initializer_list fns = {}) { + SkeletonModuleSpec m; + m.moduleName = name; + m.language = lang; + m.functions.assign(fns.begin(), fns.end()); + return m; +} + +static SkeletonProjectSpec mixedSkeleton() { + SkeletonProjectSpec sk; + sk.modules.push_back(mod("api", "typescript", {fn("handleRequest")})); + sk.modules.push_back(mod("workers", "go", {fn("dispatch")})); + sk.modules.push_back(mod("schema", "sql", {fn("migrate")})); + return sk; +} + +void test_generates_source_files_with_semanno_annotations() { + TEST(generates_source_files_with_semanno_annotations); + ScaffoldInput in{"/tmp", "demo", mixedSkeleton(), true}; + auto plan = ArchitectScaffoldGenerator::buildPlan(in); + auto* src = findBySuffix(plan, "demo/src/api/api.ts"); + CHECK(src != nullptr, "missing api scaffold file"); + CHECK(src->content.find("@Intent") != std::string::npos, "missing @Intent annotation"); + CHECK(src->content.find("handleRequest") != std::string::npos, "missing function stub"); + PASS(); +} + +void test_generates_package_json_for_node_stack() { + TEST(generates_package_json_for_node_stack); + SkeletonProjectSpec sk; + sk.modules.push_back(mod("api", "typescript", {fn("main")})); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", sk, true}); + CHECK(plan.hasFile("demo/package.json"), "missing package.json"); + PASS(); +} + +void test_generates_pyproject_for_python_stack() { + TEST(generates_pyproject_for_python_stack); + SkeletonProjectSpec sk; + sk.modules.push_back(mod("jobs", "python", {fn("runJobs")})); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", sk, true}); + CHECK(plan.hasFile("demo/pyproject.toml"), "missing pyproject.toml"); + PASS(); +} + +void test_generates_cargo_for_rust_stack() { + TEST(generates_cargo_for_rust_stack); + SkeletonProjectSpec sk; + sk.modules.push_back(mod("core", "rust", {fn("compute")})); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", sk, true}); + CHECK(plan.hasFile("demo/Cargo.toml"), "missing Cargo.toml"); + PASS(); +} + +void test_generates_cmake_for_cpp_stack() { + TEST(generates_cmake_for_cpp_stack); + SkeletonProjectSpec sk; + sk.modules.push_back(mod("engine", "cpp", {fn("solve")})); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", sk, true}); + CHECK(plan.hasFile("demo/CMakeLists.txt"), "missing CMakeLists.txt"); + PASS(); +} + +void test_directory_structure_follows_language_conventions() { + TEST(directory_structure_follows_language_conventions); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", mixedSkeleton(), true}); + CHECK(findBySuffix(plan, "demo/src/api/api.ts") != nullptr, "missing ts convention path"); + CHECK(findBySuffix(plan, "demo/internal/workers/workers.go") != nullptr, + "missing go convention path"); + CHECK(findBySuffix(plan, "demo/db/schema.sql") != nullptr, "missing sql convention path"); + PASS(); +} + +void test_gitignore_matches_selected_stack() { + TEST(gitignore_matches_selected_stack); + SkeletonProjectSpec sk; + sk.modules.push_back(mod("api", "typescript", {fn("a")})); + sk.modules.push_back(mod("jobs", "python", {fn("b")})); + sk.modules.push_back(mod("core", "rust", {fn("c")})); + sk.modules.push_back(mod("engine", "cpp", {fn("d")})); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", sk, true}); + auto* gi = findBySuffix(plan, "demo/.gitignore"); + CHECK(gi != nullptr, "missing .gitignore"); + CHECK(gi->content.find("node_modules/") != std::string::npos, "missing node ignore"); + CHECK(gi->content.find("__pycache__/") != std::string::npos, "missing python ignore"); + CHECK(gi->content.find("target/") != std::string::npos, "missing rust ignore"); + CHECK(gi->content.find("build/") != std::string::npos, "missing cpp ignore"); + PASS(); +} + +void test_whetstone_state_and_sidecars_are_generated() { + TEST(whetstone_state_and_sidecars_are_generated); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", mixedSkeleton(), true}); + CHECK(plan.hasFile("demo/.whetstone/workflow_state.json"), "missing workflow state file"); + CHECK(plan.hasFile("demo/.whetstone/sidecars/api.ast.json"), "missing api sidecar"); + CHECK(plan.hasFile("demo/.whetstone/sidecars/workers.ast.json"), "missing workers sidecar"); + PASS(); +} + +void test_operations_are_filecreate_and_filewrite_pairs() { + TEST(operations_are_filecreate_and_filewrite_pairs); + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", mixedSkeleton(), true}); + CHECK(plan.operations.size() == plan.files.size() * 2, "unexpected operation count"); + int creates = 0, writes = 0; + for (const auto& op : plan.operations) { + if (op.method == "fileCreate") ++creates; + if (op.method == "fileWrite") ++writes; + } + CHECK(creates == (int)plan.files.size(), "fileCreate count mismatch"); + CHECK(writes == (int)plan.files.size(), "fileWrite count mismatch"); + PASS(); +} + +void test_apply_plan_writes_files_to_disk() { + TEST(apply_plan_writes_files_to_disk); + fs::path root = fs::temp_directory_path() / "whetstone_step478_apply"; + fs::remove_all(root); + fs::create_directories(root); + + auto plan = ArchitectScaffoldGenerator::buildPlan({root.string(), "demo", mixedSkeleton(), true}); + std::string err; + bool ok = ArchitectScaffoldGenerator::applyPlan(plan, &err); + CHECK(ok, ("apply failed: " + err).c_str()); + CHECK(fs::exists(root / "demo/src/api/api.ts"), "missing written source file"); + CHECK(fs::exists(root / "demo/.whetstone/workflow_state.json"), "missing written workflow state"); + + std::ifstream in(root / "demo/src/api/api.ts"); + std::string body((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + CHECK(body.find("@Intent") != std::string::npos, "written file missing annotation"); + fs::remove_all(root); + PASS(); +} + +void test_apply_plan_is_idempotent_when_files_exist() { + TEST(apply_plan_is_idempotent_when_files_exist); + fs::path root = fs::temp_directory_path() / "whetstone_step478_idempotent"; + fs::remove_all(root); + fs::create_directories(root); + + auto plan = ArchitectScaffoldGenerator::buildPlan({root.string(), "demo", mixedSkeleton(), true}); + std::string err; + CHECK(ArchitectScaffoldGenerator::applyPlan(plan, &err), ("first apply failed: " + err).c_str()); + err.clear(); + CHECK(ArchitectScaffoldGenerator::applyPlan(plan, &err), ("second apply failed: " + err).c_str()); + fs::remove_all(root); + PASS(); +} + +void test_empty_skeleton_still_generates_basics() { + TEST(empty_skeleton_still_generates_basics); + SkeletonProjectSpec sk; + auto plan = ArchitectScaffoldGenerator::buildPlan({"/tmp", "demo", sk, true}); + CHECK(plan.hasFile("demo/.gitignore"), "missing .gitignore"); + CHECK(plan.hasFile("demo/.whetstone/workflow_state.json"), "missing workflow state"); + CHECK(!plan.operations.empty(), "expected non-empty file operations"); + PASS(); +} + +int main() { + std::cout << "Step 478: Scaffold File Generation Tests\n"; + + test_generates_source_files_with_semanno_annotations(); // 1 + test_generates_package_json_for_node_stack(); // 2 + test_generates_pyproject_for_python_stack(); // 3 + test_generates_cargo_for_rust_stack(); // 4 + test_generates_cmake_for_cpp_stack(); // 5 + test_directory_structure_follows_language_conventions(); // 6 + test_gitignore_matches_selected_stack(); // 7 + test_whetstone_state_and_sidecars_are_generated(); // 8 + test_operations_are_filecreate_and_filewrite_pairs(); // 9 + test_apply_plan_writes_files_to_disk(); // 10 + test_apply_plan_is_idempotent_when_files_exist(); // 11 + test_empty_skeleton_still_generates_basics(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 2badbee..f67712f 100644 --- a/progress.md +++ b/progress.md @@ -6700,3 +6700,44 @@ shapes (REST API, CLI, library, microservice, full stack). - `editor/src/ArchitectTemplates.h` within header-size limit (`182` <= `600`) - `editor/tests/step477_test.cpp` within test-file size guidance (`166` lines) - Naming/style aligned with `ARCHITECTURE.md` conventions (`PascalCase` types, `camelCase` functions) + +### Step 478: Scaffold File Generation +**Status:** PASS (12/12 tests) + +Adds concrete scaffold generation from approved skeletons, including source +files, stack-specific project config, `.gitignore`, and `.whetstone` state +files, with operation plans compatible with `fileCreate`/`fileWrite`. + +**Files added:** +- `editor/src/ArchitectScaffoldGenerator.h` — scaffold planner/applier: + - `ScaffoldInput`, `ScaffoldFileSpec`, `ScaffoldOperation`, `ScaffoldPlan` + - `buildPlan(...)` to derive file tree + operation queue from skeleton modules + - source rendering with Semanno annotation comments and per-language stubs + - config generation: + - `package.json` (JS/TS) + - `pyproject.toml` (Python) + - `Cargo.toml` (Rust) + - `CMakeLists.txt` (C/C++) + - `.gitignore` composition based on selected languages + - `.whetstone/workflow_state.json` + sidecar module files + - `applyPlan(...)` executing `fileCreate`/`fileWrite`-style operations via existing file ops +- `editor/tests/step478_test.cpp` — 12 tests covering: + - Semanno annotations in generated source files + - stack-specific config file generation + - language convention path mapping (TS/Go/SQL) + - `.gitignore` entries by stack + - `.whetstone` workflow + sidecar generation + - operation queue shape (`fileCreate` + `fileWrite` pairs) + - on-disk apply behavior and idempotency + - empty-skeleton baseline generation +- `editor/CMakeLists.txt` — `step478_test` target + +**Verification run:** +- `cmake --build editor/build-native --target step478_test step477_test` — PASS +- `./editor/build-native/step478_test` — PASS (12/12) +- `./editor/build-native/step477_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/ArchitectScaffoldGenerator.h` within header-size limit (`358` <= `600`) +- `editor/tests/step478_test.cpp` within test-file size guidance (`224` lines) +- Header-only architecture preserved; naming conventions remain `PascalCase` types and `camelCase` methods