Complete Step 478: scaffold file generation

This commit is contained in:
Bill
2026-02-16 21:03:11 -07:00
parent 58066d6f46
commit 99d81b7dad
4 changed files with 632 additions and 0 deletions

View File

@@ -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)

View File

@@ -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 <algorithm>
#include <cctype>
#include <filesystem>
#include <sstream>
#include <string>
#include <vector>
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<std::string> directories;
std::vector<ScaffoldFileSpec> files;
std::vector<ScaffoldOperation> operations;
std::vector<std::string> 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<std::string> 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<char>(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<unsigned char>(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<std::string>& values,
std::initializer_list<const char*> 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 annotationSuffix(const std::string& lang) {
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<unsigned char>(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<unsigned char>(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<std::string>& 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<std::string> 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});
}
}
};

View File

@@ -0,0 +1,224 @@
// Step 478: Scaffold File Generation Tests (12 tests)
#include "ArchitectScaffoldGenerator.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
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<SkeletonFunctionSpec> 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<char>(in)), std::istreambuf_iterator<char>());
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;
}

View File

@@ -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