Step 415: add SQL annotation mapping heuristics
This commit is contained in:
@@ -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)
|
||||
|
||||
246
editor/src/SqlAnnotationMapper.h
Normal file
246
editor/src/SqlAnnotationMapper.h
Normal file
@@ -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 <algorithm>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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<InferredAnnotation> inferForModule(const Module* module) const {
|
||||
std::vector<InferredAnnotation> out;
|
||||
if (!module) return out;
|
||||
|
||||
for (const auto* stmt : module->getChildren("statements")) {
|
||||
inferForNode(stmt, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<InferredAnnotation> inferForSqlText(
|
||||
const std::string& sqlText,
|
||||
const std::string& nodeId = "sql_text") const {
|
||||
std::vector<InferredAnnotation> 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<InferredAnnotation> inferCrossStackOrmMapping(
|
||||
const ClassDeclaration* ormClass,
|
||||
const TableDeclaration* table) const {
|
||||
std::vector<InferredAnnotation> 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<InferredAnnotation>& 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<InferredAnnotation>& out) const {
|
||||
if (!node) return;
|
||||
|
||||
if (node->conceptType == "DeleteStatement") {
|
||||
auto* del = static_cast<const DeleteStatement*>(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<const UpdateStatement*>(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<const SelectQuery*>(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<const WhereClause*>(where);
|
||||
if (containsPhrase(toLower(w->expression), "(select")) hints++;
|
||||
}
|
||||
auto* from = q->getChild("from");
|
||||
if (from && from->conceptType == "TableDeclaration") {
|
||||
const auto* t = static_cast<const TableDeclaration*>(from);
|
||||
if (containsPhrase(toLower(t->name), "(select")) hints++;
|
||||
}
|
||||
for (const auto* j : q->getChildren("joins")) {
|
||||
auto* jc = static_cast<const JoinClause*>(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<std::string> collectClassFields(const ClassDeclaration* cls) {
|
||||
std::unordered_set<std::string> out;
|
||||
for (const auto* f : cls->getChildren("fields")) {
|
||||
if (f->conceptType != "Variable") continue;
|
||||
out.insert(toLower(static_cast<const Variable*>(f)->name));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::unordered_set<std::string> collectTableColumns(const TableDeclaration* table) {
|
||||
std::unordered_set<std::string> out;
|
||||
for (const auto* c : table->getChildren("columns")) {
|
||||
if (c->conceptType != "ColumnDefinition") continue;
|
||||
out.insert(toLower(static_cast<const ColumnDefinition*>(c)->name));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static int countIntersection(const std::unordered_set<std::string>& a,
|
||||
const std::unordered_set<std::string>& 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<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
};
|
||||
230
editor/tests/step415_test.cpp
Normal file
230
editor/tests/step415_test.cpp
Normal file
@@ -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 <iostream>
|
||||
#include <string>
|
||||
|
||||
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<SqlAnnotationMapper::InferredAnnotation>& 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;
|
||||
}
|
||||
46
progress.md
46
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)
|
||||
|
||||
Reference in New Issue
Block a user