Fix cross-project targetFiles contamination in generate_taskitems
Two bugs caused taskitems to emit constcad/README.md and hivemind/README.md as targetFiles regardless of workspace: 1. Negation blindness: acceptance criteria like "no constcad, no hivemind paths" were being parsed as cross-project repo signals. Added isPrecededByNegation() helper and skip acceptance-criteria anchor entirely in the repo signal scan — those sections describe what to check, not what to target. 2. Wrong fallback order: cross-project targets were tried before workspace targets. Swapped the order so inferWorkspaceTaskTargets and inferWorkspaceFallbackTargets run first; cross-project is now last resort only when the workspace itself has nothing. Verified: sprint 02 whimptk taskitem run now emits correct whimptk-local paths (include/whimptk/InterfaceRequirements.h, src/InterfaceRequirements.cpp, etc.) with 0 escalations and executionSpecificityScore 75-90. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,29 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if `token` appears in `text` immediately preceded by a negation
|
||||
// word ("no", "not", "without", "avoid", "don't", "do not"). Used to suppress
|
||||
// cross-project repo signals when the spec explicitly says to avoid a project.
|
||||
static bool isPrecededByNegation(const std::string& text, const std::string& token) {
|
||||
static const std::vector<std::string> kNegations = {
|
||||
"no ", "not ", "without ", "avoid ", "don't ", "do not "
|
||||
};
|
||||
std::size_t pos = 0;
|
||||
while ((pos = text.find(token, pos)) != std::string::npos) {
|
||||
const std::size_t end = pos + token.size();
|
||||
const bool rightOk = end >= text.size() || !isTokenChar(text[end]);
|
||||
if (rightOk) {
|
||||
for (const auto& neg : kNegations) {
|
||||
if (pos >= neg.size() && text.substr(pos - neg.size(), neg.size()) == neg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void appendUnique(std::vector<std::string>* out, const std::string& value) {
|
||||
if (!out || value.empty()) return;
|
||||
for (const auto& existing : *out) {
|
||||
@@ -471,8 +494,14 @@
|
||||
if (lower.find("npm test") != std::string::npos) appendUnique(&commands, "npm test");
|
||||
if (lower.find("cmake --build") != std::string::npos) appendUnique(&commands, "cmake --build .");
|
||||
|
||||
// Skip acceptance-criteria requirements: they describe what to check,
|
||||
// not what to target, and often name projects explicitly to exclude them.
|
||||
if (requirement.anchor == "acceptance-criteria") continue;
|
||||
for (const auto& repo : knownCrossProjectRepos) {
|
||||
if (containsStandaloneToken(lower, repo)) appendUnique(&repoSignalCandidates, repo);
|
||||
if (containsStandaloneToken(lower, repo) &&
|
||||
!isPrecededByNegation(lower, repo)) {
|
||||
appendUnique(&repoSignalCandidates, repo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,6 +617,18 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool isWhetstoneWorkspaceRoot(const std::string& workspaceRoot) {
|
||||
namespace fs = std::filesystem;
|
||||
if (workspaceRoot.empty()) return false;
|
||||
std::error_code ec;
|
||||
const fs::path root = fs::weakly_canonical(fs::path(workspaceRoot), ec);
|
||||
if (ec || root.empty() || !fs::exists(root, ec) || !fs::is_directory(root, ec)) {
|
||||
return false;
|
||||
}
|
||||
return fs::exists(root / "editor/src/mcp/RegisterArchitectIntakeTools.h", ec) &&
|
||||
fs::exists(root / "tools/mcp/run_sprint_taskitem_pipeline.sh", ec);
|
||||
}
|
||||
|
||||
static bool hasCrossProjectIntent(const std::vector<NormalizedRequirement>& requirements) {
|
||||
int hitCount = 0;
|
||||
for (const auto& requirement : requirements) {
|
||||
@@ -732,21 +773,47 @@
|
||||
const std::string lowerTitle = intakeToLowerCopy(task.base.title + " " + task.base.intent);
|
||||
const fs::path root(workspaceRoot);
|
||||
const bool hasCargoWorkspace = !workspaceRoot.empty() && fs::exists(root / "Cargo.toml");
|
||||
const bool hasCMakeWorkspace = !workspaceRoot.empty() && fs::exists(root / "CMakeLists.txt");
|
||||
const fs::path crateCargo = root / "crates" / "whimpwm" / "Cargo.toml";
|
||||
const fs::path crateMain = root / "crates" / "whimpwm" / "src" / "main.rs";
|
||||
const fs::path crateFocus = root / "crates" / "whimpwm" / "src" / "focus_streams.rs";
|
||||
const fs::path crateCompositor = root / "crates" / "whimpwm" / "src" / "compositor.rs";
|
||||
const fs::path readme = root / "README.md";
|
||||
const fs::path vision = root / "docs" / "vision.md";
|
||||
const fs::path cmakeLists = root / "CMakeLists.txt";
|
||||
const fs::path docsArchitecture = root / "docs" / "architecture.md";
|
||||
const fs::path docsRoadmap = root / "docs" / "roadmap.md";
|
||||
const fs::path widgetHeader = root / "include" / "whimptk" / "WidgetPrimitives.h";
|
||||
const fs::path toolkitHeader = root / "include" / "whimptk" / "Toolkit.h";
|
||||
const fs::path widgetSource = root / "src" / "WidgetPrimitives.cpp";
|
||||
const fs::path toolkitSource = root / "src" / "Toolkit.cpp";
|
||||
const fs::path demoSource = root / "examples" / "whimptk_demo.cpp";
|
||||
|
||||
auto pushRelative = [&](const fs::path& path) {
|
||||
if (workspaceRoot.empty()) return;
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec) || !fs::is_regular_file(path, ec)) return;
|
||||
const fs::path rel = fs::relative(path, root, ec);
|
||||
if (ec) return;
|
||||
appendUnique(&targets, rel.generic_string());
|
||||
};
|
||||
|
||||
const bool mentionsDocs = lowerTitle.find("doc") != std::string::npos ||
|
||||
lowerTitle.find("readme") != std::string::npos ||
|
||||
lowerTitle.find("roadmap") != std::string::npos ||
|
||||
lowerTitle.find("architecture") != std::string::npos;
|
||||
const bool mentionsBuild = lowerTitle.find("verify") != std::string::npos ||
|
||||
lowerTitle.find("build") != std::string::npos ||
|
||||
lowerTitle.find("test") != std::string::npos ||
|
||||
lowerTitle.find("smoke") != std::string::npos;
|
||||
const bool mentionsDemo = lowerTitle.find("demo") != std::string::npos ||
|
||||
lowerTitle.find("example") != std::string::npos;
|
||||
const bool mentionsWidget = lowerTitle.find("widget") != std::string::npos ||
|
||||
lowerTitle.find("primitive") != std::string::npos ||
|
||||
lowerTitle.find("content") != std::string::npos ||
|
||||
lowerTitle.find("binding") != std::string::npos ||
|
||||
lowerTitle.find("presentation") != std::string::npos;
|
||||
|
||||
if (containsStandaloneToken(lowerTitle, "smithay") ||
|
||||
lowerTitle.find("dependency") != std::string::npos ||
|
||||
lowerTitle.find("cargo") != std::string::npos) {
|
||||
@@ -782,6 +849,29 @@
|
||||
if (fs::exists(crateMain)) pushRelative(crateMain);
|
||||
}
|
||||
|
||||
if (hasCMakeWorkspace) {
|
||||
if (mentionsDocs) {
|
||||
if (fs::exists(readme)) pushRelative(readme);
|
||||
if (fs::exists(docsRoadmap)) pushRelative(docsRoadmap);
|
||||
if (fs::exists(docsArchitecture)) pushRelative(docsArchitecture);
|
||||
}
|
||||
if (mentionsWidget) {
|
||||
if (fs::exists(widgetHeader)) pushRelative(widgetHeader);
|
||||
if (fs::exists(widgetSource)) pushRelative(widgetSource);
|
||||
if (fs::exists(toolkitHeader)) pushRelative(toolkitHeader);
|
||||
if (fs::exists(toolkitSource)) pushRelative(toolkitSource);
|
||||
}
|
||||
if (mentionsDemo) {
|
||||
if (fs::exists(demoSource)) pushRelative(demoSource);
|
||||
if (fs::exists(toolkitSource)) pushRelative(toolkitSource);
|
||||
if (fs::exists(widgetSource)) pushRelative(widgetSource);
|
||||
}
|
||||
if (mentionsBuild) {
|
||||
if (fs::exists(cmakeLists)) pushRelative(cmakeLists);
|
||||
if (fs::exists(demoSource)) pushRelative(demoSource);
|
||||
}
|
||||
}
|
||||
|
||||
json out = json::array();
|
||||
for (const auto& file : sortedUnique(targets)) out.push_back(file);
|
||||
return out;
|
||||
@@ -1049,7 +1139,13 @@
|
||||
const json& hints,
|
||||
bool strictExecutionContract,
|
||||
const std::string& workspaceRoot) {
|
||||
const std::string effectiveWorkspaceRoot = hints.value("projectRoot", workspaceRoot);
|
||||
std::string effectiveWorkspaceRoot = workspaceRoot;
|
||||
if (hints.contains("projectRoot") && hints["projectRoot"].is_string()) {
|
||||
const std::string hintedProjectRoot = hints["projectRoot"].get<std::string>();
|
||||
if (!hintedProjectRoot.empty()) {
|
||||
effectiveWorkspaceRoot = hintedProjectRoot;
|
||||
}
|
||||
}
|
||||
json stepIds = hints.value("stepIds", json::array());
|
||||
json targetFiles = hints.value("targetFiles", json::array());
|
||||
json requiredTools = hints.value("requiredTools", json::array());
|
||||
@@ -1075,24 +1171,26 @@
|
||||
}
|
||||
}
|
||||
if (!targetFiles.is_array() || targetFiles.empty()) {
|
||||
json crossProjectTargets = inferCrossProjectTargetsFromHints(hints);
|
||||
if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) {
|
||||
targetFiles = crossProjectTargets;
|
||||
// Try workspace-local targets first before falling back to cross-project.
|
||||
json taskTargets = inferWorkspaceTaskTargets(task, effectiveWorkspaceRoot);
|
||||
if (taskTargets.is_array() && !taskTargets.empty()) {
|
||||
targetFiles = taskTargets;
|
||||
} else {
|
||||
json taskTargets = inferWorkspaceTaskTargets(task, effectiveWorkspaceRoot);
|
||||
if (taskTargets.is_array() && !taskTargets.empty()) {
|
||||
targetFiles = taskTargets;
|
||||
} else {
|
||||
json workspaceTargets = inferWorkspaceFallbackTargets(effectiveWorkspaceRoot);
|
||||
if (workspaceTargets.is_array() && !workspaceTargets.empty()) {
|
||||
targetFiles = workspaceTargets;
|
||||
} else {
|
||||
targetFiles = json::array({
|
||||
"editor/src/mcp/RegisterArchitectIntakeTools.h",
|
||||
"editor/src/mcp/RegisterValidationTools.h",
|
||||
"tools/mcp/run_sprint_taskitem_pipeline.sh"
|
||||
});
|
||||
}
|
||||
// Only use cross-project targets if the workspace itself has nothing.
|
||||
json crossProjectTargets = inferCrossProjectTargetsFromHints(hints);
|
||||
if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) {
|
||||
targetFiles = crossProjectTargets;
|
||||
} else if (isWhetstoneWorkspaceRoot(effectiveWorkspaceRoot)) {
|
||||
targetFiles = json::array({
|
||||
"editor/src/mcp/RegisterArchitectIntakeTools.h",
|
||||
"editor/src/mcp/RegisterValidationTools.h",
|
||||
"tools/mcp/run_sprint_taskitem_pipeline.sh"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user