Complete Step 482: step spec expander
This commit is contained in:
@@ -3217,4 +3217,13 @@ target_link_libraries(step481_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step482_test tests/step482_test.cpp)
|
||||
target_include_directories(step482_test PRIVATE src)
|
||||
target_link_libraries(step482_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)
|
||||
|
||||
267
editor/src/StepSpecExpander.h
Normal file
267
editor/src/StepSpecExpander.h
Normal file
@@ -0,0 +1,267 @@
|
||||
#pragma once
|
||||
|
||||
// Step 482: Step Spec Expander
|
||||
// Expands a sprint step description into test plan, header skeleton, and build entry.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class StepComplexity {
|
||||
Trivial,
|
||||
Standard,
|
||||
Complex,
|
||||
Pipeline
|
||||
};
|
||||
|
||||
enum class PlannedTestType {
|
||||
Unit,
|
||||
Negative,
|
||||
Integration,
|
||||
Contract,
|
||||
Regression,
|
||||
Boundary,
|
||||
Property,
|
||||
Smoke,
|
||||
Performance
|
||||
};
|
||||
|
||||
struct PlannedTestStub {
|
||||
std::string name;
|
||||
PlannedTestType type = PlannedTestType::Unit;
|
||||
std::string description;
|
||||
std::string expectedRouting; // worker | architect
|
||||
};
|
||||
|
||||
struct HeaderFieldStub {
|
||||
std::string type;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct HeaderMethodStub {
|
||||
std::string returnType;
|
||||
std::string signature;
|
||||
std::string docComment;
|
||||
};
|
||||
|
||||
struct HeaderSkeletonStub {
|
||||
std::string fileName;
|
||||
std::string className;
|
||||
std::string docComment;
|
||||
std::vector<HeaderFieldStub> fields;
|
||||
std::vector<HeaderMethodStub> methods;
|
||||
};
|
||||
|
||||
struct BuildEntryStub {
|
||||
std::string buildSystem; // cmake, cargo, etc.
|
||||
std::string targetName;
|
||||
std::string testFileName;
|
||||
std::string snippet;
|
||||
};
|
||||
|
||||
struct StepSpec {
|
||||
StepComplexity complexity = StepComplexity::Standard;
|
||||
std::vector<PlannedTestStub> testPlan;
|
||||
HeaderSkeletonStub headerSkeleton;
|
||||
BuildEntryStub buildEntry;
|
||||
std::vector<std::string> warnings;
|
||||
};
|
||||
|
||||
struct StepExpanderInput {
|
||||
std::string stepMarkdown;
|
||||
std::string stepNumber; // e.g. "482"
|
||||
std::string componentName;
|
||||
std::string buildSystem = "cmake";
|
||||
std::vector<std::string> projectConventions;
|
||||
std::vector<std::string> priorStepHeaders;
|
||||
};
|
||||
|
||||
class StepSpecExpander {
|
||||
public:
|
||||
static StepSpec expand(const StepExpanderInput& in) {
|
||||
StepSpec out;
|
||||
const std::string text = lower(in.stepMarkdown);
|
||||
if (trim(in.stepMarkdown).empty()) {
|
||||
out.warnings.push_back("Step markdown is empty");
|
||||
}
|
||||
|
||||
out.complexity = classify(text);
|
||||
out.testPlan = makeTestPlan(text, in.stepNumber, out.complexity);
|
||||
out.headerSkeleton = makeHeaderSkeleton(in);
|
||||
out.buildEntry = makeBuildEntry(in);
|
||||
|
||||
if (in.projectConventions.empty()) {
|
||||
out.warnings.push_back("Project conventions not provided");
|
||||
}
|
||||
if (in.priorStepHeaders.empty()) {
|
||||
out.warnings.push_back("Prior step headers not provided");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string trim(const std::string& s) {
|
||||
size_t b = 0, e = s.size();
|
||||
while (b < e && std::isspace(static_cast<unsigned char>(s[b]))) ++b;
|
||||
while (e > b && std::isspace(static_cast<unsigned char>(s[e - 1]))) --e;
|
||||
return s.substr(b, e - b);
|
||||
}
|
||||
|
||||
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 bool hasAny(const std::string& text, std::initializer_list<const char*> terms) {
|
||||
for (const auto& t : terms) {
|
||||
if (text.find(t) != std::string::npos) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static StepComplexity classify(const std::string& text) {
|
||||
if (hasAny(text, {"pipeline", "workflow", "phase-end", "integration suite"})) {
|
||||
return StepComplexity::Pipeline;
|
||||
}
|
||||
int signals = 0;
|
||||
if (hasAny(text, {"orchestrate", "dispatch", "protocol"})) ++signals;
|
||||
if (hasAny(text, {"validate", "extract", "generate"})) ++signals;
|
||||
if (hasAny(text, {"convention", "context", "skeleton"})) ++signals;
|
||||
if (hasAny(text, {"mcp", "rpc"})) ++signals;
|
||||
if (signals >= 3) return StepComplexity::Complex;
|
||||
if (text.size() < 120 && hasAny(text, {"struct", "utility", "simple"})) {
|
||||
return StepComplexity::Trivial;
|
||||
}
|
||||
return StepComplexity::Standard;
|
||||
}
|
||||
|
||||
static std::vector<PlannedTestStub> makeTestPlan(const std::string& text,
|
||||
const std::string& step,
|
||||
StepComplexity complexity) {
|
||||
std::vector<PlannedTestStub> plan;
|
||||
|
||||
add(plan, step, PlannedTestType::Unit, "core behavior generates expected spec", "worker");
|
||||
add(plan, step, PlannedTestType::Unit, "header skeleton has required API fields", "worker");
|
||||
|
||||
if (hasAny(text, {"validate", "invalid", "reject", "error"})) {
|
||||
add(plan, step, PlannedTestType::Negative, "invalid input is rejected with warning", "worker");
|
||||
add(plan, step, PlannedTestType::Negative, "missing required fields emits warning", "worker");
|
||||
}
|
||||
if (hasAny(text, {"boundary", "limit", "score", "arithmetic", "max"})) {
|
||||
add(plan, step, PlannedTestType::Boundary, "boundary values keep stable classification", "worker");
|
||||
}
|
||||
if (hasAny(text, {"pipeline", "workflow"})) {
|
||||
add(plan, step, PlannedTestType::Integration, "full flow composes all outputs", "architect");
|
||||
add(plan, step, PlannedTestType::Integration, "output is sufficient for worker handoff", "architect");
|
||||
}
|
||||
if (hasAny(text, {"mcp", "rpc"})) {
|
||||
add(plan, step, PlannedTestType::Smoke, "tooling-facing shape can be consumed", "worker");
|
||||
}
|
||||
if (hasAny(text, {"modify", "change", "existing"})) {
|
||||
add(plan, step, PlannedTestType::Regression, "prior behavior remains stable", "architect");
|
||||
}
|
||||
|
||||
enforceBudget(plan, step, complexity);
|
||||
return plan;
|
||||
}
|
||||
|
||||
static void add(std::vector<PlannedTestStub>& plan,
|
||||
const std::string& step,
|
||||
PlannedTestType type,
|
||||
const std::string& description,
|
||||
const std::string& routing) {
|
||||
PlannedTestStub t;
|
||||
t.type = type;
|
||||
t.name = "test_step" + (step.empty() ? std::string("x") : step) + "_" +
|
||||
testTypeLabel(type) + "_" + std::to_string(plan.size() + 1);
|
||||
t.description = description;
|
||||
t.expectedRouting = routing;
|
||||
plan.push_back(std::move(t));
|
||||
}
|
||||
|
||||
static std::string testTypeLabel(PlannedTestType t) {
|
||||
switch (t) {
|
||||
case PlannedTestType::Unit: return "unit";
|
||||
case PlannedTestType::Negative: return "negative";
|
||||
case PlannedTestType::Integration: return "integration";
|
||||
case PlannedTestType::Contract: return "contract";
|
||||
case PlannedTestType::Regression: return "regression";
|
||||
case PlannedTestType::Boundary: return "boundary";
|
||||
case PlannedTestType::Property: return "property";
|
||||
case PlannedTestType::Smoke: return "smoke";
|
||||
case PlannedTestType::Performance: return "performance";
|
||||
}
|
||||
return "unit";
|
||||
}
|
||||
|
||||
static void enforceBudget(std::vector<PlannedTestStub>& plan,
|
||||
const std::string& step,
|
||||
StepComplexity complexity) {
|
||||
size_t minCount = 5, maxCount = 10;
|
||||
if (complexity == StepComplexity::Trivial) {
|
||||
minCount = 1; maxCount = 4;
|
||||
} else if (complexity == StepComplexity::Complex) {
|
||||
minCount = 10; maxCount = 15;
|
||||
} else if (complexity == StepComplexity::Pipeline) {
|
||||
minCount = 6; maxCount = 10;
|
||||
}
|
||||
|
||||
while (plan.size() < minCount) {
|
||||
add(plan, step, PlannedTestType::Unit, "additional coverage for complexity budget", "worker");
|
||||
}
|
||||
if (plan.size() > maxCount) plan.resize(maxCount);
|
||||
}
|
||||
|
||||
static std::string fallbackComponentName(const std::string& markdown) {
|
||||
std::string out;
|
||||
for (char c : markdown) {
|
||||
unsigned char uc = static_cast<unsigned char>(c);
|
||||
if (std::isalnum(uc)) out.push_back(c);
|
||||
if (out.size() >= 24) break;
|
||||
}
|
||||
if (out.empty()) return "GeneratedComponent";
|
||||
out[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[0])));
|
||||
return out;
|
||||
}
|
||||
|
||||
static HeaderSkeletonStub makeHeaderSkeleton(const StepExpanderInput& in) {
|
||||
HeaderSkeletonStub h;
|
||||
h.className = in.componentName.empty() ? fallbackComponentName(in.stepMarkdown)
|
||||
: in.componentName;
|
||||
h.fileName = h.className + ".h";
|
||||
h.docComment = "Auto-generated skeleton for sprint step expansion";
|
||||
h.fields.push_back({"std::string", "stepDescription"});
|
||||
h.fields.push_back({"StepComplexity", "complexity"});
|
||||
h.fields.push_back({"std::vector<PlannedTestStub>", "testPlan"});
|
||||
h.methods.push_back({"static StepSpec", "expand(const StepExpanderInput& in)",
|
||||
"Expand step markdown into structured spec output."});
|
||||
h.methods.push_back({"static StepComplexity", "classify(const std::string& text)",
|
||||
"Classify complexity from step description heuristics."});
|
||||
h.methods.push_back({"static std::vector<PlannedTestStub>",
|
||||
"makeTestPlan(const std::string& text, const std::string& step, StepComplexity c)",
|
||||
"Generate typed test stubs constrained by complexity budget."});
|
||||
return h;
|
||||
}
|
||||
|
||||
static BuildEntryStub makeBuildEntry(const StepExpanderInput& in) {
|
||||
BuildEntryStub b;
|
||||
b.buildSystem = in.buildSystem.empty() ? "cmake" : lower(in.buildSystem);
|
||||
const std::string step = in.stepNumber.empty() ? "x" : in.stepNumber;
|
||||
b.targetName = "step" + step + "_test";
|
||||
b.testFileName = "tests/step" + step + "_test.cpp";
|
||||
|
||||
if (b.buildSystem == "cmake") {
|
||||
b.snippet =
|
||||
"add_executable(" + b.targetName + " " + b.testFileName + ")\n"
|
||||
"target_include_directories(" + b.targetName + " PRIVATE src)\n";
|
||||
} else if (b.buildSystem == "cargo") {
|
||||
b.snippet = "[[test]]\nname = \"" + b.targetName + "\"\npath = \"" + b.testFileName + "\"\n";
|
||||
} else {
|
||||
b.snippet = "# Build entry stub for " + b.buildSystem + ": " + b.targetName;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
};
|
||||
149
editor/tests/step482_test.cpp
Normal file
149
editor/tests/step482_test.cpp
Normal file
@@ -0,0 +1,149 @@
|
||||
// Step 482: Step Spec Expander Tests (unit, negative, boundary)
|
||||
|
||||
#include "StepSpecExpander.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
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 {}
|
||||
|
||||
void test_classifies_trivial_for_small_utility_step() {
|
||||
TEST(classifies_trivial_for_small_utility_step);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Add a simple utility struct with one helper.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "UtilitySpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(spec.complexity == StepComplexity::Trivial, "expected trivial complexity");
|
||||
CHECK(spec.testPlan.size() <= 4, "trivial test budget exceeded");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_classifies_pipeline_when_workflow_keywords_present() {
|
||||
TEST(classifies_pipeline_when_workflow_keywords_present);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Build pipeline workflow integration across tools.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "PipelineSpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(spec.complexity == StepComplexity::Pipeline, "expected pipeline complexity");
|
||||
CHECK(spec.testPlan.size() >= 6, "pipeline minimum coverage not met");
|
||||
CHECK(spec.testPlan.size() <= 10, "pipeline maximum exceeded");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_includes_negative_tests_when_validation_terms_present() {
|
||||
TEST(includes_negative_tests_when_validation_terms_present);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Validate input and reject invalid shape.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "ValidatorSpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
bool foundNegative = false;
|
||||
for (const auto& t : spec.testPlan) {
|
||||
if (t.type == PlannedTestType::Negative) foundNegative = true;
|
||||
}
|
||||
CHECK(foundNegative, "expected negative test stubs");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_includes_boundary_tests_when_limit_terms_present() {
|
||||
TEST(includes_boundary_tests_when_limit_terms_present);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Compute score with max/min limit boundary checks.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "ScoringSpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
bool foundBoundary = false;
|
||||
for (const auto& t : spec.testPlan) {
|
||||
if (t.type == PlannedTestType::Boundary) foundBoundary = true;
|
||||
}
|
||||
CHECK(foundBoundary, "expected boundary test stub");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_generated_test_names_follow_step_prefix_convention() {
|
||||
TEST(generated_test_names_follow_step_prefix_convention);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Standard generator step.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "NameSpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(!spec.testPlan.empty(), "missing test stubs");
|
||||
CHECK(spec.testPlan[0].name.find("test_step482_") == 0, "test name prefix mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_header_skeleton_contains_expected_api_outline() {
|
||||
TEST(header_skeleton_contains_expected_api_outline);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Generate a step spec.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "StepSpecEngine";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(spec.headerSkeleton.fileName == "StepSpecEngine.h", "header filename mismatch");
|
||||
CHECK(spec.headerSkeleton.methods.size() >= 3, "missing method stubs");
|
||||
CHECK(spec.headerSkeleton.fields.size() >= 3, "missing field stubs");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_build_entry_uses_cmake_target_pattern() {
|
||||
TEST(build_entry_uses_cmake_target_pattern);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "Any step text.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "BuildSpec";
|
||||
in.buildSystem = "cmake";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(spec.buildEntry.targetName == "step482_test", "target naming mismatch");
|
||||
CHECK(spec.buildEntry.snippet.find("add_executable(step482_test") != std::string::npos,
|
||||
"cmake snippet mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_empty_markdown_emits_warning_negative_case() {
|
||||
TEST(empty_markdown_emits_warning_negative_case);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown = "";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "WarnSpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(!spec.warnings.empty(), "expected warning for empty markdown");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_complex_budget_expands_to_at_least_ten_tests() {
|
||||
TEST(complex_budget_expands_to_at_least_ten_tests);
|
||||
StepExpanderInput in;
|
||||
in.stepMarkdown =
|
||||
"Orchestrate dispatch protocol to extract conventions, validate context, generate skeleton, and integrate RPC.";
|
||||
in.stepNumber = "482";
|
||||
in.componentName = "ComplexSpec";
|
||||
auto spec = StepSpecExpander::expand(in);
|
||||
CHECK(spec.complexity == StepComplexity::Complex, "expected complex classification");
|
||||
CHECK(spec.testPlan.size() >= 10, "complex minimum budget not met");
|
||||
CHECK(spec.testPlan.size() <= 15, "complex budget upper bound exceeded");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 482: Step Spec Expander Tests\n";
|
||||
|
||||
test_classifies_trivial_for_small_utility_step(); // 1
|
||||
test_classifies_pipeline_when_workflow_keywords_present(); // 2
|
||||
test_includes_negative_tests_when_validation_terms_present(); // 3
|
||||
test_includes_boundary_tests_when_limit_terms_present(); // 4
|
||||
test_generated_test_names_follow_step_prefix_convention(); // 5
|
||||
test_header_skeleton_contains_expected_api_outline(); // 6
|
||||
test_build_entry_uses_cmake_target_pattern(); // 7
|
||||
test_empty_markdown_emits_warning_negative_case(); // 8
|
||||
test_complex_budget_expands_to_at_least_ten_tests(); // 9
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
38
progress.md
38
progress.md
@@ -6876,3 +6876,41 @@ awareness, and multi-language orchestration into a single structured flow.
|
||||
- **Outcome:** architect mode now supports requirement parsing, decomposition,
|
||||
stack selection, skeleton generation, review/approval, template/scaffold
|
||||
generation, dependency-aware build hints, and multi-language orchestration.
|
||||
|
||||
### Step 482: Step Spec Expander
|
||||
**Status:** PASS (9/9 tests)
|
||||
|
||||
Begins Phase 23c self-hosting by adding a step-spec expansion engine that
|
||||
turns sprint-step markdown into structured worker handoff artifacts:
|
||||
test plan stubs, header skeleton outline, and build-system entry.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/StepSpecExpander.h` — expansion engine:
|
||||
- `StepSpec`, `StepExpanderInput`, `PlannedTestStub`, `HeaderSkeletonStub`, `BuildEntryStub`
|
||||
- complexity classification:
|
||||
- `Trivial` (1-4 tests)
|
||||
- `Standard` (5-10 tests)
|
||||
- `Complex` (10-15 tests)
|
||||
- `Pipeline` (6-10 tests)
|
||||
- heuristic test-type planning from step text:
|
||||
- unit / negative / boundary / integration / smoke / regression
|
||||
- complexity-budget enforcement and deterministic test naming
|
||||
- header skeleton and build-entry stub generation (`cmake` + fallback systems)
|
||||
- `editor/tests/step482_test.cpp` — 9 tests (unit/negative/boundary focus) covering:
|
||||
- complexity classification behavior
|
||||
- negative/boundary test injection heuristics
|
||||
- naming convention for generated test stubs
|
||||
- header/build skeleton output shape
|
||||
- empty-input warning path
|
||||
- complexity budget range enforcement
|
||||
- `editor/CMakeLists.txt` — `step482_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake --build editor/build-native --target step482_test step481_test` — PASS
|
||||
- `./editor/build-native/step482_test` — PASS (9/9)
|
||||
- `./editor/build-native/step481_test` — PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/StepSpecExpander.h` within header-size limit (`267` <= `600`)
|
||||
- `editor/tests/step482_test.cpp` within test-file size guidance (`149` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user