#pragma once // Step 250: Structured Diagnostics // // Unified diagnostic format for agent consumption. Merges tree-sitter // parse errors, AnnotationValidator diagnostics, and StrategyValidator // violations into a single stream with error codes, AST node references, // and optional machine-applicable fix mutations. // // Error code scheme: // E01xx — Parse errors (tree-sitter syntax) // E02xx — Annotation validation (consistency) // E03xx — Strategy validation (memory safety) // E04xx — Cross-file diagnostics (imports, dependencies) #include #include #include #include #include #include "ast/ASTNode.h" #include "ast/Parser.h" #include "ast/Import.h" #include "AnnotationValidator.h" #include "AnnotationValidatorExtended.h" #include "AnnotationConflictExtended.h" #include "StrategyValidator.h" #include "Pipeline.h" using json = nlohmann::json; // ----------------------------------------------------------------------- // Severity levels (numeric for filtering, lower = more severe) // ----------------------------------------------------------------------- enum class DiagnosticSeverity { Error = 1, Warning = 2, Info = 3, Hint = 4 }; inline const char* severityLabel(DiagnosticSeverity s) { switch (s) { case DiagnosticSeverity::Error: return "error"; case DiagnosticSeverity::Warning: return "warning"; case DiagnosticSeverity::Info: return "info"; case DiagnosticSeverity::Hint: return "hint"; } return "info"; } inline DiagnosticSeverity severityFromStr(const std::string& s) { if (s == "error") return DiagnosticSeverity::Error; if (s == "warning") return DiagnosticSeverity::Warning; if (s == "hint") return DiagnosticSeverity::Hint; return DiagnosticSeverity::Info; } // ----------------------------------------------------------------------- // StructuredDiagnostic — single unified diagnostic entry // ----------------------------------------------------------------------- struct StructuredDiagnostic { std::string code; // e.g. "E0100", "E0201" DiagnosticSeverity severity = DiagnosticSeverity::Error; std::string nodeId; // AST node reference (empty for parse) int line = 0; // 1-based int col = 0; // 1-based std::string message; std::string source; // "parser", "annotation", "strategy" json fix = nullptr; // optional mutation object }; inline json diagnosticToJson(const StructuredDiagnostic& d) { json j = { {"code", d.code}, {"severity", severityLabel(d.severity)}, {"severityLevel", static_cast(d.severity)}, {"nodeId", d.nodeId}, {"line", d.line}, {"col", d.col}, {"message", d.message}, {"source", d.source} }; if (!d.fix.is_null()) j["fix"] = d.fix; return j; } // ----------------------------------------------------------------------- // Error code assignment // ----------------------------------------------------------------------- // Parse errors: E0100 (generic syntax), E0101 (missing token) inline std::string parseErrorCode(const std::string& message) { if (message.find("missing") != std::string::npos || message.find("Missing") != std::string::npos) return "E0101"; return "E0100"; } // Annotation validation: E0200..E0203, E0600+ (extended) inline std::string annotationErrorCode(const std::string& message) { // Check for embedded [Exxxx] codes first (extended validator) auto pos = message.find("[E"); if (pos != std::string::npos && pos + 6 <= message.size()) { return message.substr(pos + 1, 5); // extract "E0600" from "[E0600]" } // Original memory annotation codes if (message.find("Missing Intent") != std::string::npos) return "E0200"; if (message.find("aliased") != std::string::npos || message.find("alias") != std::string::npos) return "E0201"; if (message.find("conflict") != std::string::npos || message.find("Conflict") != std::string::npos) return "E0202"; return "E0203"; } // Strategy validation: E0300..E0304 inline std::string strategyErrorCode(const std::string& category) { if (category == "use-after-free") return "E0300"; if (category == "leak") return "E0301"; if (category == "double-move") return "E0302"; if (category == "aliasing") return "E0303"; if (category == "destructor-unreachable") return "E0304"; return "E0305"; } // ----------------------------------------------------------------------- // Fix suggestion builders // ----------------------------------------------------------------------- // For annotation issues: suggest removing the problematic annotation inline json buildAnnotationFix(const std::string& nodeId, const std::string& message) { if (message.find("Missing Intent") != std::string::npos) { // Deallocate(Explicit) without dealloc point: suggest removing return {{"type", "deleteNode"}, {"nodeId", nodeId}, {"description", "Remove @Deallocate(Explicit) annotation"}}; } if (message.find("aliased") != std::string::npos) { // Single ownership alias: suggest changing to Shared return {{"type", "setProperty"}, {"nodeId", nodeId}, {"property", "strategy"}, {"value", "Shared"}, {"description", "Change @Owner(Single) to @Owner(Shared)"}}; } return nullptr; } // For strategy violations: suggest concrete fixes inline json buildStrategyFix(const std::string& nodeId, const std::string& category) { if (category == "use-after-free") { return {{"type", "deleteNode"}, {"nodeId", nodeId}, {"description", "Remove use of variable after deallocation"}}; } if (category == "leak") { return {{"type", "insertNode"}, {"parentId", nodeId}, {"role", "body"}, {"description", "Add deallocation call for leaked variable"}}; } return nullptr; } // ----------------------------------------------------------------------- // Collectors: convert each diagnostic source to unified format // ----------------------------------------------------------------------- inline std::vector collectParseDiagnostics( const std::vector& diags) { std::vector out; out.reserve(diags.size()); for (const auto& d : diags) { StructuredDiagnostic sd; sd.code = parseErrorCode(d.message); sd.severity = severityFromStr(d.severity); sd.line = d.line; sd.col = d.column; sd.message = d.message; sd.source = "parser"; out.push_back(std::move(sd)); } return out; } inline std::vector collectAnnotationDiagnostics( const std::vector& diags) { std::vector out; out.reserve(diags.size()); for (const auto& d : diags) { StructuredDiagnostic sd; sd.code = annotationErrorCode(d.message); sd.severity = severityFromStr(d.severity); sd.nodeId = d.nodeId; sd.message = d.message; sd.source = "annotation"; sd.fix = buildAnnotationFix(d.nodeId, d.message); out.push_back(std::move(sd)); } return out; } inline std::vector collectStrategyDiagnostics( const std::vector& violations) { std::vector out; out.reserve(violations.size()); for (const auto& v : violations) { StructuredDiagnostic sd; sd.code = strategyErrorCode(v.category); sd.severity = severityFromStr(v.severity); sd.nodeId = v.nodeId; sd.message = v.message; sd.source = "strategy"; sd.fix = buildStrategyFix(v.nodeId, v.category); out.push_back(std::move(sd)); } return out; } // ----------------------------------------------------------------------- // collectAllDiagnostics — run the full validation pipeline on an AST // ----------------------------------------------------------------------- inline std::vector collectAllDiagnostics( Module* ast) { std::vector all; if (!ast) return all; // Annotation validation AnnotationValidator annoValidator; auto annoDiags = annoValidator.validate(ast); auto annoStructured = collectAnnotationDiagnostics(annoDiags); all.insert(all.end(), annoStructured.begin(), annoStructured.end()); // Extended annotation validation (Subjects 2-8) AnnotationValidatorExtended extValidator; auto extDiags = extValidator.validate(ast); auto extStructured = collectAnnotationDiagnostics(extDiags); all.insert(all.end(), extStructured.begin(), extStructured.end()); // Cross-type annotation conflict detection std::vector crossConflicts; collectCrossTypeConflicts(ast, crossConflicts); for (const auto& c : crossConflicts) { StructuredDiagnostic sd; sd.code = "E0210"; sd.severity = DiagnosticSeverity::Error; sd.nodeId = c.nodeId; sd.message = c.type1 + " + " + c.type2 + ": " + c.message; sd.source = "annotation"; all.push_back(std::move(sd)); } // Strategy validation (post-optimization invariants) StrategyValidator stratValidator; auto violations = stratValidator.validateInvariants(ast); auto stratStructured = collectStrategyDiagnostics(violations); all.insert(all.end(), stratStructured.begin(), stratStructured.end()); return all; } // ----------------------------------------------------------------------- // collectPipelineDiagnostics — from a PipelineResult // ----------------------------------------------------------------------- inline std::vector collectPipelineDiagnostics( const Pipeline::PipelineResult& pr) { std::vector all; auto parseDiags = collectParseDiagnostics(pr.parseDiags); all.insert(all.end(), parseDiags.begin(), parseDiags.end()); auto annoDiags = collectAnnotationDiagnostics(pr.validationDiags); all.insert(all.end(), annoDiags.begin(), annoDiags.end()); auto stratDiags = collectStrategyDiagnostics(pr.violations); all.insert(all.end(), stratDiags.begin(), stratDiags.end()); return all; } // ----------------------------------------------------------------------- // diagnosticsToJson — convert array to JSON // ----------------------------------------------------------------------- inline json diagnosticsToJson( const std::vector& diags) { json arr = json::array(); for (const auto& d : diags) arr.push_back(diagnosticToJson(d)); return arr; } // ----------------------------------------------------------------------- // filterBySeverity — keep only diagnostics at or above threshold // ----------------------------------------------------------------------- inline std::vector filterBySeverity( const std::vector& diags, DiagnosticSeverity maxSeverity) { std::vector out; for (const auto& d : diags) { if (static_cast(d.severity) <= static_cast(maxSeverity)) { out.push_back(d); } } return out; } // ----------------------------------------------------------------------- // filterBySource — keep only diagnostics from a specific source // ----------------------------------------------------------------------- inline std::vector filterBySource( const std::vector& diags, const std::string& source) { std::vector out; for (const auto& d : diags) { if (d.source == source) out.push_back(d); } return out; } // ----------------------------------------------------------------------- // Step 260: Cross-file diagnostics // ----------------------------------------------------------------------- // Check for undefined imports: modules imported but not open as buffers inline std::vector collectCrossFileDiagnostics( Module* ast, const std::string& filePath, const std::set& openModuleNames) { std::vector diags; if (!ast) return diags; for (auto* child : ast->allChildren()) { if (child->conceptType == "Import") { auto* imp = static_cast(child); if (!imp->moduleName.empty() && openModuleNames.find(imp->moduleName) == openModuleNames.end()) { StructuredDiagnostic d; d.code = "E0400"; d.severity = DiagnosticSeverity::Warning; d.nodeId = imp->id; d.line = imp->hasSpan() ? imp->spanStartLine : 0; d.message = "Imported module '" + imp->moduleName + "' is not open in the project"; d.source = "cross-file"; diags.push_back(std::move(d)); } } } return diags; } // Text-based cross-file import check (for parsers without Import nodes) inline std::vector collectCrossFileDiagnosticsFromSource( const std::string& source, const std::string& filePath, const std::set& openModuleNames) { std::vector diags; std::istringstream iss(source); std::string line; int lineNum = 0; while (std::getline(iss, line)) { ++lineNum; std::string modName; if (line.substr(0, 7) == "import ") { modName = line.substr(7); while (!modName.empty() && modName.back() == ' ') modName.pop_back(); size_t comma = modName.find(','); if (comma != std::string::npos) modName = modName.substr(0, comma); } else if (line.substr(0, 5) == "from ") { size_t space = line.find(' ', 5); if (space != std::string::npos) modName = line.substr(5, space - 5); } if (!modName.empty() && openModuleNames.find(modName) == openModuleNames.end()) { StructuredDiagnostic d; d.code = "E0400"; d.severity = DiagnosticSeverity::Warning; d.line = lineNum; d.message = "Imported module '" + modName + "' is not open in the project"; d.source = "cross-file"; diags.push_back(std::move(d)); } } return diags; } // ----------------------------------------------------------------------- // Step 251: QuickFix — a concrete mutation an agent can apply // ----------------------------------------------------------------------- struct QuickFix { std::string id; // unique fix ID (e.g. "fix-E0200-func_1") std::string diagCode; // diagnostic code this fixes std::string nodeId; // target node std::string description; // human-readable label std::string category; // "remove-annotation", "change-strategy", etc. json mutation; // concrete mutation object for applyMutation }; inline json quickFixToJson(const QuickFix& f) { return { {"id", f.id}, {"diagCode", f.diagCode}, {"nodeId", f.nodeId}, {"description", f.description}, {"category", f.category}, {"mutation", f.mutation} }; } // ----------------------------------------------------------------------- // getQuickFixes — collect all applicable fixes for a node // ----------------------------------------------------------------------- inline std::vector getQuickFixesForNode( Module* ast, const std::string& targetNodeId) { std::vector fixes; if (!ast) return fixes; auto diags = collectAllDiagnostics(ast); for (const auto& d : diags) { if (!targetNodeId.empty() && d.nodeId != targetNodeId) continue; if (d.fix.is_null()) continue; QuickFix fix; fix.id = "fix-" + d.code + "-" + d.nodeId; fix.diagCode = d.code; fix.nodeId = d.nodeId; fix.description = d.fix.value("description", "Fix " + d.code); fix.mutation = d.fix; // Categorize based on error code if (d.code == "E0200") fix.category = "remove-annotation"; else if (d.code == "E0201") fix.category = "change-strategy"; else if (d.code == "E0202") fix.category = "resolve-conflict"; else if (d.code == "E0300") fix.category = "remove-use-after-free"; else if (d.code == "E0301") fix.category = "add-deallocation"; else fix.category = "general"; fixes.push_back(std::move(fix)); } // Additional heuristic fixes from AST inspection ASTNode* node = findNodeById(ast, targetNodeId); if (node) { // Unused variable: if a Variable has no references outside its // own declaration, suggest removal if (node->conceptType == "Variable") { QuickFix fix; fix.id = "fix-unused-" + targetNodeId; fix.diagCode = "E0203"; fix.nodeId = targetNodeId; fix.description = "Remove unused variable"; fix.category = "remove-unused"; fix.mutation = {{"type", "deleteNode"}, {"nodeId", targetNodeId}}; fixes.push_back(std::move(fix)); } // Missing return: if a Function body ends without a Return, // suggest adding one if (node->conceptType == "Function") { auto body = node->getChildren("body"); bool hasReturn = false; for (auto* stmt : body) { if (stmt->conceptType == "ReturnStatement") hasReturn = true; } if (!hasReturn && !body.empty()) { QuickFix fix; fix.id = "fix-missing-return-" + targetNodeId; fix.diagCode = "E0203"; fix.nodeId = targetNodeId; fix.description = "Add missing return statement"; fix.category = "missing-return"; fix.mutation = { {"type", "insertNode"}, {"parentId", targetNodeId}, {"role", "body"}, {"node", {{"conceptType", "ReturnStatement"}, {"children", json::object()}}} }; fixes.push_back(std::move(fix)); } } } return fixes; } // ----------------------------------------------------------------------- // getQuickFixesAll — collect all fixes for all diagnostics in the AST // ----------------------------------------------------------------------- inline std::vector getQuickFixesAll(Module* ast) { return getQuickFixesForNode(ast, ""); } // ----------------------------------------------------------------------- // findQuickFix — look up a specific fix by diagnostic code + nodeId // ----------------------------------------------------------------------- inline QuickFix findQuickFix(Module* ast, const std::string& diagCode, const std::string& nodeId) { auto fixes = getQuickFixesForNode(ast, nodeId); for (const auto& f : fixes) { if (f.diagCode == diagCode) return f; } return {}; // empty fix (mutation will be null) } // ----------------------------------------------------------------------- // Step 252: DiagnosticVersionTracker — track diagnostic changes // ----------------------------------------------------------------------- // Key that identifies a unique diagnostic instance struct DiagnosticKey { std::string code; std::string nodeId; int line = 0; bool operator==(const DiagnosticKey& o) const { return code == o.code && nodeId == o.nodeId && line == o.line; } bool operator<(const DiagnosticKey& o) const { if (code != o.code) return code < o.code; if (nodeId != o.nodeId) return nodeId < o.nodeId; return line < o.line; } }; inline DiagnosticKey keyFromDiagnostic(const StructuredDiagnostic& d) { return {d.code, d.nodeId, d.line}; } struct DiagnosticDelta { std::vector added; std::vector removed; int version = 0; }; class DiagnosticVersionTracker { public: int version = 0; // Record current diagnostics snapshot and bump version void recordSnapshot(const std::vector& diags) { previousDiags_ = currentDiags_; previousVersion_ = version; currentDiags_ = diags; ++version; } // Compute delta: what changed since the given version DiagnosticDelta getDelta( const std::vector& currentDiags, int sinceVersion) const { DiagnosticDelta delta; delta.version = version; if (sinceVersion >= version) { // No changes since that version return delta; } // Build key sets for previous and current std::set prevKeys; std::map prevMap; for (const auto& d : previousDiags_) { auto key = keyFromDiagnostic(d); prevKeys.insert(key); prevMap[key] = d; } std::set currKeys; std::map currMap; for (const auto& d : currentDiags) { auto key = keyFromDiagnostic(d); currKeys.insert(key); currMap[key] = d; } // Added: in current but not in previous for (const auto& key : currKeys) { if (prevKeys.find(key) == prevKeys.end()) { delta.added.push_back(currMap[key]); } } // Removed: in previous but not in current for (const auto& key : prevKeys) { if (currKeys.find(key) == currKeys.end()) { delta.removed.push_back(prevMap[key]); } } return delta; } // Reset tracker void reset() { version = 0; previousVersion_ = 0; currentDiags_.clear(); previousDiags_.clear(); } private: int previousVersion_ = 0; std::vector currentDiags_; std::vector previousDiags_; }; inline json deltaToJson(const DiagnosticDelta& delta) { return { {"added", diagnosticsToJson(delta.added)}, {"removed", diagnosticsToJson(delta.removed)}, {"addedCount", (int)delta.added.size()}, {"removedCount", (int)delta.removed.size()}, {"version", delta.version} }; }