From 1f15e82401dc20dfacb8faf6d768881df0e6b7ba Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 12:54:39 -0700 Subject: [PATCH] Step 385: add context bundle format --- editor/CMakeLists.txt | 9 ++ editor/src/ContextBundle.h | 168 +++++++++++++++++++++++++++++ editor/tests/step385_test.cpp | 192 ++++++++++++++++++++++++++++++++++ progress.md | 39 +++++++ 4 files changed, 408 insertions(+) create mode 100644 editor/src/ContextBundle.h create mode 100644 editor/tests/step385_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index ab38b3e..5fb9f70 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2344,4 +2344,13 @@ target_link_libraries(step384_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step385_test tests/step385_test.cpp) +target_include_directories(step385_test PRIVATE src) +target_link_libraries(step385_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/ContextBundle.h b/editor/src/ContextBundle.h new file mode 100644 index 0000000..9e7183b --- /dev/null +++ b/editor/src/ContextBundle.h @@ -0,0 +1,168 @@ +#pragma once +// Step 385: Standard context bundle format for external model invocation. + +#include "RoutingEngine.h" +#include "WorkerRegistry.h" +#include +#include +#include + +struct AttemptSummary { + std::string workerType; + std::string generatedCode; + std::string feedback; + + json toJson() const { + return { + {"workerType", workerType}, + {"generatedCode", generatedCode}, + {"feedback", feedback} + }; + } + + static AttemptSummary fromJson(const json& j) { + AttemptSummary a; + a.workerType = j.value("workerType", ""); + a.generatedCode = j.value("generatedCode", ""); + a.feedback = j.value("feedback", ""); + return a; + } +}; + +struct ContextBundle { + std::string taskDescription; + std::string skeletonCode; + std::string intent; + std::vector constraints; + std::string contextCode; + std::string projectSummary; + std::string existingTests; + std::vector previousAttempts; + int tokenBudget = 0; + std::string outputFormat; // "code-only" | "code-with-explanation" + + json toJson() const { + json attempts = json::array(); + for (const auto& a : previousAttempts) attempts.push_back(a.toJson()); + return { + {"taskDescription", taskDescription}, + {"skeletonCode", skeletonCode}, + {"intent", intent}, + {"constraints", constraints}, + {"contextCode", contextCode}, + {"projectSummary", projectSummary}, + {"existingTests", existingTests}, + {"previousAttempts", attempts}, + {"tokenBudget", tokenBudget}, + {"outputFormat", outputFormat} + }; + } + + static ContextBundle fromJson(const json& j) { + ContextBundle b; + b.taskDescription = j.value("taskDescription", ""); + b.skeletonCode = j.value("skeletonCode", ""); + b.intent = j.value("intent", ""); + if (j.contains("constraints") && j["constraints"].is_array()) { + b.constraints = j["constraints"].get>(); + } + b.contextCode = j.value("contextCode", ""); + b.projectSummary = j.value("projectSummary", ""); + b.existingTests = j.value("existingTests", ""); + if (j.contains("previousAttempts") && j["previousAttempts"].is_array()) { + for (const auto& a : j["previousAttempts"]) { + b.previousAttempts.push_back(AttemptSummary::fromJson(a)); + } + } + b.tokenBudget = j.value("tokenBudget", 0); + b.outputFormat = j.value("outputFormat", "code-with-explanation"); + return b; + } +}; + +inline std::vector collectBundleConstraints(const WorkerContext& context, + const WorkItem& item) { + std::vector out; + for (const auto& anno : context.annotations) { + std::string type = anno.value("type", ""); + std::string name = anno.value("name", ""); + std::string value = anno.value("value", ""); + if (!type.empty() || !name.empty() || !value.empty()) { + std::string line = type; + if (!name.empty()) line += ":" + name; + if (!value.empty()) line += "=" + value; + out.push_back(line); + } + } + if (item.reviewRequired) out.push_back("requires-review"); + if (out.empty()) out.push_back("none"); + return out; +} + +inline ContextBundle buildBundle(const WorkItem& item, + const WorkerContext& context, + const RoutingDecision& routingDecision) { + ContextBundle bundle; + bundle.taskDescription = "Implement " + item.nodeType + " '" + item.nodeName + "'"; + bundle.skeletonCode = context.nodeAst.is_null() ? "" : context.nodeAst.dump(2); + bundle.intent = context.skeletonIntent; + if (bundle.intent.empty() && !item.rejectionFeedback.empty()) { + bundle.intent = item.rejectionFeedback; + } + bundle.constraints = collectBundleConstraints(context, item); + bundle.contextCode = context.bufferContent; + bundle.projectSummary = context.projectSummary.is_null() ? "" : + context.projectSummary.dump(2); + bundle.existingTests = ""; + + for (const auto& attempt : item.rejectionHistory) { + bundle.previousAttempts.push_back({ + attempt.workerType, + attempt.result.generatedCode, + attempt.feedback + }); + } + + bundle.tokenBudget = routingDecision.contextBudgetTokens > 0 + ? routingDecision.contextBudgetTokens + : estimateContextBudget(routingDecision.contextWidth); + bundle.outputFormat = (routingDecision.workerType == "deterministic" || + routingDecision.workerType == "template") + ? "code-only" + : "code-with-explanation"; + return bundle; +} + +inline std::string bundleToPrompt(const ContextBundle& bundle) { + std::ostringstream oss; + oss << "Task:\n" << bundle.taskDescription << "\n\n"; + oss << "Intent:\n" << (bundle.intent.empty() ? "(none)" : bundle.intent) << "\n\n"; + oss << "Constraints:\n"; + for (const auto& c : bundle.constraints) { + oss << "- " << c << "\n"; + } + oss << "\nSkeleton:\n" << (bundle.skeletonCode.empty() ? "(none)" : bundle.skeletonCode) << "\n\n"; + oss << "Context Code:\n" << (bundle.contextCode.empty() ? "(none)" : bundle.contextCode) << "\n\n"; + oss << "Project Summary:\n" << (bundle.projectSummary.empty() ? "(none)" : bundle.projectSummary) << "\n\n"; + oss << "Previous Attempts:\n"; + if (bundle.previousAttempts.empty()) { + oss << "(none)\n"; + } else { + for (const auto& a : bundle.previousAttempts) { + oss << "- worker=" << a.workerType << "\n"; + if (!a.generatedCode.empty()) oss << " code:\n" << a.generatedCode << "\n"; + if (!a.feedback.empty()) oss << " feedback: " << a.feedback << "\n"; + } + } + oss << "\nToken Budget: " << bundle.tokenBudget << "\n"; + oss << "Output Format: " << bundle.outputFormat << "\n"; + return oss.str(); +} + +inline json bundleToJson(const ContextBundle& bundle) { + return bundle.toJson(); +} + +inline int estimateBundleTokens(const ContextBundle& bundle) { + return static_cast(bundle.toJson().dump().size() / 4); +} diff --git a/editor/tests/step385_test.cpp b/editor/tests/step385_test.cpp new file mode 100644 index 0000000..c4ef32f --- /dev/null +++ b/editor/tests/step385_test.cpp @@ -0,0 +1,192 @@ +// Step 385: Context bundle format (12 tests) + +#include +#include +#include +#include "ContextBundle.h" + +static WorkItem makeItem(const std::string& id = "w1") { + WorkItem item; + item.id = id; + item.nodeId = id + "_node"; + item.nodeName = "buildThing"; + item.nodeType = "Function"; + item.bufferId = "main.py"; + item.contextWidth = "project"; + item.workerType = "llm"; + item.reviewRequired = true; + item.priority = "high"; + item.status = WI_READY; + item.createdAt = workItemTimestamp(); + return item; +} + +static WorkerContext makeContext() { + WorkerContext ctx; + ctx.nodeAst = json{{"id", "w1_node"}, {"type", "Function"}, {"name", "buildThing"}}; + ctx.bufferContent = "def helper():\n return 1\n"; + ctx.projectSummary = json{{"files", 4}, {"keyModules", json::array({"core", "api"})}}; + ctx.annotations = { + json{{"type", "Contract"}, {"name", "precondition"}, {"value", "input non-empty"}}, + json{{"type", "Risk"}, {"name", "level"}, {"value", "medium"}} + }; + ctx.skeletonIntent = "Build normalized result map"; + return ctx; +} + +static RoutingDecision makeDecision() { + RoutingDecision d; + d.workerType = "llm"; + d.contextWidth = "project"; + d.contextBudgetTokens = 8000; + d.reviewRequired = true; + d.confidence = 0.7f; + d.reasoning = "Project context"; + d.agentRole = "generator"; + return d; +} + +int main() { + int passed = 0; + + // Test 1: bundle contains core fields + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + assert(!b.taskDescription.empty()); + assert(!b.skeletonCode.empty()); + assert(!b.contextCode.empty()); + assert(b.tokenBudget == 8000); + std::cout << "Test 1 PASSED: bundle contains required fields\n"; + passed++; + } + + // Test 2: bundleToPrompt produces structured readable output + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + std::string prompt = bundleToPrompt(b); + assert(prompt.find("Task:") != std::string::npos); + assert(prompt.find("Constraints:") != std::string::npos); + assert(prompt.find("Output Format:") != std::string::npos); + std::cout << "Test 2 PASSED: prompt rendering structured\n"; + passed++; + } + + // Test 3: rejection history included in previous attempts + { + WorkItem item = makeItem(); + RejectionAttempt a; + a.workerType = "slm"; + a.result.generatedCode = "return tmp"; + a.feedback = "missed edge case"; + item.rejectionHistory.push_back(a); + auto b = buildBundle(item, makeContext(), makeDecision()); + assert(b.previousAttempts.size() == 1); + assert(b.previousAttempts[0].feedback == "missed edge case"); + std::cout << "Test 3 PASSED: rejection history carried into bundle\n"; + passed++; + } + + // Test 4: token estimate is positive and size-correlated + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + int estimate = estimateBundleTokens(b); + assert(estimate > 0); + assert(std::abs(estimate - (int)(b.toJson().dump().size() / 4)) <= 1); + std::cout << "Test 4 PASSED: token estimate reasonable\n"; + passed++; + } + + // Test 5: budget value preserved from routing decision + { + RoutingDecision d = makeDecision(); + d.contextBudgetTokens = 1234; + auto b = buildBundle(makeItem(), makeContext(), d); + assert(b.tokenBudget == 1234); + std::cout << "Test 5 PASSED: budget respected from routing decision\n"; + passed++; + } + + // Test 6: annotation constraints included + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + assert(!b.constraints.empty()); + bool hasContract = false; + for (const auto& c : b.constraints) { + if (c.find("Contract") != std::string::npos) hasContract = true; + } + assert(hasContract); + std::cout << "Test 6 PASSED: constraints include annotations\n"; + passed++; + } + + // Test 7: intent comes from worker context + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + assert(b.intent == "Build normalized result map"); + std::cout << "Test 7 PASSED: intent carried from context\n"; + passed++; + } + + // Test 8: empty context fields handled gracefully + { + WorkItem item = makeItem(); + WorkerContext ctx; + RoutingDecision d = makeDecision(); + auto b = buildBundle(item, ctx, d); + std::string prompt = bundleToPrompt(b); + assert(!prompt.empty()); + assert(b.constraints.size() == 1); // default "none" + std::cout << "Test 8 PASSED: empty fields handled gracefully\n"; + passed++; + } + + // Test 9: bundle JSON roundtrip + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + auto round = ContextBundle::fromJson(bundleToJson(b)); + assert(round.taskDescription == b.taskDescription); + assert(round.tokenBudget == b.tokenBudget); + assert(round.outputFormat == b.outputFormat); + std::cout << "Test 9 PASSED: JSON roundtrip works\n"; + passed++; + } + + // Test 10: prompt format remains model-agnostic + { + auto b = buildBundle(makeItem(), makeContext(), makeDecision()); + std::string prompt = bundleToPrompt(b); + assert(prompt.find("Claude") == std::string::npos); + assert(prompt.find("OpenAI") == std::string::npos); + std::cout << "Test 10 PASSED: prompt format model-agnostic\n"; + passed++; + } + + // Test 11: deterministic/template defaults to code-only output format + { + RoutingDecision d = makeDecision(); + d.workerType = "template"; + auto b = buildBundle(makeItem(), makeContext(), d); + assert(b.outputFormat == "code-only"); + std::cout << "Test 11 PASSED: code-only format for deterministic/template\n"; + passed++; + } + + // Test 12: previous attempts preserve insertion order + { + WorkItem item = makeItem(); + RejectionAttempt a1; a1.workerType = "slm"; a1.feedback = "first"; + RejectionAttempt a2; a2.workerType = "llm"; a2.feedback = "second"; + item.rejectionHistory.push_back(a1); + item.rejectionHistory.push_back(a2); + auto b = buildBundle(item, makeContext(), makeDecision()); + assert(b.previousAttempts.size() == 2); + assert(b.previousAttempts[0].feedback == "first"); + assert(b.previousAttempts[1].feedback == "second"); + std::cout << "Test 12 PASSED: previous-attempt order preserved\n"; + passed++; + } + + std::cout << "\nResults: " << passed << "/12\n"; + assert(passed == 12); + return 0; +} diff --git a/progress.md b/progress.md index edce948..0635bd7 100644 --- a/progress.md +++ b/progress.md @@ -3063,6 +3063,45 @@ and next-action guidance for clients. - `editor/src/WorkflowProtocol.h` within header-size limit (`185` <= `600`) - `editor/tests/step384_test.cpp` within test-file size guidance (`177` lines) +### Step 385: Context Bundle Format +**Status:** PASS (12/12 tests) + +Standardized the external-model context contract with a structured bundle, +prompt rendering, JSON transport format, and token estimation utilities. + +**Files created:** +- `editor/src/ContextBundle.h` — context bundle model: + - `ContextBundle` + `AttemptSummary` + - `buildBundle(workItem, workerContext, routingDecision)` + - `bundleToPrompt(bundle)` (model-agnostic prompt rendering) + - `bundleToJson(bundle)` / `ContextBundle::fromJson(...)` + - `estimateBundleTokens(bundle)` +- `editor/tests/step385_test.cpp` — 12 tests covering: + 1. required bundle field population + 2. prompt structure readability + 3. rejection-history inclusion + 4. token-estimate reasonableness + 5. routing budget propagation + 6. annotation-constraint extraction + 7. intent propagation + 8. empty-field handling + 9. JSON roundtrip + 10. model-agnostic prompt format + 11. output-format policy (`code-only` vs `code-with-explanation`) + 12. previous-attempt ordering + +**Files modified:** +- `editor/CMakeLists.txt` — `step385_test` target + +**Verification run:** +- `step385_test` — PASS (12/12) new step coverage +- `step384_test` — PASS (12/12) regression coverage +- `step383_test` — PASS (8/8) regression coverage + +**Architecture gate check:** +- `editor/src/ContextBundle.h` within header-size limit (`168` <= `600`) +- `editor/tests/step385_test.cpp` within test-file size guidance (`192` lines) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)