Fix cross-project target grounding and add handoff note

This commit is contained in:
Bill
2026-03-08 10:53:30 -06:00
parent 9f9d9590c3
commit a09fc55325
6 changed files with 670 additions and 22 deletions

View File

@@ -282,11 +282,81 @@
};
}
static std::string trimPathToken(const std::string& in) {
if (in.empty()) return "";
std::size_t start = 0;
std::size_t end = in.size();
auto isTrimChar = [](char c) {
return c == '"' || c == '\'' || c == '`' || c == ',' || c == ';' || c == ':' ||
c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}';
};
while (start < end && (in[start] == ' ' || in[start] == '\t' || isTrimChar(in[start]))) ++start;
while (end > start && (in[end - 1] == ' ' || in[end - 1] == '\t' || isTrimChar(in[end - 1]))) --end;
return in.substr(start, end - start);
}
static bool looksLikePathToken(const std::string& token) {
if (token.empty()) return false;
if (token.find(' ') != std::string::npos) return false;
const bool hasSlash = token.find('/') != std::string::npos;
const bool hasDot = token.find('.') != std::string::npos;
if (!hasSlash || !hasDot) return false;
if (token.rfind("http://", 0) == 0 || token.rfind("https://", 0) == 0) return false;
if (token.rfind("wstone_", 0) == 0 || token.rfind("cd ", 0) == 0) return false;
if (token.find('<') != std::string::npos || token.find('>') != std::string::npos) return false;
return true;
}
static std::string normalizeTargetFileCandidate(const std::string& raw) {
std::string value = trimPathToken(raw);
if (value.empty()) return "";
if (value.rfind("./", 0) == 0) value = value.substr(2);
const std::string homeDocsPrefix = "/home/bill/Documents/";
if (value.rfind(homeDocsPrefix, 0) == 0) {
value = value.substr(homeDocsPrefix.size());
}
if (value.rfind("CLionProjects/whetstone_DSL/", 0) == 0) {
value = value.substr(std::string("CLionProjects/whetstone_DSL/").size());
}
if (!looksLikePathToken(value)) return "";
return value;
}
static std::vector<std::string> extractPathTokensFromText(const std::string& text) {
std::vector<std::string> out;
std::string token;
auto flush = [&out, &token]() {
if (token.empty()) return;
const std::string normalized = normalizeTargetFileCandidate(token);
if (!normalized.empty()) appendUnique(&out, normalized);
token.clear();
};
for (char c : text) {
if (c == ' ' || c == '\n' || c == '\t' || c == ',' || c == ';' || c == '(' || c == ')') {
flush();
} else {
token.push_back(c);
}
}
flush();
return out;
}
static json collectExecutionHints(const std::vector<NormalizedRequirement>& requirements) {
std::vector<std::string> stepIds;
std::vector<std::string> files;
std::vector<std::string> tools;
std::vector<std::string> commands;
std::vector<std::string> repoSignals;
const std::vector<std::string> knownCrossProjectRepos = {
"cad_orchestrator",
"hivemind",
"gleaner",
"constcad",
"whetstone_planning_tool",
"whetstone_ubuntu_frontend_tool"
};
for (const auto& requirement : requirements) {
const std::string text = requirement.normalizedText;
@@ -317,12 +387,14 @@
std::size_t end = text.find('`', pos + 1);
if (end == std::string::npos) break;
std::string candidate = text.substr(pos + 1, end - pos - 1);
if (candidate.find('/') != std::string::npos || candidate.find('.') != std::string::npos) {
appendUnique(&files, candidate);
}
std::string normalizedPath = normalizeTargetFileCandidate(candidate);
if (!normalizedPath.empty()) appendUnique(&files, normalizedPath);
pos = end + 1;
}
const auto inlinePathTokens = extractPathTokensFromText(text);
for (const auto& p : inlinePathTokens) appendUnique(&files, p);
pos = 0;
while ((pos = lower.find("step", pos)) != std::string::npos) {
std::size_t end = pos + 4;
@@ -339,16 +411,123 @@
if (lower.find("go test") != std::string::npos) appendUnique(&commands, "go test ./...");
if (lower.find("npm test") != std::string::npos) appendUnique(&commands, "npm test");
if (lower.find("cmake --build") != std::string::npos) appendUnique(&commands, "cmake --build .");
for (const auto& repo : knownCrossProjectRepos) {
if (lower.find(repo) != std::string::npos) appendUnique(&repoSignals, repo);
}
}
return {
{"stepIds", sortedUnique(stepIds)},
{"targetFiles", sortedUnique(files)},
{"requiredTools", sortedUnique(tools)},
{"acceptanceCommands", sortedUnique(commands)}
{"acceptanceCommands", sortedUnique(commands)},
{"crossProjectRepos", sortedUnique(repoSignals)}
};
}
static bool isWhetstoneInternalPath(const std::string& path) {
const std::string lower = intakeToLowerCopy(path);
return lower.rfind("editor/", 0) == 0 ||
lower.rfind("tools/", 0) == 0 ||
lower.rfind("logs/", 0) == 0;
}
static bool isKnownCrossProjectRepo(const std::string& value) {
const std::string lower = intakeToLowerCopy(value);
return lower == "cad_orchestrator" ||
lower == "hivemind" ||
lower == "gleaner" ||
lower == "constcad" ||
lower == "whetstone_planning_tool" ||
lower == "whetstone_ubuntu_frontend_tool";
}
static json inferCrossProjectTargetsFromHints(const json& hints) {
std::vector<std::string> targets;
const json repos = hints.value("crossProjectRepos", json::array());
if (!repos.is_array()) return json::array();
for (const auto& repoJson : repos) {
if (!repoJson.is_string()) continue;
const std::string repo = intakeToLowerCopy(repoJson.get<std::string>());
if (!isKnownCrossProjectRepo(repo)) continue;
appendUnique(&targets, repo + "/README.md");
}
return sortedUnique(targets);
}
static bool hasCrossProjectIntent(const std::vector<NormalizedRequirement>& requirements) {
int hitCount = 0;
for (const auto& requirement : requirements) {
const std::string lower = intakeToLowerCopy(requirement.normalizedText);
if (lower.find("cross-repo") != std::string::npos ||
lower.find("multi-project") != std::string::npos ||
lower.find("multi-repo") != std::string::npos ||
lower.find("ecosystem") != std::string::npos) {
return true;
}
if (lower.find("cad_orchestrator") != std::string::npos) ++hitCount;
if (lower.find("hivemind") != std::string::npos) ++hitCount;
if (lower.find("gleaner") != std::string::npos) ++hitCount;
if (lower.find("constcad") != std::string::npos) ++hitCount;
if (hitCount >= 2) return true;
}
return false;
}
static bool hasOnlyInternalTargets(const json& tasks) {
if (!tasks.is_array() || tasks.empty()) return true;
bool sawTarget = false;
for (const auto& task : tasks) {
if (!task.is_object()) continue;
const json contract = task.value("executionContract", json::object());
const json files = contract.value("targetFiles", json::array());
if (!files.is_array()) continue;
for (const auto& fileJson : files) {
if (!fileJson.is_string()) continue;
sawTarget = true;
if (!isWhetstoneInternalPath(fileJson.get<std::string>())) return false;
}
}
return sawTarget;
}
static std::string parseExplicitTargetFileFromRequirementText(const std::string& text) {
const std::string prefix = "explicit target file:";
const std::string lower = intakeToLowerCopy(text);
if (lower.rfind(prefix, 0) != 0) return "";
std::string raw = text.substr(prefix.size());
return normalizeTargetFileCandidate(raw);
}
static std::string requirementIdFromSliceTitle(const std::string& title) {
const std::string prefix = "Requirement Slice ";
if (title.rfind(prefix, 0) != 0) return "";
std::string id = title.substr(prefix.size());
return trimPathToken(id);
}
static json selectTaskScopedTargetFiles(const AnnotatedTaskitem& task,
const std::vector<NormalizedRequirement>& requirements,
const json& defaultTargetFiles) {
std::vector<std::string> scoped;
const std::string sliceRequirementId = requirementIdFromSliceTitle(task.base.title);
for (const auto& requirement : requirements) {
const std::string file = parseExplicitTargetFileFromRequirementText(requirement.normalizedText);
if (file.empty()) continue;
const bool exactMatch = !sliceRequirementId.empty() && requirement.requirementId == sliceRequirementId;
const bool containsMatch = !sliceRequirementId.empty() &&
task.base.title.find(requirement.requirementId) != std::string::npos;
if (exactMatch || containsMatch) {
appendUnique(&scoped, file);
}
}
if (scoped.empty()) return defaultTargetFiles;
json out = json::array();
for (const auto& file : sortedUnique(scoped)) out.push_back(file);
return out;
}
static json inferResourceLocks(const AnnotatedTaskitem& task) {
std::vector<std::string> locks;
const std::string title = intakeToLowerCopy(task.base.title);
@@ -407,11 +586,16 @@
});
}
if (!targetFiles.is_array() || targetFiles.empty()) {
targetFiles = json::array({
"editor/src/mcp/RegisterArchitectIntakeTools.h",
"editor/src/mcp/RegisterValidationTools.h",
"tools/mcp/run_sprint_taskitem_pipeline.sh"
});
json crossProjectTargets = inferCrossProjectTargetsFromHints(hints);
if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) {
targetFiles = crossProjectTargets;
} else {
targetFiles = json::array({
"editor/src/mcp/RegisterArchitectIntakeTools.h",
"editor/src/mcp/RegisterValidationTools.h",
"tools/mcp/run_sprint_taskitem_pipeline.sh"
});
}
}
if (!stepIds.is_array() || stepIds.empty()) {
stepIds = json::array({"1939", "1940", "1941"});
@@ -578,7 +762,10 @@
{"escalate", task.escalate},
{"reasons", task.reasons}
};
json contract = buildExecutionContract(task, executionHints, strictExecutionContract);
json taskExecutionHints = executionHints;
taskExecutionHints["targetFiles"] = selectTaskScopedTargetFiles(task, normalized.requirements,
executionHints.value("targetFiles", json::array()));
json contract = buildExecutionContract(task, taskExecutionHints, strictExecutionContract);
if (libraryDispatchEnabled) {
std::string dispatchText = task.base.title;
if (!libraryDispatchContext.empty()) {
@@ -893,6 +1080,10 @@
if (escalateCount > 0) blockers.push_back("escalations_present");
if (strictExecutionContract && executionContractMissingCount > 0) blockers.push_back("missing_execution_contract");
if (strictExecutionContract && executionSpecificityLowCount > 0) blockers.push_back("execution_specificity_low");
if (strictExecutionContract && hasCrossProjectIntent(requirements) && hasOnlyInternalTargets(args.value("tasks", json::array()))) {
blockers.push_back("cross_project_target_mismatch");
queueWarnings.push_back("cross_project_target_mismatch");
}
bool capabilityGap = false;
if (args.contains("capabilitySignals") && args["capabilitySignals"].is_object()) {
const json& signals = args["capabilitySignals"];
@@ -948,9 +1139,11 @@
{"debug_loop_call_plan_fingerprint", debugLoopCallPlan.value("fingerprint", "")},
{"next_action", dominantGapClass == "under_constrained_taskitems"
? "tighten_taskitem_generation_contract"
: (dominantGapClass == "cross_project_targeting_gap"
? "regenerate_with_explicit_cross_project_targets"
: (dominantGapClass == "generator_capability_gap"
? "run_debug_or_codegen_capability_loop"
: "continue_with_validation_and_queue_checks")}
: "continue_with_validation_and_queue_checks"))}
};
return {
@@ -1159,6 +1352,18 @@
})}
};
}
if (blocker == "cross_project_target_mismatch") {
return {
{"blocker", blocker},
{"gap_class", "cross_project_targeting_gap"},
{"likely_root_cause", "cross-project intent detected but generated target files resolve only to whetstone-internal paths"},
{"recommended_tools", json::array({
"whetstone_architect_intake",
"whetstone_generate_taskitems",
"whetstone_queue_ready"
})}
};
}
return {
{"blocker", blocker},
{"gap_class", "unspecified"},

View File

@@ -7,7 +7,10 @@
#include "../TaskitemQualityAuditor.h"
#include <algorithm>
#include <filesystem>
#include <map>
#include <cctype>
void registerValidationTools() {
tools_.push_back({"whetstone_validate_taskitem",
@@ -22,8 +25,18 @@
{"prerequisite_ops", {{"type", "array"}, {"items", {{"type", "string"}}}}},
{"reasons", {{"type", "array"}, {"items", {{"type", "string"}}}}},
{"confidence", {{"type", "integer"}}},
{"dependency_task_ids", {{"type", "array"}, {"items", {{"type", "string"}}}}}
{"dependency_task_ids", {{"type", "array"}, {"items", {{"type", "string"}}}}},
{"execution_contract", {{"type", "object"},
{"description", "Optional execution contract metadata from task generation."}}},
{"resource_locks", {{"type", "array"}, {"items", {{"type", "string"}}},
{"description", "Optional resource lock names for mutual exclusion."}}},
{"capability_signals", {{"type", "object"},
{"description", "Optional capability failure signals (compile/test/toolchain)."}}}
}}, {"required", json::array({"task_id"})}}}}},
{"expected_project_roots", {{"type", "array"}, {"items", {{"type", "string"}}},
{"description", "Optional expected external project roots for cross-project target validation."}}},
{"strict_execution_contract", {{"type", "boolean"},
{"description", "Apply strict execution-contract scoring."}}},
{"workspace", {{"type", "string"},
{"description", "Workspace root path for file resolution."}}}
}}, {"required", json::array({"taskitems"})}}
@@ -34,12 +47,172 @@
};
}
static std::string classifyValidationIssueGapClass(const std::string& issue) {
if (issue.find("executionSpecificityScore") != std::string::npos ||
issue.find("execution contract") != std::string::npos ||
issue.find("reasons has fewer than 3") != std::string::npos) {
return "under_constrained_taskitem";
}
if (issue.find("cross_project_target_mismatch") != std::string::npos) {
return "cross_project_targeting_gap";
}
if (issue.find("capability_signals indicate") != std::string::npos) {
return "generator_capability_gap";
}
if (issue.find("unresolved file paths") != std::string::npos ||
issue.find("prerequisiteOps missing") != std::string::npos) {
return "environment_or_dependency_gap";
}
if (issue.find("dependencyTaskIds not empty") != std::string::npos) {
return "task_dependency_or_planning_gap";
}
return "generator_capability_gap";
}
static json recommendedToolsForValidationGap(const std::string& gapClass) {
if (gapClass == "under_constrained_taskitem") {
return json::array({"whetstone_generate_taskitems", "whetstone_queue_ready", "whetstone_validate_taskitem"});
}
if (gapClass == "environment_or_dependency_gap") {
return json::array({"whetstone_probe_toolchain_provider", "whetstone_normalize_diagnostics", "whetstone_validate_taskitem"});
}
if (gapClass == "task_dependency_or_planning_gap") {
return json::array({"whetstone_generate_taskitems", "whetstone_queue_ready"});
}
if (gapClass == "resource_lock_semantics_gap") {
return json::array({"whetstone_generate_taskitems", "whetstone_queue_ready", "whetstone_validate_taskitem"});
}
if (gapClass == "cross_project_targeting_gap") {
return json::array({"whetstone_architect_intake", "whetstone_generate_taskitems", "whetstone_queue_ready"});
}
return json::array({
"whetstone_probe_toolchain_provider",
"whetstone_capture_failure_packet",
"whetstone_debug_until_green"
});
}
static std::string lowerCopyValidation(const std::string& in) {
std::string out = in;
std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return out;
}
static bool isInternalTargetPath(const std::string& path) {
const std::string lower = lowerCopyValidation(path);
return lower.rfind("editor/", 0) == 0 ||
lower.rfind("tools/", 0) == 0 ||
lower.rfind("logs/", 0) == 0;
}
static bool hasCrossProjectTargetMismatch(const json& targetFiles,
const std::vector<std::string>& expectedProjectRoots) {
if (expectedProjectRoots.empty() || !targetFiles.is_array() || targetFiles.empty()) return false;
bool sawTarget = false;
for (const auto& target : targetFiles) {
if (!target.is_string()) continue;
sawTarget = true;
const std::string path = lowerCopyValidation(target.get<std::string>());
if (isInternalTargetPath(path)) continue;
for (const auto& root : expectedProjectRoots) {
if (root.empty()) continue;
const std::string prefix = lowerCopyValidation(root) + "/";
if (path.rfind(prefix, 0) == 0) return false;
}
}
return sawTarget;
}
static std::string stableValidationDigest(const std::string& input) {
unsigned long long state = 1469598103934665603ull;
for (unsigned char c : input) {
state ^= static_cast<unsigned long long>(c);
state *= 1099511628211ull;
}
const char* hex = "0123456789abcdef";
std::string out(16, '0');
for (int i = 15; i >= 0; --i) {
out[static_cast<std::size_t>(i)] = hex[state & 0x0full];
state >>= 4;
}
return out;
}
static json buildCapabilityGapCallPlan(bool hasCapabilityGap, bool strictExecutionContract) {
if (!hasCapabilityGap) {
return {
{"enabled", false},
{"profile", "none"},
{"calls", json::array()},
{"fingerprint", stableValidationDigest("none")}
};
}
json calls = json::array({
{
{"tool", "whetstone_probe_toolchain_provider"},
{"args", {
{"verify_provider", true},
{"verify_compiler", true},
{"strict_execution_contract", strictExecutionContract}
}}
},
{
{"tool", "whetstone_capture_failure_packet"},
{"args", {
{"include_compile_logs", true},
{"include_test_logs", true}
}}
},
{
{"tool", "whetstone_debug_until_green"},
{"args", {
{"max_iterations", 3},
{"enforce_determinism", true}
}}
}
});
return {
{"enabled", true},
{"profile", "generator_capability_gap_debug_loop_v1"},
{"calls", calls},
{"fingerprint", stableValidationDigest(calls.dump() + (strictExecutionContract ? "|strict=1" : "|strict=0"))}
};
}
static bool resourceLockLooksValid(const std::string& lockName) {
if (lockName.empty()) return false;
for (char c : lockName) {
const bool ok = (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == ':' || c == '-' || c == '_' || c == '/' || c == '.';
if (!ok) return false;
}
return true;
}
json runValidateTaskitem(const json& args) {
if (!args.contains("taskitems") || !args["taskitems"].is_array()) {
return {{"success", false}, {"error", "taskitems_missing_or_invalid"}};
}
std::vector<std::string> expectedProjectRoots;
if (args.contains("expected_project_roots")) {
if (!args["expected_project_roots"].is_array()) {
return {{"success", false}, {"error", "expected_project_roots_not_array"}};
}
for (const auto& root : args["expected_project_roots"]) {
if (!root.is_string()) continue;
std::string value = lowerCopyValidation(root.get<std::string>());
if (value.empty()) continue;
expectedProjectRoots.push_back(value);
}
}
std::vector<TaskitemInput> items;
const bool strictExecutionContract = args.value("strict_execution_contract", false);
for (const auto& itemJson : args["taskitems"]) {
if (!itemJson.is_object() || !itemJson.contains("task_id") || !itemJson["task_id"].is_string()) {
return {{"success", false}, {"error", "task_id_missing"}};
@@ -65,6 +238,19 @@
if (v.is_string()) item.dependencyTaskIds.push_back(v.get<std::string>());
}
}
if (itemJson.contains("execution_contract") && itemJson["execution_contract"].is_object()) {
const json& contract = itemJson["execution_contract"];
item.executionSpecificityScore = contract.value("executionSpecificityScore", 0);
} else {
item.executionSpecificityScore = itemJson.value("execution_specificity_score", 0);
}
item.strictExecutionContract = strictExecutionContract;
if (itemJson.contains("capability_signals") && itemJson["capability_signals"].is_object()) {
const json& c = itemJson["capability_signals"];
item.capabilityCompileFailed = c.value("compile_failed", false);
item.capabilityTestsFailed = c.value("tests_failed", false);
item.capabilityToolchainMissing = c.value("toolchain_missing", false);
}
items.push_back(std::move(item));
}
@@ -73,15 +259,114 @@
auto report = TaskitemQualityAuditor::audit(items, workspace);
json results = json::array();
std::map<std::string, int> gapClassCounts;
std::map<std::string, int> recommendedToolCounts;
std::size_t resultIndex = 0;
for (const auto& r : report.results) {
std::string dominantGapClass = "none";
std::vector<std::string> lockIssues;
if (resultIndex < args["taskitems"].size()) {
const auto& itemJson = args["taskitems"][resultIndex];
if (itemJson.is_object() && itemJson.contains("resource_locks") && itemJson["resource_locks"].is_array()) {
bool hasGlobal = false;
bool hasLocal = false;
for (const auto& lock : itemJson["resource_locks"]) {
if (!lock.is_string()) {
lockIssues.push_back("resource_locks contains non-string entries");
continue;
}
const std::string lockName = lock.get<std::string>();
if (!resourceLockLooksValid(lockName)) {
lockIssues.push_back("resource lock has invalid format: " + lockName);
}
if (lockName.rfind("global:", 0) == 0) hasGlobal = true;
else hasLocal = true;
}
if (hasGlobal && hasLocal) {
lockIssues.push_back("resource lock scope ambiguity: mixed local and global locks");
}
}
}
if (!r.issues.empty()) {
bool hasCapabilityIssue = false;
for (const auto& issue : r.issues) {
if (issue.find("capability_signals indicate") != std::string::npos) {
hasCapabilityIssue = true;
break;
}
}
if (hasCapabilityIssue) {
dominantGapClass = "generator_capability_gap";
} else {
dominantGapClass = classifyValidationIssueGapClass(r.issues.front());
}
}
if (!lockIssues.empty()) dominantGapClass = "resource_lock_semantics_gap";
if (resultIndex < args["taskitems"].size()) {
const auto& itemJson = args["taskitems"][resultIndex];
if (itemJson.is_object()) {
json contract = json::object();
if (itemJson.contains("execution_contract") && itemJson["execution_contract"].is_object()) {
contract = itemJson["execution_contract"];
} else if (itemJson.contains("executionContract") && itemJson["executionContract"].is_object()) {
contract = itemJson["executionContract"];
}
const json targetFiles = contract.value("targetFiles", json::array());
if (hasCrossProjectTargetMismatch(targetFiles, expectedProjectRoots)) {
lockIssues.push_back("cross_project_target_mismatch: generated targetFiles do not match expected project roots");
dominantGapClass = "cross_project_targeting_gap";
}
}
}
json recommendedTools = recommendedToolsForValidationGap(dominantGapClass);
gapClassCounts[dominantGapClass]++;
if (recommendedTools.is_array()) {
for (const auto& toolName : recommendedTools) {
if (toolName.is_string()) recommendedToolCounts[toolName.get<std::string>()]++;
}
}
json issuesOut = json::array();
for (const auto& issue : r.issues) issuesOut.push_back(issue);
for (const auto& issue : lockIssues) issuesOut.push_back(issue);
results.push_back({
{"task_id", r.taskId},
{"score", r.score},
{"execution_specificity_score", r.executionSpecificityScore},
{"self_contained", r.selfContained},
{"issues", r.issues}
{"issues", issuesOut},
{"gap_class", dominantGapClass},
{"recommended_tools", recommendedTools}
});
++resultIndex;
}
json gapCountsJson = json::object();
for (const auto& kv : gapClassCounts) gapCountsJson[kv.first] = kv.second;
std::vector<std::pair<std::string, int>> rankedTools(recommendedToolCounts.begin(), recommendedToolCounts.end());
std::sort(rankedTools.begin(), rankedTools.end(), [](const auto& a, const auto& b) {
if (a.second != b.second) return a.second > b.second;
return a.first < b.first;
});
json topTools = json::array();
for (const auto& kv : rankedTools) {
topTools.push_back({{"tool", kv.first}, {"count", kv.second}});
}
const bool executionPass = report.failingCount == 0;
const bool semanticPass = executionPass && report.warningCount == 0;
const bool recursivePass = executionPass && report.averageExecutionSpecificityScore >= 60.0;
const double confidence = std::max(0.0, std::min(1.0, (report.averageScore / 100.0) * (report.averageExecutionSpecificityScore / 100.0)));
bool hasCapabilityGap = false;
for (const auto& kv : gapClassCounts) {
if (kv.first == "generator_capability_gap" && kv.second > 0) {
hasCapabilityGap = true;
break;
}
}
json capabilityGapCallPlan = buildCapabilityGapCallPlan(hasCapabilityGap, strictExecutionContract);
return {
{"success", true},
{"report", {
@@ -90,9 +375,18 @@
{"warning_count", report.warningCount},
{"failing_count", report.failingCount},
{"average_score", report.averageScore},
{"average_execution_specificity_score", report.averageExecutionSpecificityScore},
{"results", results},
{"top_issues", report.topIssues}
{"top_issues", report.topIssues},
{"gap_class_counts", gapCountsJson},
{"top_recommended_tools", topTools},
{"capability_gap_call_plan", capabilityGapCallPlan},
{"promotion_packet", {
{"execution_pass", executionPass},
{"semantic_pass", semanticPass},
{"recursive_pass", recursivePass},
{"confidence", confidence}
}}
}}
};
}