diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index c05fcdc..eb5bb9e 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -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) diff --git a/editor/src/RequirementNormalizationConflictDetector.h b/editor/src/RequirementNormalizationConflictDetector.h new file mode 100644 index 0000000..72e8903 --- /dev/null +++ b/editor/src/RequirementNormalizationConflictDetector.h @@ -0,0 +1,166 @@ +#pragma once +// Step 575: Requirement Normalization and Conflict Detection + +#include "MarkdownSpecParser.h" + +#include +#include +#include +#include +#include +#include + +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 requirements; + std::vector 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& 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(c))) { + out.push_back(static_cast(std::tolower(static_cast(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 tokenSet(const std::string& text) { + std::set tokens; + std::string current; + for (char c : text) { + if (std::isalnum(static_cast(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& 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 detectConflicts( + const std::vector& requirements) { + std::vector 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; + } +}; diff --git a/editor/tests/step575_test.cpp b/editor/tests/step575_test.cpp new file mode 100644 index 0000000..f188599 --- /dev/null +++ b/editor/tests/step575_test.cpp @@ -0,0 +1,200 @@ +// Step 575: Requirement Normalization and Conflict Detection (12 tests) + +#include "RequirementNormalizationConflictDetector.h" + +#include + +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; +} diff --git a/progress.md b/progress.md index abedb97..f70d69a 100644 --- a/progress.md +++ b/progress.md @@ -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`