Complete Step 482: step spec expander

This commit is contained in:
Bill
2026-02-16 21:10:41 -07:00
parent 026283f53c
commit 54d1dde3f4
4 changed files with 463 additions and 0 deletions

View File

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

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

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

View File

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