Complete Step 483: convention extractor validator
This commit is contained in:
@@ -3226,4 +3226,13 @@ target_link_libraries(step482_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step483_test tests/step483_test.cpp)
|
||||
target_include_directories(step483_test PRIVATE src)
|
||||
target_link_libraries(step483_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)
|
||||
|
||||
223
editor/src/ProjectConventionAnalyzer.h
Normal file
223
editor/src/ProjectConventionAnalyzer.h
Normal file
@@ -0,0 +1,223 @@
|
||||
#pragma once
|
||||
|
||||
// Step 483: Convention Extractor + Validator
|
||||
// Extracts machine-readable project conventions and validates step outputs.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ProjectConventions {
|
||||
std::string testHarnessPattern = "TEST/CHECK macros";
|
||||
std::string assertionStyle = "CHECK/assert";
|
||||
std::map<std::string, int> sizeLimits; // header, main.cpp, function
|
||||
std::string buildSystem = "cmake";
|
||||
std::string buildTargetPattern = "add_executable(stepNNN_test tests/stepNNN_test.cpp)";
|
||||
bool headerOnly = true;
|
||||
std::string classNaming = "PascalCase";
|
||||
std::string methodNaming = "camelCase";
|
||||
std::string fileNaming = "PascalCase.h";
|
||||
};
|
||||
|
||||
struct ConventionViolation {
|
||||
std::string code;
|
||||
std::string filePath;
|
||||
std::string message;
|
||||
std::string severity = "warning";
|
||||
};
|
||||
|
||||
struct ConventionValidationReport {
|
||||
std::vector<ConventionViolation> violations;
|
||||
|
||||
bool hasViolation(const std::string& code) const {
|
||||
for (const auto& v : violations) {
|
||||
if (v.code == code) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
struct SourceFileDraft {
|
||||
std::string path;
|
||||
std::string content;
|
||||
};
|
||||
|
||||
class ProjectConventionAnalyzer {
|
||||
public:
|
||||
static ProjectConventions extract(const std::string& repoRoot) {
|
||||
ProjectConventions out;
|
||||
|
||||
const std::string architectureText = readAll(findFirstExisting(repoRoot, {
|
||||
"ARCHITECTURE.md", "architecture.md"
|
||||
}));
|
||||
if (!architectureText.empty()) {
|
||||
maybeSetLimit(architectureText, "Header file", "header", out.sizeLimits);
|
||||
maybeSetLimit(architectureText, "main.cpp", "main.cpp", out.sizeLimits);
|
||||
maybeSetLimit(architectureText, "Single function", "function", out.sizeLimits);
|
||||
if (contains(architectureText, "Header-Only")) out.headerOnly = true;
|
||||
if (contains(architectureText, "PascalCase")) out.classNaming = "PascalCase";
|
||||
if (contains(architectureText, "camelCase")) out.methodNaming = "camelCase";
|
||||
}
|
||||
|
||||
const std::string cmakeText = readAll(repoRoot + "/editor/CMakeLists.txt");
|
||||
if (contains(cmakeText, "add_executable(step")) {
|
||||
out.buildSystem = "cmake";
|
||||
out.buildTargetPattern = "add_executable(stepNNN_test tests/stepNNN_test.cpp)";
|
||||
}
|
||||
|
||||
const std::string sampleTest = readFirstMatching(repoRoot + "/editor/tests", "step*_test.cpp");
|
||||
if (contains(sampleTest, "#define TEST(") && contains(sampleTest, "#define CHECK(")) {
|
||||
out.testHarnessPattern = "TEST/PASS/FAIL/CHECK macro harness";
|
||||
}
|
||||
if (contains(sampleTest, "assert(")) {
|
||||
out.assertionStyle = "CHECK/assert";
|
||||
}
|
||||
|
||||
if (!out.sizeLimits.count("header")) out.sizeLimits["header"] = 600;
|
||||
if (!out.sizeLimits.count("main.cpp")) out.sizeLimits["main.cpp"] = 1500;
|
||||
if (!out.sizeLimits.count("function")) out.sizeLimits["function"] = 80;
|
||||
return out;
|
||||
}
|
||||
|
||||
static ConventionValidationReport validate(const ProjectConventions& c,
|
||||
const std::vector<SourceFileDraft>& files) {
|
||||
ConventionValidationReport out;
|
||||
for (const auto& f : files) {
|
||||
const int lineCount = countLines(f.content);
|
||||
if (isHeader(f.path) && lineCount > getOrDefault(c.sizeLimits, "header", 600)) {
|
||||
out.violations.push_back({"HEADER_TOO_LARGE", f.path,
|
||||
"Header exceeds size limit", "warning"});
|
||||
}
|
||||
if (isMainCpp(f.path) && lineCount > getOrDefault(c.sizeLimits, "main.cpp", 1500)) {
|
||||
out.violations.push_back({"MAIN_TOO_LARGE", f.path,
|
||||
"main.cpp exceeds size limit", "warning"});
|
||||
}
|
||||
if (isTestFile(f.path)) {
|
||||
const bool hasCheck = contains(f.content, "CHECK(") || contains(f.content, "assert(");
|
||||
if (!hasCheck) {
|
||||
out.violations.push_back({"TEST_ASSERTION_MISSING", f.path,
|
||||
"Test file missing CHECK/assert assertions", "warning"});
|
||||
}
|
||||
}
|
||||
if (isHeader(f.path)) {
|
||||
validateClassNaming(f, out);
|
||||
}
|
||||
if (f.path.find("CMakeLists.txt") != std::string::npos &&
|
||||
f.content.find("add_executable(step") == std::string::npos) {
|
||||
out.violations.push_back({"BUILD_TARGET_MISSING", f.path,
|
||||
"No step test target found in CMake snippet", "warning"});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static bool contains(const std::string& text, const std::string& needle) {
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
static int countLines(const std::string& text) {
|
||||
if (text.empty()) return 0;
|
||||
int lines = 1;
|
||||
for (char c : text) if (c == '\n') ++lines;
|
||||
return lines;
|
||||
}
|
||||
|
||||
static int getOrDefault(const std::map<std::string, int>& m,
|
||||
const std::string& key, int fallback) {
|
||||
auto it = m.find(key);
|
||||
if (it == m.end()) return fallback;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static bool isHeader(const std::string& path) {
|
||||
return endsWith(path, ".h") || endsWith(path, ".hpp");
|
||||
}
|
||||
|
||||
static bool isMainCpp(const std::string& path) {
|
||||
return endsWith(path, "main.cpp");
|
||||
}
|
||||
|
||||
static bool isTestFile(const std::string& path) {
|
||||
return path.find("/tests/") != std::string::npos && endsWith(path, "_test.cpp");
|
||||
}
|
||||
|
||||
static bool endsWith(const std::string& value, const std::string& suffix) {
|
||||
return value.size() >= suffix.size() &&
|
||||
value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0;
|
||||
}
|
||||
|
||||
static std::string readAll(const std::string& path) {
|
||||
if (path.empty()) return "";
|
||||
std::ifstream in(path);
|
||||
if (!in.is_open()) return "";
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static std::string findFirstExisting(const std::string& root,
|
||||
std::initializer_list<const char*> candidates) {
|
||||
for (const auto& c : candidates) {
|
||||
const std::string p = root + "/" + c;
|
||||
if (std::filesystem::exists(p)) return p;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static std::string readFirstMatching(const std::string& dir, const std::string& patternGlob) {
|
||||
(void)patternGlob; // Simplified: read first step test file found.
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(dir, ec)) return "";
|
||||
for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
|
||||
if (ec) break;
|
||||
if (!entry.is_regular_file()) continue;
|
||||
auto name = entry.path().filename().string();
|
||||
if (name.find("step") == 0 && name.find("_test.cpp") != std::string::npos) {
|
||||
return readAll(entry.path().string());
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void maybeSetLimit(const std::string& architectureText,
|
||||
const std::string& key,
|
||||
const std::string& outKey,
|
||||
std::map<std::string, int>& limits) {
|
||||
std::regex row("\\|\\s*" + key + "\\s*\\|\\s*([0-9]+)\\s*\\|");
|
||||
std::smatch m;
|
||||
if (std::regex_search(architectureText, m, row) && m.size() > 1) {
|
||||
limits[outKey] = std::stoi(m[1].str());
|
||||
}
|
||||
}
|
||||
|
||||
static bool isPascalCase(const std::string& name) {
|
||||
if (name.empty()) return false;
|
||||
if (!std::isupper(static_cast<unsigned char>(name[0]))) return false;
|
||||
for (char c : name) {
|
||||
unsigned char uc = static_cast<unsigned char>(c);
|
||||
if (!std::isalnum(uc) && c != '_') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void validateClassNaming(const SourceFileDraft& f,
|
||||
ConventionValidationReport& out) {
|
||||
std::regex classRe("\\b(class|struct)\\s+([A-Za-z_][A-Za-z0-9_]*)");
|
||||
auto begin = std::sregex_iterator(f.content.begin(), f.content.end(), classRe);
|
||||
auto end = std::sregex_iterator();
|
||||
for (auto it = begin; it != end; ++it) {
|
||||
std::string cls = (*it)[2].str();
|
||||
if (!isPascalCase(cls)) {
|
||||
out.violations.push_back({"CLASS_NAMING_MISMATCH", f.path,
|
||||
"Class/struct should use PascalCase: " + cls, "warning"});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
146
editor/tests/step483_test.cpp
Normal file
146
editor/tests/step483_test.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
// Step 483: Convention Extractor + Validator Tests (unit, negative, regression)
|
||||
|
||||
#include "ProjectConventionAnalyzer.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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 {}
|
||||
|
||||
void test_extract_reads_size_limits_from_architecture_doc() {
|
||||
TEST(extract_reads_size_limits_from_architecture_doc);
|
||||
auto c = ProjectConventionAnalyzer::extract(".");
|
||||
CHECK(c.sizeLimits["header"] == 600, "header limit mismatch");
|
||||
CHECK(c.sizeLimits["main.cpp"] == 1500, "main limit mismatch");
|
||||
CHECK(c.sizeLimits["function"] == 80, "function limit mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_extract_detects_cmake_target_pattern() {
|
||||
TEST(extract_detects_cmake_target_pattern);
|
||||
auto c = ProjectConventionAnalyzer::extract(".");
|
||||
CHECK(c.buildSystem == "cmake", "expected cmake build system");
|
||||
CHECK(c.buildTargetPattern.find("add_executable(step") != std::string::npos,
|
||||
"missing build target pattern");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_extract_detects_test_harness_macros() {
|
||||
TEST(extract_detects_test_harness_macros);
|
||||
auto c = ProjectConventionAnalyzer::extract(".");
|
||||
CHECK(c.testHarnessPattern.find("TEST") != std::string::npos, "missing TEST harness detection");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_flags_header_line_limit_violation_negative() {
|
||||
TEST(validate_flags_header_line_limit_violation_negative);
|
||||
ProjectConventions c;
|
||||
c.sizeLimits["header"] = 3;
|
||||
SourceFileDraft bigHeader{
|
||||
"editor/src/TooBig.h",
|
||||
"#pragma once\nline1\nline2\nline3\nline4\n"
|
||||
};
|
||||
auto r = ProjectConventionAnalyzer::validate(c, {bigHeader});
|
||||
CHECK(r.hasViolation("HEADER_TOO_LARGE"), "expected HEADER_TOO_LARGE violation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_flags_missing_test_assertions_negative() {
|
||||
TEST(validate_flags_missing_test_assertions_negative);
|
||||
ProjectConventions c;
|
||||
SourceFileDraft testFile{
|
||||
"editor/tests/step999_test.cpp",
|
||||
"int main(){ return 0; }\n"
|
||||
};
|
||||
auto r = ProjectConventionAnalyzer::validate(c, {testFile});
|
||||
CHECK(r.hasViolation("TEST_ASSERTION_MISSING"), "expected missing assertion violation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_flags_class_naming_mismatch_negative() {
|
||||
TEST(validate_flags_class_naming_mismatch_negative);
|
||||
ProjectConventions c;
|
||||
SourceFileDraft h{
|
||||
"editor/src/bad_name.h",
|
||||
"struct bad_name { int x; };"
|
||||
};
|
||||
auto r = ProjectConventionAnalyzer::validate(c, {h});
|
||||
CHECK(r.hasViolation("CLASS_NAMING_MISMATCH"), "expected class naming violation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_flags_missing_cmake_step_target_negative() {
|
||||
TEST(validate_flags_missing_cmake_step_target_negative);
|
||||
ProjectConventions c;
|
||||
SourceFileDraft cm{
|
||||
"editor/CMakeLists.txt",
|
||||
"add_executable(other tests/other.cpp)\n"
|
||||
};
|
||||
auto r = ProjectConventionAnalyzer::validate(c, {cm});
|
||||
CHECK(r.hasViolation("BUILD_TARGET_MISSING"), "expected build target violation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_validate_accepts_well_formed_step_files() {
|
||||
TEST(validate_accepts_well_formed_step_files);
|
||||
ProjectConventions c;
|
||||
c.sizeLimits["header"] = 600;
|
||||
SourceFileDraft header{
|
||||
"editor/src/GoodName.h",
|
||||
"struct GoodName { int x; };"
|
||||
};
|
||||
SourceFileDraft testFile{
|
||||
"editor/tests/step999_test.cpp",
|
||||
"#define CHECK(a,b) do{}while(0)\nint main(){ CHECK(true, \"ok\"); return 0; }\n"
|
||||
};
|
||||
SourceFileDraft cm{
|
||||
"editor/CMakeLists.txt",
|
||||
"add_executable(step999_test tests/step999_test.cpp)\n"
|
||||
};
|
||||
auto r = ProjectConventionAnalyzer::validate(c, {header, testFile, cm});
|
||||
CHECK(r.violations.empty(), "expected no violations");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_regression_step482_files_validate_without_warnings() {
|
||||
TEST(regression_step482_files_validate_without_warnings);
|
||||
auto c = ProjectConventionAnalyzer::extract(".");
|
||||
std::ifstream hIn("editor/src/StepSpecExpander.h");
|
||||
std::ifstream tIn("editor/tests/step482_test.cpp");
|
||||
std::ostringstream hs, ts;
|
||||
hs << hIn.rdbuf();
|
||||
ts << tIn.rdbuf();
|
||||
std::vector<SourceFileDraft> files{
|
||||
{"editor/src/StepSpecExpander.h", hs.str()},
|
||||
{"editor/tests/step482_test.cpp", ts.str()}
|
||||
};
|
||||
auto r = ProjectConventionAnalyzer::validate(c, files);
|
||||
CHECK(!r.hasViolation("HEADER_TOO_LARGE"), "unexpected header limit violation");
|
||||
CHECK(!r.hasViolation("TEST_ASSERTION_MISSING"), "unexpected test assertion violation");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 483: Convention Extractor + Validator Tests\n";
|
||||
|
||||
test_extract_reads_size_limits_from_architecture_doc(); // 1
|
||||
test_extract_detects_cmake_target_pattern(); // 2
|
||||
test_extract_detects_test_harness_macros(); // 3
|
||||
test_validate_flags_header_line_limit_violation_negative(); // 4
|
||||
test_validate_flags_missing_test_assertions_negative(); // 5
|
||||
test_validate_flags_class_naming_mismatch_negative(); // 6
|
||||
test_validate_flags_missing_cmake_step_target_negative(); // 7
|
||||
test_validate_accepts_well_formed_step_files(); // 8
|
||||
test_regression_step482_files_validate_without_warnings(); // 9
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
37
progress.md
37
progress.md
@@ -6914,3 +6914,40 @@ test plan stubs, header skeleton outline, and build-system entry.
|
||||
- `editor/src/StepSpecExpander.h` within header-size limit (`267` <= `600`)
|
||||
- `editor/tests/step482_test.cpp` within test-file size guidance (`149` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 483: Convention Extractor + Validator
|
||||
**Status:** PASS (9/9 tests)
|
||||
|
||||
Adds machine-readable project convention extraction and warning-level
|
||||
validation for generated step artifacts, enabling architect review of
|
||||
worker output conformance.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/ProjectConventionAnalyzer.h` — extractor + validator:
|
||||
- `ProjectConventions`, `SourceFileDraft`
|
||||
- `ConventionViolation`, `ConventionValidationReport`
|
||||
- `extract(repoRoot)`:
|
||||
- parses architecture limits (`header`, `main.cpp`, `function`)
|
||||
- detects build target pattern from `editor/CMakeLists.txt`
|
||||
- detects test harness conventions from step test files
|
||||
- `validate(conventions, files)` warning checks:
|
||||
- header/main line-limit violations
|
||||
- missing test assertions (`CHECK/assert`)
|
||||
- class/struct naming mismatch (`PascalCase`)
|
||||
- missing step test target in CMake snippet
|
||||
- `editor/tests/step483_test.cpp` — 9 tests (unit/negative/regression):
|
||||
- extraction correctness from repo docs/build files
|
||||
- negative cases for size, assertions, naming, and build entry violations
|
||||
- valid-file acceptance path
|
||||
- regression validation against Step 482 outputs
|
||||
- `editor/CMakeLists.txt` — `step483_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake --build editor/build-native --target step483_test step482_test` — PASS
|
||||
- `./editor/build-native/step483_test` — PASS (9/9)
|
||||
- `./editor/build-native/step482_test` — PASS (9/9) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/ProjectConventionAnalyzer.h` within header-size limit (`223` <= `600`)
|
||||
- `editor/tests/step483_test.cpp` within test-file size guidance (`146` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user