Complete Step 478: scaffold file generation
This commit is contained in:
@@ -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)
|
||||
|
||||
358
editor/src/ArchitectScaffoldGenerator.h
Normal file
358
editor/src/ArchitectScaffoldGenerator.h
Normal 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});
|
||||
}
|
||||
}
|
||||
};
|
||||
224
editor/tests/step478_test.cpp
Normal file
224
editor/tests/step478_test.cpp
Normal 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;
|
||||
}
|
||||
41
progress.md
41
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
|
||||
|
||||
Reference in New Issue
Block a user