Step 499: add greenfield scenario runner and tests
This commit is contained in:
@@ -3370,4 +3370,13 @@ target_link_libraries(step498_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step499_test tests/step499_test.cpp)
|
||||
target_include_directories(step499_test PRIVATE src)
|
||||
target_link_libraries(step499_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)
|
||||
|
||||
153
editor/src/GreenfieldScenarioRunner.h
Normal file
153
editor/src/GreenfieldScenarioRunner.h
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
// Step 499: Scenario — Greenfield Project
|
||||
// End-to-end scenario runner for "URL shortener with user accounts".
|
||||
|
||||
#include "ArchitectModuleDecomposer.h"
|
||||
#include "ArchitectProblemParser.h"
|
||||
#include "ArchitectScaffoldGenerator.h"
|
||||
#include "ArchitectSkeletonGenerator.h"
|
||||
#include "ArchitectTechStackSelector.h"
|
||||
#include "SafetyAuditor.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ScenarioTask {
|
||||
std::string moduleName;
|
||||
std::string functionName;
|
||||
std::string routeTo; // deterministic | llm | human
|
||||
bool autoCompleted = false;
|
||||
bool reviewRequired = false;
|
||||
std::string contextBundle;
|
||||
};
|
||||
|
||||
struct GreenfieldScenarioResult {
|
||||
std::string prompt;
|
||||
StructuredRequirements requirements;
|
||||
ModuleGraph graph;
|
||||
TechStackDecision stack;
|
||||
SkeletonProjectSpec skeleton;
|
||||
ScaffoldPlan scaffold;
|
||||
std::vector<ScenarioTask> tasks;
|
||||
std::vector<SafetyFinding> securityFindings;
|
||||
bool scaffoldApplied = false;
|
||||
std::vector<std::string> createdPaths;
|
||||
std::vector<std::string> notes;
|
||||
};
|
||||
|
||||
class GreenfieldScenarioRunner {
|
||||
public:
|
||||
static GreenfieldScenarioResult run(const std::string& workspaceRoot,
|
||||
const std::string& projectName = "url_shortener") {
|
||||
GreenfieldScenarioResult out;
|
||||
out.prompt = "Build a URL shortener with user accounts";
|
||||
|
||||
out.requirements = ArchitectProblemParser::parse(
|
||||
"Build a URL shortener with user accounts, auth, REST API endpoints, CRUD links, "
|
||||
"PostgreSQL, and a TypeScript frontend.");
|
||||
out.graph = ArchitectModuleDecomposer::decompose(out.requirements, DecompositionStrategy::Layered);
|
||||
|
||||
std::map<std::string, std::string> prefs{
|
||||
{"backend_language", "python"},
|
||||
{"database", "postgresql"},
|
||||
{"frontend_language", "typescript"}
|
||||
};
|
||||
out.stack = ArchitectTechStackSelector::select(out.requirements, out.graph, prefs);
|
||||
out.skeleton = ArchitectSkeletonGenerator::generate(out.requirements, out.graph, out.stack);
|
||||
out.scaffold = ArchitectScaffoldGenerator::buildPlan({
|
||||
workspaceRoot, projectName, out.skeleton, true
|
||||
});
|
||||
|
||||
out.tasks = buildTasks(out.skeleton);
|
||||
runSecurityScan(out);
|
||||
materialize(workspaceRoot, out.scaffold, out);
|
||||
out.notes.push_back("Greenfield scenario executed end-to-end");
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::vector<ScenarioTask> buildTasks(const SkeletonProjectSpec& skeleton) {
|
||||
std::vector<ScenarioTask> out;
|
||||
for (const auto& mod : skeleton.modules) {
|
||||
for (const auto& fn : mod.functions) {
|
||||
ScenarioTask t;
|
||||
t.moduleName = mod.moduleName;
|
||||
t.functionName = fn.name;
|
||||
routeTask(t);
|
||||
out.push_back(std::move(t));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void routeTask(ScenarioTask& t) {
|
||||
const std::string n = lower(t.functionName);
|
||||
const std::string m = lower(t.moduleName);
|
||||
|
||||
if (m.find("auth") != std::string::npos || n.find("auth") != std::string::npos ||
|
||||
n.find("login") != std::string::npos || n.find("authorize") != std::string::npos) {
|
||||
t.routeTo = "human";
|
||||
t.reviewRequired = true;
|
||||
t.contextBundle = "Threat model + trust boundaries + auth policy";
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCrudLike(n) || m == "data" || m == "models") {
|
||||
t.routeTo = "deterministic";
|
||||
t.autoCompleted = true;
|
||||
t.contextBundle = "Schema + endpoint contract";
|
||||
return;
|
||||
}
|
||||
|
||||
t.routeTo = "llm";
|
||||
t.contextBundle = "Module graph + function signature + related dependencies";
|
||||
}
|
||||
|
||||
static bool isCrudLike(const std::string& n) {
|
||||
return contains(n, "create") || contains(n, "get") || contains(n, "list") ||
|
||||
contains(n, "update") || contains(n, "delete") || contains(n, "save") ||
|
||||
contains(n, "load") || contains(n, "validate");
|
||||
}
|
||||
|
||||
static void runSecurityScan(GreenfieldScenarioResult& out) {
|
||||
std::string combined;
|
||||
for (const auto& f : out.scaffold.files) {
|
||||
if (f.relativePath.find("/src/") != std::string::npos ||
|
||||
f.relativePath.find("/internal/") != std::string::npos ||
|
||||
f.relativePath.find("/db/") != std::string::npos) {
|
||||
combined += "\n" + f.content;
|
||||
}
|
||||
}
|
||||
auto report = SafetyAuditor::audit(combined, "cpp", "generated_scenario");
|
||||
out.securityFindings = report.findings;
|
||||
}
|
||||
|
||||
static void materialize(const std::string& workspaceRoot,
|
||||
const ScaffoldPlan& plan,
|
||||
GreenfieldScenarioResult& out) {
|
||||
std::string err;
|
||||
out.scaffoldApplied = ArchitectScaffoldGenerator::applyPlan(plan, &err);
|
||||
if (!out.scaffoldApplied) {
|
||||
out.notes.push_back("Scaffold apply failed: " + err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& f : plan.files) {
|
||||
const auto p = std::filesystem::path(workspaceRoot) / f.relativePath;
|
||||
if (std::filesystem::exists(p)) out.createdPaths.push_back(p.string());
|
||||
}
|
||||
}
|
||||
|
||||
static std::string lower(std::string s) {
|
||||
for (auto& c : s) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool contains(const std::string& text, const std::string& needle) {
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
};
|
||||
191
editor/tests/step499_test.cpp
Normal file
191
editor/tests/step499_test.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
// Step 499: Scenario — Greenfield Project Tests (12 tests)
|
||||
|
||||
#include "GreenfieldScenarioRunner.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
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 {}
|
||||
|
||||
static fs::path makeRoot(const std::string& suffix) {
|
||||
fs::path root = fs::temp_directory_path() / ("whetstone_step499_" + suffix);
|
||||
fs::remove_all(root);
|
||||
fs::create_directories(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
void test_prompt_is_greenfield_url_shortener_with_user_accounts() {
|
||||
TEST(prompt_is_greenfield_url_shortener_with_user_accounts);
|
||||
auto root = makeRoot("prompt");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(r.prompt.find("URL shortener") != std::string::npos ||
|
||||
r.prompt.find("URL shortener") != std::string::npos, "prompt mismatch");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_decomposition_produces_api_auth_data_ui_modules() {
|
||||
TEST(decomposition_produces_api_auth_data_ui_modules);
|
||||
auto root = makeRoot("decompose");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(r.graph.hasModule("api"), "missing api module");
|
||||
CHECK(r.graph.hasModule("auth"), "missing auth module");
|
||||
CHECK(r.graph.hasModule("data"), "missing data module");
|
||||
CHECK(r.graph.hasModule("ui"), "missing ui module");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_stack_selection_matches_python_postgresql_typescript_expectation() {
|
||||
TEST(stack_selection_matches_python_postgresql_typescript_expectation);
|
||||
auto root = makeRoot("stack");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(r.stack.getChoice("api").language == "python", "api should be python");
|
||||
CHECK(r.stack.getChoice("data").database == "postgresql", "data should be postgresql");
|
||||
CHECK(r.stack.getChoice("ui").language == "typescript", "ui should be typescript");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_skeleton_contains_annotations_on_generated_functions() {
|
||||
TEST(skeleton_contains_annotations_on_generated_functions);
|
||||
auto root = makeRoot("skeleton");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(!r.skeleton.modules.empty(), "missing skeleton modules");
|
||||
bool hasIntent = false;
|
||||
for (const auto& m : r.skeleton.modules) {
|
||||
for (const auto& f : m.functions) {
|
||||
for (const auto& a : f.annotations) if (a.key == "@Intent") hasIntent = true;
|
||||
}
|
||||
}
|
||||
CHECK(hasIntent, "expected @Intent annotations");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_workflow_tasks_include_deterministic_autocomplete_items() {
|
||||
TEST(workflow_tasks_include_deterministic_autocomplete_items);
|
||||
auto root = makeRoot("det");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
bool found = false;
|
||||
for (const auto& t : r.tasks) {
|
||||
if (t.routeTo == "deterministic" && t.autoCompleted) found = true;
|
||||
}
|
||||
CHECK(found, "expected deterministic auto-complete tasks");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_llm_tasks_are_prepared_with_nonempty_context_bundles() {
|
||||
TEST(llm_tasks_are_prepared_with_nonempty_context_bundles);
|
||||
auto root = makeRoot("llm");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
bool found = false;
|
||||
for (const auto& t : r.tasks) {
|
||||
if (t.routeTo == "llm") {
|
||||
found = true;
|
||||
CHECK(!t.contextBundle.empty(), "llm context bundle missing");
|
||||
}
|
||||
}
|
||||
CHECK(found, "expected llm task");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_auth_logic_surfaces_human_review_tasks() {
|
||||
TEST(auth_logic_surfaces_human_review_tasks);
|
||||
auto root = makeRoot("human");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
bool found = false;
|
||||
for (const auto& t : r.tasks) {
|
||||
if (t.routeTo == "human") {
|
||||
found = true;
|
||||
CHECK(t.reviewRequired, "human task should require review");
|
||||
}
|
||||
}
|
||||
CHECK(found, "expected human review task for auth logic");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_security_scan_executes_and_returns_structured_findings_vector() {
|
||||
TEST(security_scan_executes_and_returns_structured_findings_vector);
|
||||
auto root = makeRoot("scan");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(r.securityFindings.size() >= 0, "security findings vector missing");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_project_structure_is_materialized_on_disk() {
|
||||
TEST(project_structure_is_materialized_on_disk);
|
||||
auto root = makeRoot("disk");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(r.scaffoldApplied, "expected scaffold apply success");
|
||||
CHECK(!r.createdPaths.empty(), "expected created files");
|
||||
CHECK(fs::exists(root / "url_shortener/.whetstone/workflow_state.json"), "missing workflow state file");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_scaffold_contains_whetstone_sidecars_and_gitignore() {
|
||||
TEST(scaffold_contains_whetstone_sidecars_and_gitignore);
|
||||
auto root = makeRoot("scaffold");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(r.scaffold.hasFile("url_shortener/.gitignore"), "missing .gitignore");
|
||||
CHECK(r.scaffold.hasFile("url_shortener/.whetstone/workflow_state.json"), "missing workflow state");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_task_routing_distribution_includes_all_three_worker_paths() {
|
||||
TEST(task_routing_distribution_includes_all_three_worker_paths);
|
||||
auto root = makeRoot("routing");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
bool hasDet = false, hasLlm = false, hasHuman = false;
|
||||
for (const auto& t : r.tasks) {
|
||||
if (t.routeTo == "deterministic") hasDet = true;
|
||||
if (t.routeTo == "llm") hasLlm = true;
|
||||
if (t.routeTo == "human") hasHuman = true;
|
||||
}
|
||||
CHECK(hasDet && hasLlm && hasHuman, "expected deterministic/llm/human routes");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_scenario_notes_include_execution_summary() {
|
||||
TEST(scenario_notes_include_execution_summary);
|
||||
auto root = makeRoot("notes");
|
||||
auto r = GreenfieldScenarioRunner::run(root.string(), "url_shortener");
|
||||
CHECK(!r.notes.empty(), "missing notes");
|
||||
CHECK(r.notes[0].find("end-to-end") != std::string::npos, "missing execution summary note");
|
||||
fs::remove_all(root);
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 499: Scenario — Greenfield Project Tests\n";
|
||||
|
||||
test_prompt_is_greenfield_url_shortener_with_user_accounts(); // 1
|
||||
test_decomposition_produces_api_auth_data_ui_modules(); // 2
|
||||
test_stack_selection_matches_python_postgresql_typescript_expectation(); // 3
|
||||
test_skeleton_contains_annotations_on_generated_functions(); // 4
|
||||
test_workflow_tasks_include_deterministic_autocomplete_items(); // 5
|
||||
test_llm_tasks_are_prepared_with_nonempty_context_bundles(); // 6
|
||||
test_auth_logic_surfaces_human_review_tasks(); // 7
|
||||
test_security_scan_executes_and_returns_structured_findings_vector(); // 8
|
||||
test_project_structure_is_materialized_on_disk(); // 9
|
||||
test_scaffold_contains_whetstone_sidecars_and_gitignore(); // 10
|
||||
test_task_routing_distribution_includes_all_three_worker_paths(); // 11
|
||||
test_scenario_notes_include_execution_summary(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
31
progress.md
31
progress.md
@@ -7614,3 +7614,34 @@ and workflow viability.
|
||||
- self-modernization workflow routing + deterministic output validation
|
||||
- consolidated self-host metrics/gap analysis
|
||||
- phase-level self-hosting thesis gate evaluation
|
||||
|
||||
### Step 499: Scenario - Greenfield Project
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Implements the first Phase 25b end-to-end scenario for:
|
||||
"Build a URL shortener with user accounts".
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/GreenfieldScenarioRunner.h` - scenario orchestration across:
|
||||
- problem parsing and layered decomposition
|
||||
- stack selection with explicit preferences (`python`, `postgresql`, `typescript`)
|
||||
- annotated skeleton generation + scaffold planning/materialization
|
||||
- task routing (`deterministic` / `llm` / `human`) with context bundles
|
||||
- security auditing over generated code surfaces
|
||||
- `editor/tests/step499_test.cpp` - 12 scenario tests covering:
|
||||
- decomposition + stack expectations
|
||||
- annotation presence in generated skeleton functions
|
||||
- deterministic auto-complete, llm context preparation, and human-review surfacing
|
||||
- scaffold sidecars/workflow-state materialization on disk
|
||||
- security-findings/report shape and execution notes
|
||||
- `editor/CMakeLists.txt` - `step499_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake --build editor/build-native --target step499_test step498_test` - PASS
|
||||
- `./editor/build-native/step499_test` - PASS (12/12)
|
||||
- `./editor/build-native/step498_test` - PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/GreenfieldScenarioRunner.h` within header-size limit (`153` <= `600`)
|
||||
- `editor/tests/step499_test.cpp` within test-file size guidance (`191` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user