From 54d1dde3f40a124023eba5e99843247c926ce285 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 21:10:41 -0700 Subject: [PATCH] Complete Step 482: step spec expander --- editor/CMakeLists.txt | 9 ++ editor/src/StepSpecExpander.h | 267 ++++++++++++++++++++++++++++++++++ editor/tests/step482_test.cpp | 149 +++++++++++++++++++ progress.md | 38 +++++ 4 files changed, 463 insertions(+) create mode 100644 editor/src/StepSpecExpander.h create mode 100644 editor/tests/step482_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index f4f2510..08f24ca 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/StepSpecExpander.h b/editor/src/StepSpecExpander.h new file mode 100644 index 0000000..5e22e60 --- /dev/null +++ b/editor/src/StepSpecExpander.h @@ -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 +#include +#include +#include + +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 fields; + std::vector 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 testPlan; + HeaderSkeletonStub headerSkeleton; + BuildEntryStub buildEntry; + std::vector warnings; +}; + +struct StepExpanderInput { + std::string stepMarkdown; + std::string stepNumber; // e.g. "482" + std::string componentName; + std::string buildSystem = "cmake"; + std::vector projectConventions; + std::vector 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(s[b]))) ++b; + while (e > b && std::isspace(static_cast(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(std::tolower(c)); }); + return out; + } + + static bool hasAny(const std::string& text, std::initializer_list 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 makeTestPlan(const std::string& text, + const std::string& step, + StepComplexity complexity) { + std::vector 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& 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& 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(c); + if (std::isalnum(uc)) out.push_back(c); + if (out.size() >= 24) break; + } + if (out.empty()) return "GeneratedComponent"; + out[0] = static_cast(std::toupper(static_cast(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", "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", + "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; + } +}; diff --git a/editor/tests/step482_test.cpp b/editor/tests/step482_test.cpp new file mode 100644 index 0000000..5cc0693 --- /dev/null +++ b/editor/tests/step482_test.cpp @@ -0,0 +1,149 @@ +// Step 482: Step Spec Expander Tests (unit, negative, boundary) + +#include "StepSpecExpander.h" + +#include +#include + +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; +} diff --git a/progress.md b/progress.md index 3e6836e..7b49f22 100644 --- a/progress.md +++ b/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`