Refine taskitem workflow guidance and sprint intake shaping

This commit is contained in:
Bill
2026-03-21 02:17:17 -06:00
parent 69243141dd
commit 017456a476
11 changed files with 1049 additions and 23 deletions

View File

@@ -29,6 +29,7 @@ public:
std::string line;
std::string sectionTitle;
std::string sectionAnchor;
std::string subsectionTitle;
std::string proseBuffer;
int proseStartLine = 0;
int lineNo = 0;
@@ -50,9 +51,23 @@ public:
++lineNo;
std::string trimmed = trim(line);
if (isHeading(trimmed)) {
flushProse();
sectionTitle = trimHeading(trimmed);
sectionAnchor = anchorize(sectionTitle);
const int level = headingLevel(trimmed);
if (level >= 3 && isRequirementHeading(sectionAnchor)) {
flushProse();
subsectionTitle = trimHeading(trimmed);
ParsedRequirement req;
req.text = normalizeText(subsectionTitle);
req.sourceText = subsectionTitle;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out.push_back(req);
} else {
flushProse();
sectionTitle = trimHeading(trimmed);
sectionAnchor = anchorize(sectionTitle);
subsectionTitle.clear();
}
continue;
}
@@ -71,10 +86,11 @@ public:
std::string bulletText = trim(trimmed.substr(2));
if (bulletText.empty()) continue;
if (isQuotedExampleItem(bulletText)) continue;
if (tryParseFieldList(bulletText, lineNo, sectionTitle, sectionAnchor, &out)) continue;
std::string contextualText = applySubsectionContext(subsectionTitle, bulletText);
if (tryParseFieldList(contextualText, lineNo, sectionTitle, sectionAnchor, &out)) continue;
ParsedRequirement req;
req.text = normalizeText(bulletText);
req.sourceText = bulletText;
req.text = normalizeText(contextualText);
req.sourceText = contextualText;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
@@ -89,20 +105,22 @@ public:
std::string itemText = trim(trimmed.substr(markerEnd + 1));
if (itemText.empty()) continue;
if (isQuotedExampleItem(itemText)) continue;
std::string contextualText = applySubsectionContext(subsectionTitle, itemText);
ParsedRequirement req;
req.text = normalizeText(itemText);
req.sourceText = itemText;
req.text = normalizeText(contextualText);
req.sourceText = contextualText;
req.sourceLine = lineNo;
req.sectionAnchor = sectionAnchor;
req.sectionTitle = sectionTitle;
out.push_back(req);
} else if (isProseLine(trimmed)) {
std::string contextualText = applySubsectionContext(subsectionTitle, trimmed);
if (proseBuffer.empty()) {
proseBuffer = trimmed;
proseBuffer = contextualText;
proseStartLine = lineNo;
} else {
proseBuffer += " ";
proseBuffer += trimmed;
proseBuffer += contextualText;
}
} else {
flushProse();
@@ -172,6 +190,12 @@ private:
return intakeCompactLowerAlnumSpaces(text);
}
static std::string applySubsectionContext(const std::string& subsectionTitle,
const std::string& text) {
if (subsectionTitle.empty()) return text;
return subsectionTitle + ": " + text;
}
static bool isHeading(const std::string& line) {
if (line.size() < 2) return false;
if (line[0] != '#') return false;
@@ -180,6 +204,12 @@ private:
return hashes > 0 && hashes < line.size() && line[hashes] == ' ';
}
static int headingLevel(const std::string& line) {
int hashes = 0;
while (hashes < static_cast<int>(line.size()) && line[hashes] == '#') ++hashes;
return hashes;
}
static std::string trimHeading(const std::string& headingLine) {
std::size_t i = 0;
while (i < headingLine.size() && headingLine[i] == '#') ++i;

View File

@@ -87,6 +87,16 @@ private:
return false;
}
static std::string workItemGroupKey(const std::string& text) {
if (text.size() < 2) return "";
if (text[0] != 'w') return "";
std::size_t i = 1;
while (i < text.size() && std::isdigit(static_cast<unsigned char>(text[i]))) ++i;
if (i == 1) return "";
if (i < text.size() && text[i] != '.' && text[i] != ':' && text[i] != ' ' && text[i] != '-') return "";
return text.substr(0, i);
}
static bool isPlanningMetaRequirement(const NormalizedRequirement& requirement) {
const std::string lower = lowerCopy(requirement.normalizedText);
if (requirement.anchor == "environment-projection-targets" ||
@@ -162,6 +172,25 @@ private:
return summary.empty() ? text : summary;
}
static std::string stripLeadingDisplayVerb(std::string text) {
const std::vector<std::string> prefixes = {
"implement ",
"plan ",
"verify ",
"apply constraint ",
"integrate dependency ",
"implement feature ",
"define field "
};
const std::string lower = lowerCopy(text);
for (const auto& prefix : prefixes) {
if (lower.rfind(prefix, 0) == 0 && text.size() > prefix.size()) {
return text.substr(prefix.size());
}
}
return text;
}
static std::string workstreamPrefix(NormalizedRequirementKind kind) {
if (kind == NormalizedRequirementKind::Goal) return "Plan";
if (kind == NormalizedRequirementKind::Requirement) return "Implement";
@@ -177,6 +206,8 @@ private:
? requirement.normalizedText
: requirement.sourceText;
const std::string lower = lowerCopy(sourceText);
const std::string itemKey = workItemGroupKey(lower);
if (!itemKey.empty()) return itemKey;
if (requirement.kind == NormalizedRequirementKind::Acceptance) return "verification";
if (containsAny(lower, {"smithay", "dependency", "cargo", "substrate"})) return "smithay-bootstrap";
@@ -207,7 +238,6 @@ private:
const std::vector<NormalizedRequirement>& requirements) {
DecomposedWorkstream workstream;
workstream.workstreamId = workstreamId;
workstream.title = workstreamGroupTitle(key);
std::string intent;
for (const auto& requirement : requirements) {
const std::string sourceText = requirement.sourceText.empty()
@@ -219,6 +249,28 @@ private:
}
workstream.intent = intent;
workstream.uncertaintyScore = computeUncertainty(requirements);
if (!requirements.empty() &&
key != "docs-alignment" &&
key != "verification" &&
key != "state-definitions") {
const auto& lead = requirements.front();
const std::string& textForDisplay = lead.sourceText.empty()
? lead.normalizedText
: lead.sourceText;
const std::string prefix = workstreamPrefix(lead.kind);
std::string titleSource = stripLeadingDisplayVerb(textForDisplay);
const std::string prefixLower = lowerCopy(prefix);
const std::string titleSourceLower = lowerCopy(titleSource);
if (titleSourceLower.rfind(prefixLower, 0) == 0 &&
titleSource.size() > prefixLower.size() &&
titleSource[prefixLower.size()] == ' ') {
titleSource = titleSource.substr(prefixLower.size() + 1);
}
workstream.title = prefix + " " + normalizeRequirementSummary(titleSource);
} else {
workstream.title = workstreamGroupTitle(key);
}
return workstream;
}

View File

@@ -325,6 +325,14 @@
static std::string normalizeTargetFileCandidate(const std::string& raw) {
std::string value = trimPathToken(raw);
if (value.empty()) return "";
while (!value.empty() &&
(value.back() == '.' || value.back() == ':' || value.back() == '`')) {
value.pop_back();
}
while (!value.empty() &&
(value.front() == '`' || value.front() == '"' || value.front() == '\'')) {
value.erase(value.begin());
}
if (value.rfind("./", 0) == 0) value = value.substr(2);
const std::string homeDocsPrefix = "/home/bill/Documents/";
if (value.rfind(homeDocsPrefix, 0) == 0) {
@@ -333,6 +341,9 @@
if (value.rfind("CLionProjects/whetstone_DSL/", 0) == 0) {
value = value.substr(std::string("CLionProjects/whetstone_DSL/").size());
}
if (value.find('`') != std::string::npos) return "";
if (value.find('\\') != std::string::npos) return "";
if (value.find("..") != std::string::npos) return "";
if (!looksLikePathToken(value)) return "";
return value;
}
@@ -357,12 +368,30 @@
return out;
}
static std::string extractActiveProjectRootFromText(const std::string& text) {
const std::string lower = intakeToLowerCopy(text);
if (lower.find("active project") == std::string::npos) return "";
const std::string prefix = "/home/bill/Documents/";
const std::size_t pos = text.find(prefix);
if (pos == std::string::npos) return "";
std::size_t end = pos;
while (end < text.size()) {
const char c = text[end];
if (c == ' ' || c == '\n' || c == '\t' || c == '`' || c == '"' || c == '\'' || c == ',' || c == ';' || c == ')') {
break;
}
++end;
}
return text.substr(pos, end - pos);
}
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> repoSignalCandidates;
std::string projectRoot;
bool explicitCrossProjectIntent = false;
const std::vector<std::string> knownCrossProjectRepos = {
@@ -381,6 +410,10 @@
const std::string text = requirement.normalizedText;
const std::string lower = intakeToLowerCopy(sourceText);
if (projectRoot.empty()) {
projectRoot = extractActiveProjectRootFromText(sourceText);
}
if (lower.find("cross-repo") != std::string::npos ||
lower.find("multi-project") != std::string::npos ||
lower.find("multi-repo") != std::string::npos ||
@@ -453,7 +486,8 @@
{"targetFiles", sortedUnique(files)},
{"requiredTools", sortedUnique(tools)},
{"acceptanceCommands", sortedUnique(commands)},
{"crossProjectRepos", sortedUnique(repoSignals)}
{"crossProjectRepos", sortedUnique(repoSignals)},
{"projectRoot", projectRoot}
};
}
@@ -623,12 +657,74 @@
appendUnique(&scoped, path);
}
}
if (scoped.empty()) return defaultTargetFiles;
(void)defaultTargetFiles;
if (scoped.empty()) return json::array();
json out = json::array();
for (const auto& file : sortedUnique(scoped)) out.push_back(file);
return out;
}
static bool commandStartsWith(const std::string& value, const std::string& prefix) {
return value.rfind(prefix, 0) == 0;
}
static bool commandCompatibleWithTargets(const std::string& command,
bool sawRust,
bool sawPython,
bool sawGo,
bool sawJsonOnly) {
const std::string lower = intakeToLowerCopy(trimPathToken(command));
if (lower.empty()) return false;
if (lower == "whetstone_validate_taskitem") return true;
if (commandStartsWith(lower, "cargo ")) return sawRust;
if (commandStartsWith(lower, "pytest") || commandStartsWith(lower, "python3 ")) {
return sawPython || sawJsonOnly;
}
if (commandStartsWith(lower, "go test")) return sawGo;
return !(sawRust || sawPython || sawGo || sawJsonOnly);
}
static json sanitizeAcceptanceCommands(const json& candidateCommands,
const json& targetFiles) {
bool sawRust = false;
bool sawPython = false;
bool sawGo = false;
bool sawJsonOnly = true;
if (targetFiles.is_array()) {
for (const auto& fileJson : targetFiles) {
if (!fileJson.is_string()) continue;
const std::string path = fileJson.get<std::string>();
if (endsWith(path, ".rs") || endsWith(path, "Cargo.toml")) {
sawRust = true;
sawJsonOnly = false;
} else if (endsWith(path, ".py")) {
sawPython = true;
sawJsonOnly = false;
} else if (endsWith(path, ".go") || endsWith(path, "go.mod")) {
sawGo = true;
sawJsonOnly = false;
} else if (!endsWith(path, ".json") && !endsWith(path, ".schema.json")) {
sawJsonOnly = false;
}
}
}
std::vector<std::string> filtered;
if (candidateCommands.is_array()) {
for (const auto& commandJson : candidateCommands) {
if (!commandJson.is_string()) continue;
const std::string command = trimPathToken(commandJson.get<std::string>());
if (command.empty()) continue;
if (!commandCompatibleWithTargets(command, sawRust, sawPython, sawGo, sawJsonOnly)) continue;
appendUnique(&filtered, command);
}
}
json out = json::array();
for (const auto& command : sortedUnique(filtered)) out.push_back(command);
return out;
}
static json inferWorkspaceTaskTargets(const AnnotatedTaskitem& task,
const std::string& workspaceRoot) {
namespace fs = std::filesystem;
@@ -887,19 +983,43 @@
static json inferTestFiles(const AnnotatedTaskitem& task,
const json& targetFiles,
const std::string& verificationType) {
const std::string& verificationType,
const std::string& workspaceRoot) {
namespace fs = std::filesystem;
std::vector<std::string> out;
if (verificationType != "unit" && verificationType != "integration") {
return json::array();
}
bool sawRust = false;
if (targetFiles.is_array()) {
for (const auto& fileJson : targetFiles) {
if (!fileJson.is_string()) continue;
appendDerivedTestFile(&out, fileJson.get<std::string>());
const std::string file = fileJson.get<std::string>();
if (endsWith(file, ".rs") || endsWith(file, "Cargo.toml")) sawRust = true;
if (file.find("/tests/") != std::string::npos || file.rfind("tests/", 0) == 0) {
appendUnique(&out, file);
continue;
}
appendDerivedTestFile(&out, file);
}
}
if (sawRust && !workspaceRoot.empty()) {
std::error_code ec;
const fs::path root = fs::weakly_canonical(fs::path(workspaceRoot), ec);
if (!ec) {
const fs::path preferred = root / "crates" / "whimpwm" / "tests" / "main_test.rs";
if (fs::exists(preferred, ec) && fs::is_regular_file(preferred, ec)) {
out.clear();
appendUnique(&out, "crates/whimpwm/tests/main_test.rs");
}
}
}
if (out.empty()) {
appendUnique(&out, "tests/test_" + task.base.taskId + ".py");
if (sawRust) {
appendUnique(&out, "crates/whimpwm/tests/main_test.rs");
} else {
appendUnique(&out, "tests/test_" + task.base.taskId + ".py");
}
}
json jsonOut = json::array();
for (const auto& file : sortedUnique(out)) jsonOut.push_back(file);
@@ -929,12 +1049,13 @@
const json& hints,
bool strictExecutionContract,
const std::string& workspaceRoot) {
const std::string effectiveWorkspaceRoot = hints.value("projectRoot", workspaceRoot);
json stepIds = hints.value("stepIds", json::array());
json targetFiles = hints.value("targetFiles", json::array());
json requiredTools = hints.value("requiredTools", json::array());
json acceptanceCommands = hints.value("acceptanceCommands", json::array());
std::string verificationType = inferVerificationType(task, targetFiles);
json testFiles = inferTestFiles(task, targetFiles, verificationType);
json testFiles = inferTestFiles(task, targetFiles, verificationType, effectiveWorkspaceRoot);
if (strictExecutionContract) {
if (!requiredTools.is_array() || requiredTools.empty()) {
@@ -958,11 +1079,11 @@
if (crossProjectTargets.is_array() && !crossProjectTargets.empty()) {
targetFiles = crossProjectTargets;
} else {
json taskTargets = inferWorkspaceTaskTargets(task, workspaceRoot);
json taskTargets = inferWorkspaceTaskTargets(task, effectiveWorkspaceRoot);
if (taskTargets.is_array() && !taskTargets.empty()) {
targetFiles = taskTargets;
} else {
json workspaceTargets = inferWorkspaceFallbackTargets(workspaceRoot);
json workspaceTargets = inferWorkspaceFallbackTargets(effectiveWorkspaceRoot);
if (workspaceTargets.is_array() && !workspaceTargets.empty()) {
targetFiles = workspaceTargets;
} else {
@@ -979,12 +1100,26 @@
if (!stepIds.is_array() || stepIds.empty()) {
stepIds = inferStepIds(task, targetFiles, verificationType);
}
if (!acceptanceCommands.is_array() || acceptanceCommands.empty()) {
acceptanceCommands = inferAcceptanceCommands(task, targetFiles, verificationType);
acceptanceCommands = sanitizeAcceptanceCommands(acceptanceCommands, targetFiles);
json inferredAcceptanceCommands = inferAcceptanceCommands(task, targetFiles, verificationType);
std::vector<std::string> mergedAcceptanceCommands;
if (acceptanceCommands.is_array()) {
for (const auto& commandJson : acceptanceCommands) {
if (commandJson.is_string()) appendUnique(&mergedAcceptanceCommands, commandJson.get<std::string>());
}
}
if (inferredAcceptanceCommands.is_array()) {
for (const auto& commandJson : inferredAcceptanceCommands) {
if (commandJson.is_string()) appendUnique(&mergedAcceptanceCommands, commandJson.get<std::string>());
}
}
acceptanceCommands = json::array();
for (const auto& command : sortedUnique(mergedAcceptanceCommands)) {
acceptanceCommands.push_back(command);
}
if ((!testFiles.is_array() || testFiles.empty()) &&
(verificationType == "unit" || verificationType == "integration")) {
testFiles = inferTestFiles(task, targetFiles, verificationType);
testFiles = inferTestFiles(task, targetFiles, verificationType, effectiveWorkspaceRoot);
}
}

View File

@@ -52,6 +52,8 @@
issue.find("execution contract") != std::string::npos ||
issue.find("verificationType") != std::string::npos ||
issue.find("testFiles") != std::string::npos ||
issue.find("malformed_target_path") != std::string::npos ||
issue.find("incompatible_acceptance_commands") != std::string::npos ||
issue.find("reasons are minimal") != std::string::npos) {
return "under_constrained_taskitem";
}
@@ -127,6 +129,46 @@
return sawTarget;
}
static bool hasMalformedTargetPath(const json& targetFiles) {
if (!targetFiles.is_array()) return false;
for (const auto& target : targetFiles) {
if (!target.is_string()) continue;
const std::string path = target.get<std::string>();
if (path.empty()) return true;
if (path.find('`') != std::string::npos || path.find('\\') != std::string::npos) return true;
if (path.find("..") != std::string::npos) return true;
if (path.back() == '.' || path.back() == ':' || path.back() == '/') return true;
}
return false;
}
static bool hasIncompatibleAcceptanceCommands(const json& targetFiles,
const json& acceptanceCommands) {
if (!acceptanceCommands.is_array() || acceptanceCommands.empty()) return false;
bool sawRust = false;
bool sawPython = false;
bool sawGo = false;
if (targetFiles.is_array()) {
for (const auto& target : targetFiles) {
if (!target.is_string()) continue;
const std::string path = lowerCopyValidation(target.get<std::string>());
if (path.size() >= 3 && path.compare(path.size() - 3, 3, ".rs") == 0) sawRust = true;
if (path.size() >= 3 && path.compare(path.size() - 3, 3, ".py") == 0) sawPython = true;
if (path.size() >= 3 && path.compare(path.size() - 3, 3, ".go") == 0) sawGo = true;
if (path == "cargo.toml" || (path.size() >= 10 && path.compare(path.size() - 10, 10, "cargo.toml") == 0)) sawRust = true;
if (path == "go.mod" || (path.size() >= 6 && path.compare(path.size() - 6, 6, "go.mod") == 0)) sawGo = true;
}
}
for (const auto& commandJson : acceptanceCommands) {
if (!commandJson.is_string()) continue;
const std::string command = lowerCopyValidation(commandJson.get<std::string>());
if (command.rfind("go test", 0) == 0 && !sawGo) return true;
if (command.rfind("cargo ", 0) == 0 && !sawRust) return true;
if ((command.rfind("pytest", 0) == 0 || command.rfind("python3 ", 0) == 0) && !sawPython) return true;
}
return false;
}
static std::string stableValidationDigest(const std::string& input) {
unsigned long long state = 1469598103934665603ull;
for (unsigned char c : input) {
@@ -335,6 +377,15 @@
lockIssues.push_back("cross_project_target_mismatch: generated targetFiles do not match expected project roots");
dominantGapClass = "cross_project_targeting_gap";
}
if (hasMalformedTargetPath(targetFiles)) {
lockIssues.push_back("malformed_target_path: generated targetFiles contain invalid path tokens");
dominantGapClass = "under_constrained_taskitem";
}
const json acceptanceCommands = contract.value("acceptanceCommands", json::array());
if (hasIncompatibleAcceptanceCommands(targetFiles, acceptanceCommands)) {
lockIssues.push_back("incompatible_acceptance_commands: generated commands do not match detected repo language");
dominantGapClass = "under_constrained_taskitem";
}
}
}
json recommendedTools = recommendedToolsForValidationGap(dominantGapClass);