Fix cross-project target grounding and add handoff note
This commit is contained in:
@@ -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"},
|
||||
|
||||
@@ -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}
|
||||
}}
|
||||
}}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user