Step 575: add requirement normalization conflict detection

This commit is contained in:
Bill
2026-02-17 11:04:34 -07:00
parent b249920685
commit 2a10a80383
4 changed files with 410 additions and 0 deletions

View File

@@ -4054,4 +4054,13 @@ target_link_libraries(step574_test PRIVATE
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go)
add_executable(step575_test tests/step575_test.cpp)
target_include_directories(step575_test PRIVATE src)
target_link_libraries(step575_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)

View File

@@ -0,0 +1,166 @@
#pragma once
// Step 575: Requirement Normalization and Conflict Detection
#include "MarkdownSpecParser.h"
#include <algorithm>
#include <cctype>
#include <map>
#include <set>
#include <string>
#include <vector>
enum class NormalizedRequirementKind {
Goal,
Constraint,
Dependency,
Acceptance
};
struct NormalizedRequirement {
std::string requirementId;
NormalizedRequirementKind kind = NormalizedRequirementKind::Goal;
std::string normalizedText;
std::string anchor;
int sourceLine = 0;
bool ambiguous = false;
};
struct RequirementConflict {
std::string leftRequirementId;
std::string rightRequirementId;
std::string conflictType;
std::string detail;
};
struct RequirementNormalizationResult {
std::vector<NormalizedRequirement> requirements;
std::vector<RequirementConflict> conflicts;
};
class RequirementNormalizationConflictDetector {
public:
static bool normalize(const ParsedMarkdownSpec& spec,
RequirementNormalizationResult* outResult,
std::string* error) {
if (!outResult || !error) return false;
error->clear();
RequirementNormalizationResult result;
appendCategory(spec.goals, NormalizedRequirementKind::Goal, "goal", &result);
appendCategory(spec.constraints, NormalizedRequirementKind::Constraint, "constraint", &result);
appendCategory(spec.dependencies, NormalizedRequirementKind::Dependency, "dependency", &result);
appendCategory(spec.acceptanceCriteria, NormalizedRequirementKind::Acceptance, "acceptance", &result);
if (result.requirements.empty()) {
*error = "no_requirements_found";
return false;
}
result.conflicts = detectConflicts(result.requirements);
*outResult = result;
return true;
}
private:
static void appendCategory(const std::vector<ParsedRequirementItem>& items,
NormalizedRequirementKind kind,
const std::string& idPrefix,
RequirementNormalizationResult* out) {
for (std::size_t i = 0; i < items.size(); ++i) {
const auto& item = items[i];
NormalizedRequirement requirement;
requirement.requirementId = idPrefix + "-" + std::to_string(i + 1);
requirement.kind = kind;
requirement.normalizedText = normalizeText(item.text);
requirement.anchor = item.anchor;
requirement.sourceLine = item.line;
requirement.ambiguous = isAmbiguous(requirement.normalizedText);
out->requirements.push_back(requirement);
}
}
static std::string normalizeText(const std::string& text) {
std::string out;
bool lastSpace = false;
for (char c : text) {
if (std::isalnum(static_cast<unsigned char>(c))) {
out.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
lastSpace = false;
} else {
if (!lastSpace) out.push_back(' ');
lastSpace = true;
}
}
while (!out.empty() && out.front() == ' ') out.erase(out.begin());
while (!out.empty() && out.back() == ' ') out.pop_back();
return out;
}
static bool isAmbiguous(const std::string& normalizedText) {
static const char* ambiguousTokens[] = {"maybe", "possibly", "somehow", "etc", "nice"};
for (const auto* token : ambiguousTokens) {
if (normalizedText.find(token) != std::string::npos) return true;
}
return false;
}
static std::set<std::string> tokenSet(const std::string& text) {
std::set<std::string> tokens;
std::string current;
for (char c : text) {
if (std::isalnum(static_cast<unsigned char>(c))) {
current.push_back(c);
} else if (!current.empty()) {
tokens.insert(current);
current.clear();
}
}
if (!current.empty()) tokens.insert(current);
return tokens;
}
static bool hasNegation(const std::set<std::string>& tokens) {
return tokens.count("not") != 0 ||
tokens.count("never") != 0 ||
tokens.count("forbid") != 0 ||
tokens.count("forbidden") != 0 ||
tokens.count("disallow") != 0 ||
tokens.count("disable") != 0;
}
static std::vector<RequirementConflict> detectConflicts(
const std::vector<NormalizedRequirement>& requirements) {
std::vector<RequirementConflict> conflicts;
for (std::size_t i = 0; i < requirements.size(); ++i) {
for (std::size_t j = i + 1; j < requirements.size(); ++j) {
const auto& a = requirements[i];
const auto& b = requirements[j];
if (a.kind != NormalizedRequirementKind::Constraint ||
b.kind != NormalizedRequirementKind::Constraint) continue;
const auto tokensA = tokenSet(a.normalizedText);
const auto tokensB = tokenSet(b.normalizedText);
if (tokensA.empty() || tokensB.empty()) continue;
std::size_t overlap = 0;
for (const auto& token : tokensA) {
if (tokensB.count(token) != 0) ++overlap;
}
if (overlap == 0) continue;
const bool negationA = hasNegation(tokensA);
const bool negationB = hasNegation(tokensB);
if (negationA == negationB) continue;
RequirementConflict conflict;
conflict.leftRequirementId = a.requirementId;
conflict.rightRequirementId = b.requirementId;
conflict.conflictType = "constraint_contradiction";
conflict.detail = "overlap_tokens=" + std::to_string(overlap);
conflicts.push_back(conflict);
}
}
return conflicts;
}
};

View File

@@ -0,0 +1,200 @@
// Step 575: Requirement Normalization and Conflict Detection (12 tests)
#include "RequirementNormalizationConflictDetector.h"
#include <iostream>
static int passed = 0, failed = 0;
#define TEST(name) { std::cout << " " << #name << "... "; }
#define PASS() { std::cout << "PASS\n"; ++passed; }
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
static ParsedRequirementItem item(const std::string& text,
const std::string& section,
const std::string& anchor,
int line) {
ParsedRequirementItem i;
i.text = text;
i.sectionTitle = section;
i.anchor = anchor;
i.line = line;
return i;
}
void test_normalize_from_all_categories() {
TEST(normalize_from_all_categories);
ParsedMarkdownSpec spec;
spec.goals.push_back(item("Improve quality", "Goals", "goals", 3));
spec.constraints.push_back(item("Must be deterministic", "Constraints", "constraints", 6));
spec.dependencies.push_back(item("Need CMake", "Dependencies", "dependencies", 9));
spec.acceptanceCriteria.push_back(item("All tests pass", "Acceptance", "acceptance", 12));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.requirements.size() == 4, "requirement count mismatch");
PASS();
}
void test_normalized_text_is_lowercased_and_compacted() {
TEST(normalized_text_is_lowercased_and_compacted);
ParsedMarkdownSpec spec;
spec.goals.push_back(item(" Keep, HEADER-Only! ", "Goals", "goals", 4));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.requirements[0].normalizedText == "keep header only", "normalized text mismatch");
PASS();
}
void test_requirement_ids_are_stable_per_category() {
TEST(requirement_ids_are_stable_per_category);
ParsedMarkdownSpec spec;
spec.goals.push_back(item("Goal A", "Goals", "goals", 1));
spec.goals.push_back(item("Goal B", "Goals", "goals", 2));
spec.constraints.push_back(item("Constraint A", "Constraints", "constraints", 3));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.requirements[0].requirementId == "goal-1", "goal-1 expected");
CHECK(result.requirements[1].requirementId == "goal-2", "goal-2 expected");
CHECK(result.requirements[2].requirementId == "constraint-1", "constraint-1 expected");
PASS();
}
void test_preserves_anchor_and_source_line() {
TEST(preserves_anchor_and_source_line);
ParsedMarkdownSpec spec;
spec.constraints.push_back(item("Must stay small", "Constraints", "constraints", 44));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.requirements[0].anchor == "constraints", "anchor mismatch");
CHECK(result.requirements[0].sourceLine == 44, "source line mismatch");
PASS();
}
void test_ambiguous_language_is_flagged() {
TEST(ambiguous_language_is_flagged);
ParsedMarkdownSpec spec;
spec.goals.push_back(item("Maybe improve performance somehow", "Goals", "goals", 5));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.requirements[0].ambiguous, "ambiguous flag should be true");
PASS();
}
void test_clear_language_is_not_flagged_ambiguous() {
TEST(clear_language_is_not_flagged_ambiguous);
ParsedMarkdownSpec spec;
spec.goals.push_back(item("Improve parser throughput by 20 percent", "Goals", "goals", 5));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(!result.requirements[0].ambiguous, "ambiguous flag should be false");
PASS();
}
void test_detects_conflict_between_positive_and_negative_constraints() {
TEST(detects_conflict_between_positive_and_negative_constraints);
ParsedMarkdownSpec spec;
spec.constraints.push_back(item("Enable dynamic plugins", "Constraints", "constraints", 6));
spec.constraints.push_back(item("Do not enable dynamic plugins", "Constraints", "constraints", 7));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.conflicts.size() == 1, "one conflict expected");
CHECK(result.conflicts[0].conflictType == "constraint_contradiction", "conflict type mismatch");
PASS();
}
void test_no_conflict_for_unrelated_constraints() {
TEST(no_conflict_for_unrelated_constraints);
ParsedMarkdownSpec spec;
spec.constraints.push_back(item("Enable syntax highlighting", "Constraints", "constraints", 6));
spec.constraints.push_back(item("Require offline mode", "Constraints", "constraints", 7));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.conflicts.empty(), "no conflicts expected");
PASS();
}
void test_no_conflict_when_both_constraints_negative() {
TEST(no_conflict_when_both_constraints_negative);
ParsedMarkdownSpec spec;
spec.constraints.push_back(item("Do not allow network", "Constraints", "constraints", 6));
spec.constraints.push_back(item("Never allow telemetry", "Constraints", "constraints", 7));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.conflicts.empty(), "both negative constraints should not conflict");
PASS();
}
void test_conflict_records_requirement_ids() {
TEST(conflict_records_requirement_ids);
ParsedMarkdownSpec spec;
spec.constraints.push_back(item("Use cache", "Constraints", "constraints", 6));
spec.constraints.push_back(item("Do not use cache", "Constraints", "constraints", 7));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.conflicts[0].leftRequirementId == "constraint-1", "left id mismatch");
CHECK(result.conflicts[0].rightRequirementId == "constraint-2", "right id mismatch");
PASS();
}
void test_no_requirements_fails() {
TEST(no_requirements_fails);
ParsedMarkdownSpec spec;
RequirementNormalizationResult result;
std::string error;
CHECK(!RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should fail");
CHECK(error == "no_requirements_found", "wrong error");
PASS();
}
void test_non_constraint_items_do_not_generate_conflicts() {
TEST(non_constraint_items_do_not_generate_conflicts);
ParsedMarkdownSpec spec;
spec.goals.push_back(item("Enable cache", "Goals", "goals", 3));
spec.goals.push_back(item("Do not enable cache", "Goals", "goals", 4));
RequirementNormalizationResult result;
std::string error;
CHECK(RequirementNormalizationConflictDetector::normalize(spec, &result, &error), "normalize should succeed");
CHECK(result.conflicts.empty(), "goal contradiction should not be marked as constraint conflict");
PASS();
}
int main() {
std::cout << "Step 575: Requirement Normalization and Conflict Detection\n";
test_normalize_from_all_categories(); // 1
test_normalized_text_is_lowercased_and_compacted(); // 2
test_requirement_ids_are_stable_per_category(); // 3
test_preserves_anchor_and_source_line(); // 4
test_ambiguous_language_is_flagged(); // 5
test_clear_language_is_not_flagged_ambiguous(); // 6
test_detects_conflict_between_positive_and_negative_constraints();// 7
test_no_conflict_for_unrelated_constraints(); // 8
test_no_conflict_when_both_constraints_negative(); // 9
test_conflict_records_requirement_ids(); // 10
test_no_requirements_fails(); // 11
test_non_constraint_items_do_not_generate_conflicts(); // 12
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -10591,3 +10591,38 @@ extraction, categorized requirement bullets, and source-anchor traceability.
- `editor/src/MarkdownSpecParser.h` within header-size limit (`142` <= `600`)
- `editor/tests/step574_test.cpp` within test-file size guidance (`178` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
### Step 575: Requirement Normalization and Conflict Detection
**Status:** PASS (12/12 tests)
Implements requirement normalization and contradiction detection across parsed
intake requirements, including ambiguity flags and source traceability.
**Files added:**
- `editor/src/RequirementNormalizationConflictDetector.h` - normalization module:
- category-to-record normalization (goal/constraint/dependency/acceptance)
- normalized text canonicalization for consistent downstream processing
- requirement id generation and trace field preservation
- ambiguity token detection for architect review signaling
- constraint contradiction detection via overlap + negation analysis
- `editor/tests/step575_test.cpp` - 12 tests covering:
- normalization across all categories
- canonical text behavior and id stability behavior
- traceability field preservation behavior
- ambiguity flag behavior
- conflict/no-conflict scenarios and conflict metadata behavior
- empty input failure behavior
**Files modified:**
- `editor/CMakeLists.txt` - `step575_test` target
**Verification run:**
- `cmake -S editor -B editor/build-native` - PASS
- `cmake --build editor/build-native --target step575_test step574_test` - PASS
- `./editor/build-native/step575_test` - PASS (12/12)
- `./editor/build-native/step574_test` - PASS (12/12) regression coverage
**Architecture gate check:**
- `editor/src/RequirementNormalizationConflictDetector.h` within header-size limit (`166` <= `600`)
- `editor/tests/step575_test.cpp` within test-file size guidance (`200` lines)
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`