diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 32c9e24..5a0f648 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -11039,3 +11039,18 @@ target_include_directories(step1941_test PRIVATE src) add_executable(step1942_test tests/step1942_test.cpp) target_include_directories(step1942_test PRIVATE src) + +add_executable(step1943_test tests/step1943_test.cpp) +target_include_directories(step1943_test PRIVATE src) + +add_executable(step1944_test tests/step1944_test.cpp) +target_include_directories(step1944_test PRIVATE src) + +add_executable(step1945_test tests/step1945_test.cpp) +target_include_directories(step1945_test PRIVATE src) + +add_executable(step1946_test tests/step1946_test.cpp) +target_include_directories(step1946_test PRIVATE src) + +add_executable(step1947_test tests/step1947_test.cpp) +target_include_directories(step1947_test PRIVATE src) diff --git a/editor/src/PipelineExecutionPlan.h b/editor/src/PipelineExecutionPlan.h new file mode 100644 index 0000000..de7f46e --- /dev/null +++ b/editor/src/PipelineExecutionPlan.h @@ -0,0 +1,56 @@ +#pragma once +#include "PipelineGraph.h" +#include "PipelineStageSpec.h" +#include +#include + +namespace whetstone { + +class PipelineExecutionPlan { +public: + void addStage(const PipelineStageSpec& spec) { + registry_.registerStage(spec); + graph_.addStage(spec.stageId); + } + + void addEdge(const std::string& fromId, const std::string& toId) { + graph_.addEdge(fromId, toId); + } + + // Stages in topological order. + std::vector executionOrder() const { + std::vector result; + for (const auto& id : graph_.topologicalOrder()) + result.push_back(registry_.getStage(id)); + return result; + } + + // Language per stage in execution order. + std::vector languageSequence() const { + std::vector langs; + for (const auto& spec : executionOrder()) + langs.push_back(spec.language); + return langs; + } + + bool isValid() const { + return !graph_.hasCycle() && graph_.stageCount() > 0; + } + + // Pairs "langA->langB" wherever adjacent stages in execution order differ in language. + std::vector boundaryPairs() const { + auto order = executionOrder(); + std::vector pairs; + for (std::size_t i = 1; i < order.size(); ++i) { + if (order[i-1].language != order[i].language) + pairs.push_back(order[i-1].language + "->" + order[i].language); + } + return pairs; + } + +private: + PipelineStageRegistry registry_; + PipelineGraph graph_; +}; + +} // namespace whetstone diff --git a/editor/src/PipelineGraph.h b/editor/src/PipelineGraph.h new file mode 100644 index 0000000..c966fb5 --- /dev/null +++ b/editor/src/PipelineGraph.h @@ -0,0 +1,84 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace whetstone { + +class PipelineGraph { +public: + void addStage(const std::string& stageId) { + // Ensure entry exists even with no edges. + succ_[stageId]; + pred_[stageId]; + } + + void addEdge(const std::string& fromId, const std::string& toId) { + succ_[fromId].push_back(toId); + pred_[toId].push_back(fromId); + // Ensure reverse entries exist. + succ_[toId]; + pred_[fromId]; + } + + std::vector successors(const std::string& stageId) const { + auto it = succ_.find(stageId); + return it == succ_.end() ? std::vector{} : it->second; + } + + std::vector predecessors(const std::string& stageId) const { + auto it = pred_.find(stageId); + return it == pred_.end() ? std::vector{} : it->second; + } + + // Kahn's algorithm. Returns empty if cycle detected. + std::vector topologicalOrder() const { + // Build in-degree map. + std::map indegree; + for (const auto& [id, _] : succ_) indegree[id] = 0; + for (const auto& [id, preds] : pred_) + indegree[id] = static_cast(preds.size()); + + std::queue q; + for (const auto& [id, deg] : indegree) + if (deg == 0) q.push(id); + + std::vector order; + while (!q.empty()) { + auto cur = q.front(); q.pop(); + order.push_back(cur); + for (const auto& s : successors(cur)) { + if (--indegree[s] == 0) q.push(s); + } + } + // If we didn't visit all nodes, there's a cycle. + if (order.size() != succ_.size()) return {}; + return order; + } + + bool hasCycle() const { return topologicalOrder().empty() && !succ_.empty(); } + + std::vector sourceStages() const { + std::vector result; + for (const auto& [id, preds] : pred_) + if (preds.empty()) result.push_back(id); + return result; + } + + std::vector sinkStages() const { + std::vector result; + for (const auto& [id, succs] : succ_) + if (succs.empty()) result.push_back(id); + return result; + } + + int stageCount() const { return static_cast(succ_.size()); } + +private: + std::map> succ_; + std::map> pred_; +}; + +} // namespace whetstone diff --git a/editor/src/PipelineParityValidator.h b/editor/src/PipelineParityValidator.h new file mode 100644 index 0000000..03c9d97 --- /dev/null +++ b/editor/src/PipelineParityValidator.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include + +namespace whetstone { + +struct StageOutput { + std::string stageId; + std::string language; + std::string output; + bool success = true; +}; + +class PipelineParityValidator { +public: + void setReference(const std::string& stageId, const std::string& output) { + references_[stageId] = output; + } + + struct ValidationResult { + bool passed = true; + std::vector failedStages; + }; + + // For each StageOutput: fail if success=false OR (reference set AND output != reference). + ValidationResult validate(const std::vector& outputs) const { + ValidationResult result; + for (const auto& so : outputs) { + bool fail = !so.success; + if (!fail) { + auto it = references_.find(so.stageId); + if (it != references_.end() && so.output != it->second) + fail = true; + } + if (fail) { + result.passed = false; + result.failedStages.push_back(so.stageId); + } + } + return result; + } + + // True if output matches reference, or no reference set for this stage. + bool stagePass(const std::string& stageId, const std::string& output) const { + auto it = references_.find(stageId); + if (it == references_.end()) return true; + return output == it->second; + } + + int referenceCount() const { return static_cast(references_.size()); } + +private: + std::map references_; +}; + +} // namespace whetstone diff --git a/editor/src/PipelineStageSpec.h b/editor/src/PipelineStageSpec.h new file mode 100644 index 0000000..dc50f68 --- /dev/null +++ b/editor/src/PipelineStageSpec.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include +#include + +namespace whetstone { + +struct PipelineStageSpec { + std::string stageId; + std::string language; + std::string description; + std::string inputType; + std::string outputType; +}; + +class PipelineStageRegistry { +public: + void registerStage(const PipelineStageSpec& spec) { + stages_[spec.stageId] = spec; + } + + PipelineStageSpec getStage(const std::string& stageId) const { + auto it = stages_.find(stageId); + if (it == stages_.end()) + throw std::out_of_range("stage not found: " + stageId); + return it->second; + } + + bool hasStage(const std::string& stageId) const { + return stages_.count(stageId) > 0; + } + + std::vector stageIds() const { + std::vector ids; + for (const auto& [id, _] : stages_) ids.push_back(id); + return ids; + } + + std::vector stagesForLanguage(const std::string& language) const { + std::vector result; + for (const auto& [_, spec] : stages_) + if (spec.language == language) result.push_back(spec); + return result; + } + + int stageCount() const { return static_cast(stages_.size()); } + +private: + std::map stages_; +}; + +} // namespace whetstone diff --git a/editor/src/Sprint283IntegrationSummary.h b/editor/src/Sprint283IntegrationSummary.h new file mode 100644 index 0000000..a610a20 --- /dev/null +++ b/editor/src/Sprint283IntegrationSummary.h @@ -0,0 +1,21 @@ +#pragma once +// Sprint 283 Integration Summary +// Steps 1943–1947: poly-pipeline +// +// 1943: PipelineStageSpec + PipelineStageRegistry — stage descriptors +// 1944: PipelineGraph — DAG with Kahn topo sort +// 1945: PipelineExecutionPlan — registry + graph combined +// 1946: PipelineParityValidator — reference-output validation +// 1947: Integration — 5-stage Python→TS pipeline + +#include + +namespace whetstone { + +struct Sprint283IntegrationSummary { + int stepsCompleted = 5; + bool success = true; + std::string sprintName() const { return "Sprint 283: poly-pipeline"; } +}; + +} // namespace whetstone diff --git a/editor/tests/step1943_test.cpp b/editor/tests/step1943_test.cpp new file mode 100644 index 0000000..5ba0fb4 --- /dev/null +++ b/editor/tests/step1943_test.cpp @@ -0,0 +1,86 @@ +// Step 1943: PipelineStageSpec + PipelineStageRegistry +// +// t1: registerStage and hasStage/stageCount +// t2: getStage returns correct spec; throws for unknown +// t3: stageIds returns all registered ids +// t4: stagesForLanguage filters correctly +// t5: re-registering same id overwrites + +#include "PipelineStageSpec.h" +#include +#include + +namespace ws = whetstone; +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< 0 ? 1 : 0; +} diff --git a/editor/tests/step1944_test.cpp b/editor/tests/step1944_test.cpp new file mode 100644 index 0000000..1b7267a --- /dev/null +++ b/editor/tests/step1944_test.cpp @@ -0,0 +1,105 @@ +// Step 1944: PipelineGraph +// +// t1: addStage and stageCount +// t2: addEdge; successors and predecessors +// t3: topologicalOrder on linear chain +// t4: hasCycle detects cycle; acyclic returns false +// t5: sourceStages and sinkStages + +#include "PipelineGraph.h" +#include +#include + +namespace ws = whetstone; +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "<b"); + auto sc = g.successors("b"); + C(sc.size()==1 && sc[0]=="c", "b->c"); + auto pb = g.predecessors("b"); + C(pb.size()==1 && pb[0]=="a", "b pred a"); + C(g.predecessors("a").empty(), "a no pred"); + C(g.successors("c").empty(), "c no succ"); + P(); +} + +void t3(){ + T(topologicalOrder_linear_chain); + ws::PipelineGraph g; + g.addEdge("ingest","normalize"); + g.addEdge("normalize","fanout"); + g.addEdge("fanout","transform"); + g.addEdge("transform","serialize"); + auto order = g.topologicalOrder(); + C(order.size() == 5, "five stages in order"); + // ingest must come before normalize, etc. + auto pos = [&](const std::string& s){ + return std::find(order.begin(),order.end(),s) - order.begin(); + }; + C(pos("ingest") < pos("normalize"), "ingest before normalize"); + C(pos("normalize") < pos("fanout"), "normalize before fanout"); + C(pos("fanout") < pos("transform"), "fanout before transform"); + C(pos("transform") < pos("serialize"), "transform before serialize"); + P(); +} + +void t4(){ + T(hasCycle_detection); + ws::PipelineGraph acyclic; + acyclic.addEdge("a","b"); + acyclic.addEdge("b","c"); + C(!acyclic.hasCycle(), "acyclic has no cycle"); + + ws::PipelineGraph cyclic; + cyclic.addEdge("a","b"); + cyclic.addEdge("b","c"); + cyclic.addEdge("c","a"); + C(cyclic.hasCycle(), "cycle detected"); + C(cyclic.topologicalOrder().empty(), "topo empty on cycle"); + P(); +} + +void t5(){ + T(sourceStages_and_sinkStages); + ws::PipelineGraph g; + g.addEdge("ingest","normalize"); + g.addEdge("normalize","serialize"); + auto src = g.sourceStages(); + C(src.size()==1 && src[0]=="ingest", "ingest is source"); + auto sink = g.sinkStages(); + C(sink.size()==1 && sink[0]=="serialize","serialize is sink"); + // diamond: two sources, one sink + ws::PipelineGraph d; + d.addEdge("a","c"); + d.addEdge("b","c"); + C(d.sourceStages().size()==2, "diamond: two sources"); + C(d.sinkStages().size()==1, "diamond: one sink"); + P(); +} + +int main(){ + std::cout << "Step 1944: PipelineGraph\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1945_test.cpp b/editor/tests/step1945_test.cpp new file mode 100644 index 0000000..bb8cef3 --- /dev/null +++ b/editor/tests/step1945_test.cpp @@ -0,0 +1,107 @@ +// Step 1945: PipelineExecutionPlan +// +// t1: addStage and addEdge; isValid +// t2: executionOrder respects topological order +// t3: languageSequence returns language per stage in order +// t4: boundaryPairs detects language transitions +// t5: isValid false on cycle; empty plan invalid + +#include "PipelineExecutionPlan.h" +#include +#include + +namespace ws = whetstone; +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "<Rust") hasPyRs = true; + C(hasPyRs, "Python->Rust boundary"); + + // Same-language stages produce no boundary + ws::PipelineExecutionPlan p2; + p2.addStage({"a","Go","","",""}); + p2.addStage({"b","Go","","",""}); + p2.addEdge("a","b"); + C(p2.boundaryPairs().empty(), "no boundary for same language"); + P(); +} + +void t5(){ + T(isValid_cycle_and_empty); + ws::PipelineExecutionPlan empty; + C(!empty.isValid(), "empty plan invalid"); + + ws::PipelineExecutionPlan cyclic; + cyclic.addStage({"a","Python","","",""}); + cyclic.addStage({"b","Rust","","",""}); + cyclic.addEdge("a","b"); + cyclic.addEdge("b","a"); + C(!cyclic.isValid(), "cyclic plan invalid"); + P(); +} + +int main(){ + std::cout << "Step 1945: PipelineExecutionPlan\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1946_test.cpp b/editor/tests/step1946_test.cpp new file mode 100644 index 0000000..a3a8033 --- /dev/null +++ b/editor/tests/step1946_test.cpp @@ -0,0 +1,90 @@ +// Step 1946: PipelineParityValidator +// +// t1: setReference and referenceCount +// t2: validate passes when all outputs match references +// t3: validate fails when output mismatches reference +// t4: validate fails when success=false regardless of output +// t5: stagePass correct; no reference set always passes + +#include "PipelineParityValidator.h" +#include + +namespace ws = whetstone; +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "< outputs = { + {"ingest","Python","records\n",true}, + {"norm", "Rust", "normed\n", true} + }; + auto r = v.validate(outputs); + C(r.passed, "passed"); + C(r.failedStages.empty(),"no failures"); + P(); +} + +void t3(){ + T(validate_fails_on_mismatch); + ws::PipelineParityValidator v; + v.setReference("norm","normed\n"); + std::vector outputs = { + {"norm","Rust","WRONG\n",true} + }; + auto r = v.validate(outputs); + C(!r.passed, "not passed"); + C(r.failedStages.size()==1,"one failure"); + C(r.failedStages[0]=="norm","norm failed"); + P(); +} + +void t4(){ + T(validate_fails_when_success_false); + ws::PipelineParityValidator v; + v.setReference("fanout","batches\n"); + std::vector outputs = { + {"fanout","Go","batches\n",false} // success=false despite matching output + }; + auto r = v.validate(outputs); + C(!r.passed, "not passed (success=false)"); + C(r.failedStages[0]=="fanout","fanout failed"); + // Stage with no reference but success=false also fails + std::vector outputs2 = {{"x","Go","anything",false}}; + auto r2 = v.validate(outputs2); + C(!r2.passed, "no-ref stage fails when success=false"); + P(); +} + +void t5(){ + T(stagePass_with_and_without_reference); + ws::PipelineParityValidator v; + v.setReference("norm","normed\n"); + C( v.stagePass("norm","normed\n"), "correct output passes"); + C(!v.stagePass("norm","wrong\n"), "wrong output fails"); + C( v.stagePass("unknown","anything"),"no reference always passes"); + P(); +} + +int main(){ + std::cout << "Step 1946: PipelineParityValidator\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +} diff --git a/editor/tests/step1947_test.cpp b/editor/tests/step1947_test.cpp new file mode 100644 index 0000000..c4fdcb7 --- /dev/null +++ b/editor/tests/step1947_test.cpp @@ -0,0 +1,116 @@ +// Step 1947: Sprint 283 Integration — poly-pipeline +// +// 5-stage pipeline: Python(ingest)→Rust(normalize)→Go(fanout)→Haskell(transform)→TypeScript(serialize) +// +// t1: plan is valid, execution order correct, 4 boundary pairs +// t2: language sequence is Python Rust Go Haskell TypeScript +// t3: parity validation passes when all stage outputs match references +// t4: parity validation fails when one stage output is wrong +// t5: Sprint283IntegrationSummary reports 5 steps + +#include "PipelineExecutionPlan.h" +#include "PipelineParityValidator.h" +#include "Sprint283IntegrationSummary.h" +#include +#include + +namespace ws = whetstone; +static int p=0,f=0; +#define T(n) { std::cout<<" "<<#n<<"... "; } +#define P() { std::cout<<"PASS\n"; ++p; } +#define F(m) { std::cout<<"FAIL: "<Rust") != pairs.end(); + bool hasRsGo = std::find(pairs.begin(),pairs.end(),"Rust->Go") != pairs.end(); + bool hasGoHs = std::find(pairs.begin(),pairs.end(),"Go->Haskell") != pairs.end(); + bool hasHsTs = std::find(pairs.begin(),pairs.end(),"Haskell->TypeScript") != pairs.end(); + C(hasPyRs && hasRsGo && hasGoHs && hasHsTs, "all four language transitions"); + P(); +} + +void t2(){ + T(language_sequence_correct); + auto plan = makePipeline(); + auto langs = plan.languageSequence(); + C(langs.size() == 5, "five languages"); + C(langs[0] == "Python", "Python first"); + C(langs[1] == "Rust", "Rust second"); + C(langs[2] == "Go", "Go third"); + C(langs[3] == "Haskell", "Haskell fourth"); + C(langs[4] == "TypeScript", "TypeScript fifth"); + P(); +} + +void t3(){ + T(parity_validation_all_pass); + ws::PipelineParityValidator v; + v.setReference("ingest", "row1,row2\n"); + v.setReference("normalize", "norm1,norm2\n"); + v.setReference("fanout", "batch1\n"); + v.setReference("transform", "result1\n"); + v.setReference("serialize", "{\"data\":1}\n"); + std::vector outputs = { + {"ingest", "Python", "row1,row2\n", true}, + {"normalize", "Rust", "norm1,norm2\n", true}, + {"fanout", "Go", "batch1\n", true}, + {"transform", "Haskell", "result1\n", true}, + {"serialize", "TypeScript", "{\"data\":1}\n",true} + }; + auto r = v.validate(outputs); + C(r.passed, "all pass"); + C(r.failedStages.empty(),"no failures"); + P(); +} + +void t4(){ + T(parity_validation_fails_on_bad_stage); + ws::PipelineParityValidator v; + v.setReference("fanout","batch1\n"); + v.setReference("transform","result1\n"); + std::vector outputs = { + {"fanout", "Go", "WRONG\n", true}, + {"transform", "Haskell","result1\n", true} + }; + auto r = v.validate(outputs); + C(!r.passed, "not passed"); + C(r.failedStages.size()==1,"one failure"); + C(r.failedStages[0]=="fanout","fanout failed"); + P(); +} + +void t5(){ + T(sprint283_integration_summary); + ws::Sprint283IntegrationSummary s; + C(s.stepsCompleted == 5, "5 steps"); + C(s.success, "success"); + C(s.sprintName() == "Sprint 283: poly-pipeline","name"); + P(); +} + +int main(){ + std::cout << "Step 1947: Sprint 283 Integration\n"; + t1(); t2(); t3(); t4(); t5(); + std::cout << "\n" << p << "/" << (p+f) << " passed\n"; + return f > 0 ? 1 : 0; +}