Files
whetstone_DSL/editor/src/GreenfieldScenarioRunner.h

154 lines
5.5 KiB
C++

#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;
}
};