diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 86dc01b..3d12f21 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -2614,4 +2614,13 @@ target_link_libraries(step414_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step415_test tests/step415_test.cpp) +target_include_directories(step415_test PRIVATE src) +target_link_libraries(step415_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/SqlAnnotationMapper.h b/editor/src/SqlAnnotationMapper.h new file mode 100644 index 0000000..bbb7bc3 --- /dev/null +++ b/editor/src/SqlAnnotationMapper.h @@ -0,0 +1,246 @@ +#pragma once + +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/ClassDeclaration.h" +#include "ast/Variable.h" +#include "ast/SqlNodes.h" +#include "ast/Annotation.h" +#include "ast/Expression.h" + +#include +#include +#include +#include +#include + +class SqlAnnotationMapper { +public: + struct InferredAnnotation { + std::string nodeId; + std::string annotationType; + std::string key; + std::string value; + std::string reason; + double confidence = 0.0; + }; + + std::vector inferForModule(const Module* module) const { + std::vector out; + if (!module) return out; + + for (const auto* stmt : module->getChildren("statements")) { + inferForNode(stmt, out); + } + return out; + } + + std::vector inferForSqlText( + const std::string& sqlText, + const std::string& nodeId = "sql_text") const { + std::vector out; + std::string lower = toLower(sqlText); + + if (containsPhrase(lower, "drop table")) { + out.push_back({nodeId, "RiskAnnotation", "level", "high", + "DROP TABLE is destructive and high-risk", 0.95}); + } + if (containsPhrase(lower, "delete from") && !containsPhrase(lower, " where ")) { + out.push_back({nodeId, "RiskAnnotation", "level", "high", + "DELETE without WHERE can remove all rows", 0.95}); + } + if (containsPhrase(lower, " limit ") || containsPhrase(lower, " top ")) { + out.push_back({nodeId, "BoundsCheckAnnotation", "enabled", "true", + "LIMIT/TOP bounds result set size", 0.85}); + } + return out; + } + + std::vector inferCrossStackOrmMapping( + const ClassDeclaration* ormClass, + const TableDeclaration* table) const { + std::vector out; + if (!ormClass || !table) return out; + + auto classFields = collectClassFields(ormClass); + auto tableColumns = collectTableColumns(table); + const int matched = countIntersection(classFields, tableColumns); + const int denom = std::max(1, std::max((int)classFields.size(), (int)tableColumns.size())); + + out.push_back({ormClass->id, "ContractAnnotation", "preconditions", + "sql table " + table->name + " exists", "ORM class depends on backing SQL table", 0.88}); + out.push_back({table->id, "ContractAnnotation", "preconditions", + "orm class " + ormClass->name + " stays schema-compatible", + "SQL table participates in ORM mapping contract", 0.84}); + + if (matched * 2 < denom) { + out.push_back({ormClass->id, "RiskAnnotation", "level", "medium", + "ORM fields diverge from SQL columns; migration risk", 0.82}); + } + + return out; + } + + void applyInferredAnnotations( + ASTNode* root, + const std::vector& inferred) const { + if (!root) return; + for (const auto& inf : inferred) { + ASTNode* node = findNodeById(root, inf.nodeId); + if (!node) continue; + Annotation* anno = createAnnotationNode(inf); + if (!anno) continue; + if (anno->id.empty()) anno->id = "sql_inferred_" + inf.nodeId + "_" + inf.annotationType; + node->addChild("annotations", anno); + } + } + +private: + void inferForNode(const ASTNode* node, std::vector& out) const { + if (!node) return; + + if (node->conceptType == "DeleteStatement") { + auto* del = static_cast(node); + if (del->getChild("where") == nullptr) { + out.push_back({node->id, "RiskAnnotation", "level", "high", + "DELETE without WHERE can remove all rows", 0.95}); + } + } else if (node->conceptType == "UpdateStatement") { + auto* up = static_cast(node); + if (up->getChild("where") == nullptr) { + out.push_back({node->id, "RiskAnnotation", "level", "medium", + "UPDATE without WHERE can overwrite all rows", 0.86}); + } + } else if (node->conceptType == "SelectQuery") { + auto* q = static_cast(node); + const int joinDepth = (int)q->getChildren("joins").size(); + const int subqueries = countSubqueryHints(q); + + out.push_back({node->id, "ComplexityAnnotation", "timeComplexity", + complexityFromQuery(joinDepth, subqueries), + "JOIN depth and subquery nesting estimated from query structure", + confidenceFromComplexity(joinDepth, subqueries)}); + + if (hasBoundsClause(q)) { + out.push_back({node->id, "BoundsCheckAnnotation", "enabled", "true", + "LIMIT/TOP bounds result set size", 0.86}); + } + + if (dependsOnIndexes(q)) { + out.push_back({node->id, "ContractAnnotation", "preconditions", + "index exists on predicate/join columns", + "Query shape indicates index dependency for expected performance", + 0.78}); + } + } + } + + static bool hasBoundsClause(const SelectQuery* q) { + return q->getChild("limit") != nullptr || q->getChild("top") != nullptr; + } + + static bool dependsOnIndexes(const SelectQuery* q) { + return !q->getChildren("joins").empty() || q->getChild("where") != nullptr; + } + + static int countSubqueryHints(const SelectQuery* q) { + int hints = 0; + auto* where = q->getChild("where"); + if (where && where->conceptType == "WhereClause") { + const auto* w = static_cast(where); + if (containsPhrase(toLower(w->expression), "(select")) hints++; + } + auto* from = q->getChild("from"); + if (from && from->conceptType == "TableDeclaration") { + const auto* t = static_cast(from); + if (containsPhrase(toLower(t->name), "(select")) hints++; + } + for (const auto* j : q->getChildren("joins")) { + auto* jc = static_cast(j); + if (containsPhrase(toLower(jc->tableName), "(select")) hints++; + } + return hints; + } + + static std::string complexityFromQuery(int joinDepth, int subqueries) { + if (joinDepth >= 3 || subqueries >= 2) return "O(n^3)"; + if (joinDepth >= 2 || subqueries >= 1) return "O(n^2)"; + if (joinDepth >= 1) return "O(n log n)"; + return "O(n)"; + } + + static double confidenceFromComplexity(int joinDepth, int subqueries) { + return std::min(0.95, 0.55 + 0.1 * joinDepth + 0.15 * subqueries); + } + + static std::unordered_set collectClassFields(const ClassDeclaration* cls) { + std::unordered_set out; + for (const auto* f : cls->getChildren("fields")) { + if (f->conceptType != "Variable") continue; + out.insert(toLower(static_cast(f)->name)); + } + return out; + } + + static std::unordered_set collectTableColumns(const TableDeclaration* table) { + std::unordered_set out; + for (const auto* c : table->getChildren("columns")) { + if (c->conceptType != "ColumnDefinition") continue; + out.insert(toLower(static_cast(c)->name)); + } + return out; + } + + static int countIntersection(const std::unordered_set& a, + const std::unordered_set& b) { + int count = 0; + for (const auto& v : a) if (b.count(v)) ++count; + return count; + } + + static Annotation* createAnnotationNode(const InferredAnnotation& inf) { + if (inf.annotationType == "RiskAnnotation") { + auto* a = new RiskAnnotation(); + a->level = inf.value; + a->reason = inf.reason; + return a; + } + if (inf.annotationType == "ComplexityAnnotation") { + auto* a = new ComplexityAnnotation(); + a->timeComplexity = inf.value; + return a; + } + if (inf.annotationType == "BoundsCheckAnnotation") { + auto* a = new BoundsCheckAnnotation(); + a->enabled = (toLower(inf.value) != "false"); + return a; + } + if (inf.annotationType == "ContractAnnotation") { + auto* a = new ContractAnnotation(); + if (inf.key == "preconditions") a->preconditions = inf.value; + else a->preconditions = inf.key + "=" + inf.value; + return a; + } + return nullptr; + } + + static ASTNode* findNodeById(ASTNode* node, const std::string& id) { + if (!node) return nullptr; + if (node->id == id) return node; + for (auto* c : node->allChildren()) { + if (auto* hit = findNodeById(c, id)) return hit; + } + return nullptr; + } + + static bool containsPhrase(const std::string& text, const std::string& phrase) { + return text.find(phrase) != std::string::npos; + } + + static std::string toLower(const std::string& s) { + std::string out = s; + std::transform(out.begin(), out.end(), out.begin(), + [](unsigned char c){ return static_cast(std::tolower(c)); }); + return out; + } +}; diff --git a/editor/tests/step415_test.cpp b/editor/tests/step415_test.cpp new file mode 100644 index 0000000..37eabb2 --- /dev/null +++ b/editor/tests/step415_test.cpp @@ -0,0 +1,230 @@ +// Step 415: SQL Annotation Mapping Tests (12 tests) + +#include "SqlAnnotationMapper.h" +#include "ast/Module.h" +#include "ast/SqlNodes.h" +#include "ast/ClassDeclaration.h" +#include "ast/Variable.h" +#include "ast/Annotation.h" + +#include +#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 bool hasInferred(const std::vector& v, + const std::string& nodeId, + const std::string& annoType, + const std::string& valueContains = "") { + for (const auto& a : v) { + if (a.nodeId == nodeId && a.annotationType == annoType) { + if (valueContains.empty()) return true; + if (a.value.find(valueContains) != std::string::npos || + a.reason.find(valueContains) != std::string::npos) return true; + } + } + return false; +} + +void test_risk_high_delete_without_where() { + TEST(risk_high_delete_without_where); + Module mod; + mod.id = "m1"; + auto* d = new DeleteStatement("del1", "users"); + mod.addChild("statements", d); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(hasInferred(inf, "del1", "RiskAnnotation", "high"), "expected high risk"); + PASS(); +} + +void test_no_high_risk_delete_with_where() { + TEST(no_high_risk_delete_with_where); + Module mod; + mod.id = "m1"; + auto* d = new DeleteStatement("del1", "users"); + d->setChild("where", new WhereClause("w1", "id = 1")); + mod.addChild("statements", d); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(!hasInferred(inf, "del1", "RiskAnnotation", "high"), "should not flag high risk"); + PASS(); +} + +void test_risk_high_drop_table_text() { + TEST(risk_high_drop_table_text); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForSqlText("DROP TABLE users;"); + CHECK(hasInferred(inf, "sql_text", "RiskAnnotation", "high"), "expected drop table high risk"); + PASS(); +} + +void test_complexity_from_join_depth() { + TEST(complexity_from_join_depth); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + q->addChild("joins", new JoinClause("j1", "LEFT", "orders")); + q->addChild("joins", new JoinClause("j2", "LEFT", "profiles")); + mod.addChild("statements", q); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(hasInferred(inf, "q1", "ComplexityAnnotation", "O(n^2)"), + "expected O(n^2) for multi-join query"); + PASS(); +} + +void test_complexity_from_subquery_hint() { + TEST(complexity_from_subquery_hint); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + q->setChild("where", new WhereClause("w1", "id IN (SELECT user_id FROM orders)")); + mod.addChild("statements", q); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(hasInferred(inf, "q1", "ComplexityAnnotation", "O(n^2)"), + "expected O(n^2) for subquery"); + PASS(); +} + +void test_boundscheck_from_limit_node() { + TEST(boundscheck_from_limit_node); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + q->setChild("limit", new IntegerLiteral("i1", 10)); + mod.addChild("statements", q); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(hasInferred(inf, "q1", "BoundsCheckAnnotation"), "expected bounds check"); + PASS(); +} + +void test_boundscheck_from_top_node() { + TEST(boundscheck_from_top_node); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + q->setChild("top", new IntegerLiteral("i1", 5)); + mod.addChild("statements", q); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(hasInferred(inf, "q1", "BoundsCheckAnnotation"), "expected bounds check"); + PASS(); +} + +void test_no_boundscheck_without_limit_or_top() { + TEST(no_boundscheck_without_limit_or_top); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + mod.addChild("statements", q); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(!hasInferred(inf, "q1", "BoundsCheckAnnotation"), "unexpected bounds check"); + PASS(); +} + +void test_contract_for_index_dependent_query() { + TEST(contract_for_index_dependent_query); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + q->setChild("where", new WhereClause("w1", "email = 'x'")); + mod.addChild("statements", q); + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + CHECK(hasInferred(inf, "q1", "ContractAnnotation", "index exists"), + "expected index precondition contract"); + PASS(); +} + +void test_cross_stack_orm_to_sql_contract() { + TEST(cross_stack_orm_to_sql_contract); + ClassDeclaration cls("c1", "User"); + cls.addChild("fields", new Variable("f1", "id")); + cls.addChild("fields", new Variable("f2", "email")); + TableDeclaration table("t1", "users"); + table.addChild("columns", new ColumnDefinition("col1", "id", "INT")); + table.addChild("columns", new ColumnDefinition("col2", "email", "TEXT")); + + SqlAnnotationMapper mapper; + auto inf = mapper.inferCrossStackOrmMapping(&cls, &table); + CHECK(hasInferred(inf, "c1", "ContractAnnotation", "sql table users exists"), + "expected class contract"); + CHECK(hasInferred(inf, "t1", "ContractAnnotation", "orm class User"), + "expected table contract"); + PASS(); +} + +void test_cross_stack_mismatch_risk() { + TEST(cross_stack_mismatch_risk); + ClassDeclaration cls("c1", "Account"); + cls.addChild("fields", new Variable("f1", "name")); + cls.addChild("fields", new Variable("f2", "createdAt")); + TableDeclaration table("t1", "accounts"); + table.addChild("columns", new ColumnDefinition("col1", "id", "INT")); + table.addChild("columns", new ColumnDefinition("col2", "email", "TEXT")); + + SqlAnnotationMapper mapper; + auto inf = mapper.inferCrossStackOrmMapping(&cls, &table); + CHECK(hasInferred(inf, "c1", "RiskAnnotation", "medium"), "expected mismatch risk"); + PASS(); +} + +void test_apply_inferred_annotations_to_ast() { + TEST(apply_inferred_annotations_to_ast); + Module mod; + mod.id = "m1"; + auto* q = new SelectQuery(); + q->id = "q1"; + q->setChild("limit", new IntegerLiteral("i1", 10)); + mod.addChild("statements", q); + + SqlAnnotationMapper mapper; + auto inf = mapper.inferForModule(&mod); + mapper.applyInferredAnnotations(&mod, inf); + + auto annos = q->getChildren("annotations"); + bool hasBounds = false; + bool hasComplex = false; + for (auto* a : annos) { + if (a->conceptType == "BoundsCheckAnnotation") hasBounds = true; + if (a->conceptType == "ComplexityAnnotation") hasComplex = true; + } + CHECK(hasBounds, "expected applied BoundsCheckAnnotation"); + CHECK(hasComplex, "expected applied ComplexityAnnotation"); + PASS(); +} + +int main() { + std::cout << "Step 415: SQL Annotation Mapping Tests\n"; + + test_risk_high_delete_without_where(); // 1 + test_no_high_risk_delete_with_where(); // 2 + test_risk_high_drop_table_text(); // 3 + test_complexity_from_join_depth(); // 4 + test_complexity_from_subquery_hint(); // 5 + test_boundscheck_from_limit_node(); // 6 + test_boundscheck_from_top_node(); // 7 + test_no_boundscheck_without_limit_or_top(); // 8 + test_contract_for_index_dependent_query(); // 9 + test_cross_stack_orm_to_sql_contract(); // 10 + test_cross_stack_mismatch_risk(); // 11 + test_apply_inferred_annotations_to_ast(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 5f04f20..66436e6 100644 --- a/progress.md +++ b/progress.md @@ -4130,6 +4130,52 @@ dialect-specific features (`AUTO_INCREMENT`, backtick identifiers, - `editor/src/MCPServer.h` (`1679` > `600`) - `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`) +### Step 415: SQL Annotation Mapping +**Status:** PASS (12/12 tests) + +Added SQL-specific semantic annotation mapping focused on risk, complexity, +bounds checks, index contracts, and cross-stack ORM<->schema consistency. + +**Files created:** +- `editor/src/SqlAnnotationMapper.h` — SQL annotation inference/mapping support: + - module-level AST inference for: + - `RiskAnnotation` on destructive SQL patterns (DELETE without WHERE, UPDATE without WHERE) + - `ComplexityAnnotation` from JOIN depth + subquery hints + - `BoundsCheckAnnotation` from `LIMIT`/`TOP` clauses + - `ContractAnnotation` precondition for index-dependent queries + - text-level SQL inference for `DROP TABLE` and bounds clause patterns + - cross-stack mapping for Python ORM class fields vs SQL table columns + - inferred-annotation application helper to attach generated annotations to AST nodes +- `editor/tests/step415_test.cpp` — 12 tests covering: + 1. high risk on DELETE without WHERE + 2. no high-risk false positive on DELETE with WHERE + 3. high risk on DROP TABLE text + 4. complexity from multi-join depth + 5. complexity from subquery nesting hint + 6. bounds check inference from LIMIT + 7. bounds check inference from TOP + 8. no bounds-check false positive without LIMIT/TOP + 9. index precondition contract inference + 10. cross-stack ORM<->SQL contract mapping + 11. cross-stack schema mismatch risk mapping + 12. inferred-annotation application to AST nodes + +**Files modified:** +- `editor/CMakeLists.txt` — `step415_test` target + +**Verification run:** +- `step415_test` — PASS (12/12) new step coverage +- `step414_test` — PASS (12/12) regression coverage +- `step413_test` — PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/SqlAnnotationMapper.h` within header-size limit (`246` <= `600`) +- `editor/tests/step415_test.cpp` within test-file size guidance (`230` lines) +- Legacy oversized headers persist: + - `editor/src/ast/Serialization.h` (`1427` > `600`) + - `editor/src/MCPServer.h` (`1679` > `600`) + - `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`) + # Roadmap Planning — Sprints 12-25+ ## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)