SQL 정적 분석과 PPT·HTML 품질 기준을 강화하고 언어 fallback을 고도화한다
- SQL 전용 정적 분석 계층을 추가해 PostgreSQL/MySQL/SQL Server/SQLite/Oracle 방언 추정, statement kind 분류, object 추출, destructive DDL·broad DML·transaction boundary 위험 감지를 지원한다 - CodeLanguageCatalog의 SQL manifest/build/test/lint 힌트와 fallback summary를 SQL 분석 결과 중심으로 보강해 no-LSP 환경에서도 dialect·risk·next checks를 직접 안내한다 - DeckPlanningService가 구조화된 content 슬라이드를 kpi_dashboard/comparison/roadmap/chart로 자동 승격하고 DeckQualityReviewService·DeckRepairGuideService가 KPI 근거, verdict, owner/timeline, takeaway 부족을 별도 진단·보정한다 - HtmlSkill에 kpi_panel 섹션을 추가하고 ArtifactQualityReviewService·ArtifactRepairGuideService가 board/strategy 문서의 KPI·evidence·decision 연결 부족을 더 정확히 감지하도록 확장한다 - README.md, docs/DEVELOPMENT.md, docs/NEXT_ROADMAP.md에 2026-04-15 11:17 (KST) 기준 작업 이력과 검증 결과를 반영했다 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_sql_doc_batch\ -p:IntermediateOutputPath=obj\verify_sql_doc_batch\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter SqlDialectDetectorTests|SqlAnalysisServiceTests|CodeLanguageCatalogTests|DeckPlanningServiceTests|DeckQualityReviewServiceTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|HtmlSkillConsultingSectionsTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests -p:OutputPath=bin\verify_sql_doc_batch_tests\ -p:IntermediateOutputPath=obj\verify_sql_doc_batch_tests\ : 통과 47
This commit is contained in:
@@ -15,6 +15,7 @@ public class ArtifactQualityReviewServiceTests
|
||||
<h2>Current State</h2><p>Current state detail.</p><p>Evidence paragraph.</p>
|
||||
<div class="callout-info">Important message</div>
|
||||
<table><tr><th>Metric</th><th>Value</th></tr><tr><td>NPS</td><td>61</td></tr></table>
|
||||
<div class="kpi-panel"></div>
|
||||
<div class="comparison-grid"></div>
|
||||
<div class="roadmap-block"></div>
|
||||
<div class="decision-summary"></div>
|
||||
@@ -138,6 +139,27 @@ public class ArtifactQualityReviewServiceTests
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("Strategy brief should include explicit decisions", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReviewHtml_ShouldRecommendKpiSupport_ForBoardAndStrategyPanels()
|
||||
{
|
||||
var html =
|
||||
"""
|
||||
<h2>Executive Summary</h2><p>Summary text.</p><p>Supporting paragraph.</p>
|
||||
<section class="board-report-panel"></section>
|
||||
<section class="strategy-brief-panel"></section>
|
||||
<div class="decision-summary"></div>
|
||||
<div class="comparison-grid"></div>
|
||||
<div class="roadmap-block"></div>
|
||||
<h2>Recommendation</h2><p>Recommendation text.</p><p>More detail.</p>
|
||||
<h2>Appendix</h2><p>Reference detail.</p><p>More detail.</p>
|
||||
""";
|
||||
|
||||
var review = ArtifactQualityReviewService.ReviewHtml("KPI Gap", html, hasCover: true, hasTableOfContents: true, printReady: true);
|
||||
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("KPI panel or metric strip", StringComparison.OrdinalIgnoreCase));
|
||||
review.Issues.Should().Contain(issue => issue.Message.Contains("KPI or evidence support blocks", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReviewStructuredDocument_ShouldRecommendEvidenceTableAndCallouts_ForLongBusinessDoc()
|
||||
{
|
||||
|
||||
@@ -36,12 +36,14 @@ public class ArtifactRepairGuideServiceTests
|
||||
["Includes print-ready CSS"],
|
||||
[
|
||||
new ArtifactReviewIssue("Board-ready report should include a decision summary block.", ArtifactReviewSeverity.Warning),
|
||||
new ArtifactReviewIssue("Board-ready report would be stronger with a KPI panel or metric strip.", ArtifactReviewSeverity.Info),
|
||||
new ArtifactReviewIssue("Strategy brief should include explicit decisions or a decision summary block.", ArtifactReviewSeverity.Warning)
|
||||
]);
|
||||
|
||||
var guide = ArtifactRepairGuideService.BuildGuide(review);
|
||||
|
||||
guide.Should().Contain("decision summary");
|
||||
guide.Should().Contain("KPI");
|
||||
guide.Should().Contain("strategy brief");
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ public class CodeLanguageCatalogTests
|
||||
CodeLanguageCatalog.GetBuildHints("go").Should().Contain("go build ./...");
|
||||
CodeLanguageCatalog.GetTestHints("kotlin").Should().Contain("./gradlew test");
|
||||
CodeLanguageCatalog.GetLintHints("javascript").Should().Contain("eslint .");
|
||||
CodeLanguageCatalog.GetManifestHints("sql").Should().Contain("schema.sql");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -92,4 +93,15 @@ public class CodeLanguageCatalogTests
|
||||
summaries.Should().Contain(summary => summary.StartsWith("Go:"));
|
||||
summaries.Should().Contain(summary => summary.StartsWith("Rust:"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildWorkflowSummary_ShouldExposeSqlSpecificWorkflowHints()
|
||||
{
|
||||
var summary = CodeLanguageCatalog.BuildWorkflowSummary("sql");
|
||||
|
||||
summary.Should().Contain("SQL");
|
||||
summary.Should().Contain("schema.sql");
|
||||
summary.Should().Contain("disposable database");
|
||||
summary.Should().Contain("dialect");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,4 +51,51 @@ public class DeckPlanningServiceTests
|
||||
result.AutoRepairCount.Should().BeGreaterThan(0);
|
||||
result.Storyline.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prepare_ShouldPromoteStructuredContentSlides_ToBetterLayouts()
|
||||
{
|
||||
using var args = JsonDocument.Parse(
|
||||
"""
|
||||
{
|
||||
"title": "Operating Review",
|
||||
"slides": [
|
||||
{
|
||||
"layout": "content",
|
||||
"title": "KPI Snapshot",
|
||||
"kpis": [
|
||||
{ "label": "Margin", "value": "+4.2pt" },
|
||||
{ "label": "Lead Time", "value": "-18%" },
|
||||
{ "label": "Retention", "value": "92%" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "content",
|
||||
"title": "Options",
|
||||
"options": [
|
||||
{ "name": "Pilot", "pros": "Low risk", "cons": "Slow", "verdict": "Too cautious" },
|
||||
{ "name": "Phased", "pros": "Balanced", "cons": "Needs PMO", "verdict": "Recommended" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "content",
|
||||
"title": "Execution",
|
||||
"phases": [
|
||||
{ "title": "Design", "detail": "Scope and governance" },
|
||||
{ "title": "Launch", "detail": "Rollout wave" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
using var result = DeckPlanningService.Prepare(args.RootElement, args.RootElement.GetProperty("slides"), "Operating Review");
|
||||
var layouts = result.Slides.EnumerateArray()
|
||||
.Select(slide => slide.GetProperty("layout").GetString())
|
||||
.ToList();
|
||||
|
||||
layouts.Should().Contain("kpi_dashboard");
|
||||
layouts.Should().Contain("comparison");
|
||||
layouts.Should().Contain("roadmap");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,16 @@ public class HtmlSkillConsultingSectionsTests
|
||||
"rationale": "Retention and margin improved in the pilot region.",
|
||||
"actions": ["Approve budget", "Launch phase 2"]
|
||||
},
|
||||
{
|
||||
"type": "kpi_panel",
|
||||
"title": "KPI Panel",
|
||||
"headline": "Pilot economics remain above threshold",
|
||||
"items": [
|
||||
{ "label": "Margin", "value": "+4.2pt", "trend": "up", "note": "pilot" },
|
||||
{ "label": "Lead Time", "value": "-18%", "trend": "down", "note": "handoff" }
|
||||
],
|
||||
"takeaway": "The pilot shows both service and margin improvement."
|
||||
},
|
||||
{
|
||||
"type": "evidence_cards",
|
||||
"title": "Evidence",
|
||||
@@ -101,6 +111,7 @@ public class HtmlSkillConsultingSectionsTests
|
||||
html.Should().Contain("roadmap-block");
|
||||
html.Should().Contain("matrix-grid");
|
||||
html.Should().Contain("decision-summary");
|
||||
html.Should().Contain("kpi-panel");
|
||||
html.Should().Contain("evidence-cards");
|
||||
html.Should().Contain("board-report-panel");
|
||||
html.Should().Contain("strategy-brief-panel");
|
||||
|
||||
58
src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs
Normal file
58
src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System.IO;
|
||||
using AxCopilot.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class SqlAnalysisServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Analyze_ShouldSurfaceStatementKindsObjectsAndRisks()
|
||||
{
|
||||
const string sql =
|
||||
"""
|
||||
BEGIN TRANSACTION;
|
||||
CREATE TABLE orders (id SERIAL PRIMARY KEY, customer_id INT);
|
||||
UPDATE orders SET customer_id = 0;
|
||||
DROP TABLE legacy_orders;
|
||||
COMMIT;
|
||||
""";
|
||||
|
||||
var report = SqlAnalysisService.Analyze(sql, "20260415_orders_migration.sql");
|
||||
|
||||
report.Dialect.Should().Be("PostgreSQL");
|
||||
report.StatementKinds.Should().Contain("CREATE TABLE");
|
||||
report.StatementKinds.Should().Contain("UPDATE");
|
||||
report.StatementKinds.Should().Contain("DROP TABLE");
|
||||
report.Objects.Should().Contain("orders");
|
||||
report.Objects.Should().Contain("legacy_orders");
|
||||
report.Risks.Should().Contain(risk => risk.Contains("UPDATE statement without WHERE", StringComparison.OrdinalIgnoreCase));
|
||||
report.Risks.Should().Contain(risk => risk.Contains("Destructive DROP", StringComparison.OrdinalIgnoreCase));
|
||||
report.SuggestedChecks.Should().Contain(check => check.Contains("rollback", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFallbackSummary_ShouldReadSqlFileAndReturnSqlSpecificSummary()
|
||||
{
|
||||
var path = Path.GetTempFileName() + ".sql";
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT); DELETE FROM users;");
|
||||
|
||||
var summary = SqlAnalysisService.BuildFallbackSummary(path);
|
||||
|
||||
summary.Should().Contain("SQL static analysis");
|
||||
summary.Should().Contain("PostgreSQL");
|
||||
summary.Should().Contain("CREATE TABLE");
|
||||
summary.Should().Contain("DELETE");
|
||||
summary.Should().Contain("users");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/AxCopilot.Tests/Services/SqlDialectDetectorTests.cs
Normal file
18
src/AxCopilot.Tests/Services/SqlDialectDetectorTests.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using AxCopilot.Services;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace AxCopilot.Tests.Services;
|
||||
|
||||
public class SqlDialectDetectorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);", "PostgreSQL")]
|
||||
[InlineData("CREATE TABLE [dbo].[Users] (Id INT IDENTITY(1,1), Name NVARCHAR(100)); GO", "SQL Server")]
|
||||
[InlineData("CREATE TABLE `users` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100)) ENGINE=InnoDB;", "MySQL")]
|
||||
[InlineData("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); PRAGMA foreign_keys=ON;", "SQLite")]
|
||||
public void DetectDialect_ShouldRecognizeCommonDialects(string sql, string expected)
|
||||
{
|
||||
SqlDialectDetector.DetectDialect(sql).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,7 @@ public static class ArtifactQualityReviewService
|
||||
var roadmapCount = Regex.Matches(html, @"roadmap(-block|-phase)?", RegexOptions.IgnoreCase).Count;
|
||||
var matrixCount = Regex.Matches(html, @"matrix-grid|risk-matrix", RegexOptions.IgnoreCase).Count;
|
||||
var decisionCount = Regex.Matches(html, @"decision-summary", RegexOptions.IgnoreCase).Count;
|
||||
var kpiPanelCount = Regex.Matches(html, @"kpi-panel", RegexOptions.IgnoreCase).Count;
|
||||
var evidenceCardCount = Regex.Matches(html, @"evidence-cards|evidence-card", RegexOptions.IgnoreCase).Count;
|
||||
var boardPanelCount = Regex.Matches(html, @"board-report-panel", RegexOptions.IgnoreCase).Count;
|
||||
var strategyBriefCount = Regex.Matches(html, @"strategy-brief-panel", RegexOptions.IgnoreCase).Count;
|
||||
@@ -141,6 +142,7 @@ public static class ArtifactQualityReviewService
|
||||
+ (roadmapCount > 0 ? 1 : 0)
|
||||
+ (matrixCount > 0 ? 1 : 0)
|
||||
+ (decisionCount > 0 ? 1 : 0)
|
||||
+ (kpiPanelCount > 0 ? 1 : 0)
|
||||
+ (evidenceCardCount > 0 ? 1 : 0)
|
||||
+ (kpiCount > 0 ? 1 : 0);
|
||||
var supportingBlockEstimate = paragraphCount
|
||||
@@ -150,6 +152,7 @@ public static class ArtifactQualityReviewService
|
||||
+ roadmapCount
|
||||
+ matrixCount
|
||||
+ decisionCount
|
||||
+ kpiPanelCount
|
||||
+ evidenceCardCount
|
||||
+ boardPanelCount
|
||||
+ strategyBriefCount
|
||||
@@ -161,11 +164,12 @@ public static class ArtifactQualityReviewService
|
||||
if (printReady) strengths.Add("Includes print-ready CSS");
|
||||
if (hasPrintFrame) strengths.Add("Includes print header/footer frame");
|
||||
if (majorSectionEstimate >= 5) strengths.Add($"Contains {majorSectionEstimate} major sections");
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || decisionCount > 0 || evidenceCardCount > 0 || boardPanelCount > 0 || strategyBriefCount > 0 || kpiCount > 0)
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || decisionCount > 0 || kpiPanelCount > 0 || evidenceCardCount > 0 || boardPanelCount > 0 || strategyBriefCount > 0 || kpiCount > 0)
|
||||
strengths.Add("Uses structured business blocks");
|
||||
if (calloutCount > 0) strengths.Add("Uses callout blocks for emphasis");
|
||||
if (boardPanelCount > 0) strengths.Add("Includes board-ready summary panel");
|
||||
if (strategyBriefCount > 0) strengths.Add("Includes strategy brief panel");
|
||||
if (kpiPanelCount > 0) strengths.Add("Includes KPI panel");
|
||||
|
||||
if (html.Length < 1800)
|
||||
issues.Add(new("Body content may be too short for an executive-quality document.", ArtifactReviewSeverity.Warning));
|
||||
@@ -173,7 +177,7 @@ public static class ArtifactQualityReviewService
|
||||
issues.Add(new("Major section count is low for a business report.", ArtifactReviewSeverity.Warning));
|
||||
if (supportingBlockEstimate < Math.Max(4, majorSectionEstimate * 2))
|
||||
issues.Add(new("Several sections may need more supporting paragraphs.", ArtifactReviewSeverity.Warning));
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + decisionCount + evidenceCardCount + boardPanelCount + strategyBriefCount + kpiCount == 0)
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + decisionCount + kpiPanelCount + evidenceCardCount + boardPanelCount + strategyBriefCount + kpiCount == 0)
|
||||
issues.Add(new("Structured visual blocks are limited.", ArtifactReviewSeverity.Warning));
|
||||
if (placeholderCount > 0)
|
||||
issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", ArtifactReviewSeverity.Critical));
|
||||
@@ -187,12 +191,18 @@ public static class ArtifactQualityReviewService
|
||||
issues.Add(new("Print-ready report could benefit from a cover page.", ArtifactReviewSeverity.Info));
|
||||
if (boardPanelCount > 0 && decisionCount == 0)
|
||||
issues.Add(new("Board-ready report should include a decision summary block.", ArtifactReviewSeverity.Warning));
|
||||
if (boardPanelCount > 0 && kpiPanelCount == 0 && kpiCount == 0)
|
||||
issues.Add(new("Board-ready report would be stronger with a KPI panel or metric strip.", ArtifactReviewSeverity.Info));
|
||||
if (boardPanelCount > 0 && evidenceCardCount == 0 && tableCount == 0)
|
||||
issues.Add(new("Board-ready report would benefit from evidence cards or a supporting table.", ArtifactReviewSeverity.Info));
|
||||
if (strategyBriefCount > 0 && decisionCount == 0)
|
||||
issues.Add(new("Strategy brief should include explicit decisions or a decision summary block.", ArtifactReviewSeverity.Warning));
|
||||
if (strategyBriefCount > 0 && comparisonCount + roadmapCount == 0)
|
||||
issues.Add(new("Strategy brief would be stronger with a comparison or roadmap block.", ArtifactReviewSeverity.Info));
|
||||
if (strategyBriefCount > 0 && evidenceCardCount + kpiPanelCount == 0)
|
||||
issues.Add(new("Strategy brief would benefit from KPI or evidence support blocks.", ArtifactReviewSeverity.Info));
|
||||
if (kpiPanelCount > 0 && decisionCount == 0 && boardPanelCount == 0)
|
||||
issues.Add(new("KPI panel should roll into an explicit decision or next-step block.", ArtifactReviewSeverity.Info));
|
||||
|
||||
return BuildReport("html", strengths, issues);
|
||||
}
|
||||
|
||||
@@ -36,16 +36,22 @@ public static class ArtifactRepairGuideService
|
||||
{
|
||||
if (message.Contains("Board-ready report should include a decision summary block", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add a decision summary block directly after the board ask so the approval point is explicit";
|
||||
if (message.Contains("KPI panel or metric strip", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add a KPI panel or metric strip so the board report shows the top-line business signal at a glance";
|
||||
if (message.Contains("decision summary or evidence cards", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add a decision summary and evidence cards near the recommendation section";
|
||||
if (message.Contains("Strategy brief should include explicit decisions", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add an explicit decisions block or decision summary to the strategy brief";
|
||||
if (message.Contains("KPI or evidence support blocks", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add KPI or evidence support blocks so the strategy brief is backed by measurable proof";
|
||||
if (message.Contains("cover page", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add a cover page for print-ready or board-facing reports";
|
||||
if (message.Contains("table of contents", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add a table of contents for longer print-ready reports";
|
||||
if (message.Contains("comparison or roadmap", StringComparison.OrdinalIgnoreCase))
|
||||
return "Add comparison or roadmap blocks to make options and sequencing explicit";
|
||||
if (message.Contains("explicit decision or next-step block", StringComparison.OrdinalIgnoreCase))
|
||||
return "Connect the KPI panel to an explicit decision or next-step block";
|
||||
if (message.Contains("supporting paragraphs", StringComparison.OrdinalIgnoreCase))
|
||||
return "Expand the core sections with supporting paragraphs and business evidence";
|
||||
if (message.Contains("Structured visual blocks are limited", StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@@ -163,6 +163,35 @@ public static class DeckPlanningService
|
||||
repairs++;
|
||||
}
|
||||
|
||||
var requestedLayout = layout.Trim().ToLowerInvariant();
|
||||
if (requestedLayout == "content")
|
||||
{
|
||||
if (HasArrayItems(slide, "kpis"))
|
||||
{
|
||||
slide["layout"] = "kpi_dashboard";
|
||||
layout = "kpi_dashboard";
|
||||
repairs++;
|
||||
}
|
||||
else if (HasChartData(slide))
|
||||
{
|
||||
slide["layout"] = "chart";
|
||||
layout = "chart";
|
||||
repairs++;
|
||||
}
|
||||
else if (HasArrayItems(slide, "options"))
|
||||
{
|
||||
slide["layout"] = "comparison";
|
||||
layout = "comparison";
|
||||
repairs++;
|
||||
}
|
||||
else if (HasArrayItems(slide, "phases"))
|
||||
{
|
||||
slide["layout"] = "roadmap";
|
||||
layout = "roadmap";
|
||||
repairs++;
|
||||
}
|
||||
}
|
||||
|
||||
switch (layout.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "issue_tree":
|
||||
@@ -599,6 +628,13 @@ public static class DeckPlanningService
|
||||
values.Count > 0;
|
||||
}
|
||||
|
||||
private static bool HasArrayItems(JsonObject slide, string propertyName)
|
||||
{
|
||||
return slide.TryGetPropertyValue(propertyName, out var node)
|
||||
&& node is JsonArray array
|
||||
&& array.Count > 0;
|
||||
}
|
||||
|
||||
private static int ClampArray(JsonObject slide, string propertyName, int maxCount)
|
||||
{
|
||||
if (!slide.TryGetPropertyValue(propertyName, out var node) || node is not JsonArray array || array.Count <= maxCount)
|
||||
|
||||
@@ -212,6 +212,11 @@ public static class DeckQualityReviewService
|
||||
issues.Add(new($"Slide {slideNumber}: Executive Summary should include a recommendation or decision ask.", DeckReviewSeverity.Info));
|
||||
added++;
|
||||
}
|
||||
if (ReadJsonArrayCount(slide, "kpis") == 0)
|
||||
{
|
||||
issues.Add(new($"Slide {slideNumber}: Executive Summary should include KPI cards or quantified evidence.", DeckReviewSeverity.Info));
|
||||
added++;
|
||||
}
|
||||
break;
|
||||
case "recommendation":
|
||||
if (string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
|
||||
@@ -232,6 +237,11 @@ public static class DeckQualityReviewService
|
||||
issues.Add(new($"Slide {slideNumber}: comparison slide needs at least two options.", DeckReviewSeverity.Warning));
|
||||
added++;
|
||||
}
|
||||
else if (!slide.GetRawText().Contains("\"verdict\"", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
issues.Add(new($"Slide {slideNumber}: comparison slide should highlight a clear verdict.", DeckReviewSeverity.Info));
|
||||
added++;
|
||||
}
|
||||
break;
|
||||
case "roadmap":
|
||||
if (ReadJsonArrayCount(slide, "phases") == 0)
|
||||
@@ -244,6 +254,12 @@ public static class DeckQualityReviewService
|
||||
issues.Add(new($"Slide {slideNumber}: roadmap slide should show at least two phases.", DeckReviewSeverity.Info));
|
||||
added++;
|
||||
}
|
||||
if (!slide.GetRawText().Contains("\"owner\"", StringComparison.OrdinalIgnoreCase) ||
|
||||
!slide.GetRawText().Contains("\"timeline\"", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
issues.Add(new($"Slide {slideNumber}: roadmap slide would be stronger with timeline and owner labels.", DeckReviewSeverity.Info));
|
||||
added++;
|
||||
}
|
||||
break;
|
||||
case "chart":
|
||||
if (ReadJsonArrayCount(slide, "chart_labels") == 0 || ReadJsonArrayCount(slide, "chart_values") == 0)
|
||||
@@ -259,6 +275,19 @@ public static class DeckQualityReviewService
|
||||
added++;
|
||||
}
|
||||
break;
|
||||
case "kpi_dashboard":
|
||||
if (ReadJsonArrayCount(slide, "kpis") < 3)
|
||||
{
|
||||
issues.Add(new($"Slide {slideNumber}: KPI dashboard should show at least three metrics.", DeckReviewSeverity.Warning));
|
||||
added++;
|
||||
}
|
||||
if (ReadStringList(slide, "summary_points").Count == 0 &&
|
||||
ReadStringList(slide, "body").Count == 0)
|
||||
{
|
||||
issues.Add(new($"Slide {slideNumber}: KPI dashboard needs takeaway points, not just metric tiles.", DeckReviewSeverity.Info));
|
||||
added++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return added;
|
||||
|
||||
@@ -16,12 +16,18 @@ public static class DeckRepairGuideService
|
||||
=> "Add a recommendation or decision ask directly to the Executive Summary",
|
||||
var message when message.Contains("Executive Summary", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add or strengthen the Executive Summary with 2-3 evidence-backed takeaways",
|
||||
var message when message.Contains("KPI cards or quantified evidence", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add KPI cards or quantified evidence so the executive message is backed by numbers",
|
||||
var message when message.Contains("supporting rationale or next steps", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add supporting rationale or next steps so the recommendation turns into action",
|
||||
var message when message.Contains("Recommendation", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add a clear recommendation or decision request slide near the end of the deck",
|
||||
var message when message.Contains("roadmap", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add a roadmap slide with phases, owners, and timing",
|
||||
var message when message.Contains("clear verdict", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Label the preferred option explicitly so the comparison slide lands a decision",
|
||||
var message when message.Contains("timeline and owner labels", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add timeline and owner labels so the roadmap reads like an execution plan",
|
||||
var message when message.Contains("Appendix or evidence slide", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add appendix or evidence slides with supporting data, assumptions, or source tables",
|
||||
var message when message.Contains("duplicate headline", StringComparison.OrdinalIgnoreCase)
|
||||
@@ -42,6 +48,10 @@ public static class DeckRepairGuideService
|
||||
=> "Provide chart labels and values or replace the slide with a comparison/evidence layout",
|
||||
var message when message.Contains("table slide", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Provide complete table headers and rows or simplify the slide to key callouts",
|
||||
var message when message.Contains("KPI dashboard should show at least three metrics", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Expand the KPI dashboard to at least three core metrics so the trend is clear",
|
||||
var message when message.Contains("KPI dashboard needs takeaway points", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Add takeaway points beneath the KPI tiles so the metrics translate into action",
|
||||
var message when message.Contains("text-heavy", StringComparison.OrdinalIgnoreCase)
|
||||
=> "Convert text-heavy slides into message-led visuals with fewer bullets",
|
||||
var message when message.Contains("Evidence slides", StringComparison.OrdinalIgnoreCase)
|
||||
|
||||
@@ -53,6 +53,7 @@ public class HtmlSkill : IAgentTool
|
||||
"'chart' {kind:'bar|horizontal_bar', title, data:[{label, value, color}]}, " +
|
||||
"'cards' {items:[{title, body, badge, icon}]}, " +
|
||||
"'decision_summary' {title, decision, rationale, actions:['...']}, " +
|
||||
"'kpi_panel' {title, headline, items:[{label,value,trend,note}], takeaway}, " +
|
||||
"'evidence_cards' {title, items:[{title, detail, source, tag}]}, " +
|
||||
"'board_report' {title, decision, recommendation, rationale, metrics:[{label,value,note}], risks:['...'], next_steps:['...']}, " +
|
||||
"'strategy_brief' {title, strategic_question, thesis, implications:['...'], decisions:['...']}, " +
|
||||
@@ -357,6 +358,9 @@ public class HtmlSkill : IAgentTool
|
||||
case "decision_summary":
|
||||
sb.AppendLine(RenderDecisionSummary(section));
|
||||
break;
|
||||
case "kpi_panel":
|
||||
sb.AppendLine(RenderKpiPanel(section));
|
||||
break;
|
||||
case "evidence_cards":
|
||||
sb.AppendLine(RenderEvidenceCards(section));
|
||||
break;
|
||||
@@ -605,6 +609,24 @@ public class HtmlSkill : IAgentTool
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderKpiPanel(JsonElement s)
|
||||
{
|
||||
var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : "KPI Panel";
|
||||
var headline = s.SafeTryGetProperty("headline", out var headlineEl) ? headlineEl.SafeGetString() : null;
|
||||
var takeaway = s.SafeTryGetProperty("takeaway", out var takeawayEl) ? takeawayEl.SafeGetString() : null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<section class=\"kpi-panel\" style=\"margin:1.4rem 0;padding:1.15rem 1.2rem;border:1px solid #dbe4f0;border-radius:18px;background:linear-gradient(180deg,#f8fafc,#ffffff);box-shadow:0 10px 26px rgba(15,23,42,.05);\">");
|
||||
sb.AppendLine($"<div style=\"font-size:1rem;font-weight:800;color:#0f172a;margin-bottom:.55rem\">{Escape(title ?? "KPI Panel")}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(headline))
|
||||
sb.AppendLine($"<div style=\"font-size:.95rem;font-weight:700;color:#1d4ed8;margin-bottom:.85rem\">{MarkdownToHtml(headline)}</div>");
|
||||
sb.AppendLine(RenderKpi(s));
|
||||
if (!string.IsNullOrWhiteSpace(takeaway))
|
||||
sb.AppendLine($"<div style=\"margin-top:.85rem;padding:.8rem 1rem;border-radius:14px;background:#eef2ff;color:#312e81;font-weight:700\">Takeaway: {MarkdownToHtml(takeaway)}</div>");
|
||||
sb.AppendLine("</section>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderEvidenceCards(JsonElement s)
|
||||
{
|
||||
if (!s.SafeTryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array)
|
||||
|
||||
@@ -39,6 +39,7 @@ public static class CodeLanguageCatalog
|
||||
["ruby"] = ["Gemfile", "Gemfile.lock", "*.gemspec"],
|
||||
["kotlin"] = ["build.gradle.kts", "settings.gradle.kts", "gradle.properties"],
|
||||
["swift"] = ["Package.swift", "*.xcodeproj", "*.xcworkspace"],
|
||||
["sql"] = ["migrations/*.sql", "schema.sql", "seed.sql", "*.sqlproj"],
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string[]> s_buildHints =
|
||||
@@ -56,6 +57,7 @@ public static class CodeLanguageCatalog
|
||||
["ruby"] = ["bundle exec rake", "ruby -c"],
|
||||
["kotlin"] = ["gradle build", "./gradlew build"],
|
||||
["swift"] = ["swift build", "xcodebuild build"],
|
||||
["sql"] = ["apply in a disposable database first", "review migration order and dependency impact"],
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string[]> s_testHints =
|
||||
@@ -73,6 +75,7 @@ public static class CodeLanguageCatalog
|
||||
["ruby"] = ["bundle exec rspec", "bundle exec rake test"],
|
||||
["kotlin"] = ["gradle test", "./gradlew test"],
|
||||
["swift"] = ["swift test", "xcodebuild test"],
|
||||
["sql"] = ["run the script against a disposable database", "verify affected row counts and dependent queries"],
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string[]> s_lintHints =
|
||||
@@ -89,6 +92,7 @@ public static class CodeLanguageCatalog
|
||||
["ruby"] = ["rubocop", "standardrb"],
|
||||
["kotlin"] = ["./gradlew ktlintCheck", "./gradlew detekt"],
|
||||
["swift"] = ["swiftformat --lint", "swiftlint"],
|
||||
["sql"] = ["check destructive statements and transaction boundaries", "review index, constraint, and view impact"],
|
||||
};
|
||||
|
||||
private static readonly ReadOnlyCollection<CodeLanguageCapability> s_all =
|
||||
@@ -257,8 +261,8 @@ public static class CodeLanguageCatalog
|
||||
"SQL",
|
||||
[".sql"],
|
||||
[
|
||||
"Preserve migration ordering, transactional safety, and index/constraint compatibility.",
|
||||
"Call out destructive or data-migrating changes explicitly."
|
||||
"Preserve migration ordering, dialect assumptions, and transaction boundaries.",
|
||||
"Call out destructive DDL, broad DML, and index or constraint impact explicitly."
|
||||
]),
|
||||
new(
|
||||
"web",
|
||||
@@ -351,7 +355,7 @@ public static class CodeLanguageCatalog
|
||||
}
|
||||
|
||||
foreach (var capability in s_all.Where(x =>
|
||||
x.Key is "csharp" or "python" or "java" or "cpp" or "typescript" or "javascript" or "go" or "rust" or "kotlin" or "swift"))
|
||||
x.Key is "csharp" or "python" or "java" or "cpp" or "typescript" or "javascript" or "go" or "rust" or "kotlin" or "swift" or "sql"))
|
||||
{
|
||||
var summary = capability.Guidance.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(summary))
|
||||
@@ -437,6 +441,8 @@ public static class CodeLanguageCatalog
|
||||
var primaryGuidance = capability.Guidance.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(primaryGuidance))
|
||||
parts.Add("focus: " + primaryGuidance);
|
||||
if (string.Equals(capability.Key, "sql", StringComparison.OrdinalIgnoreCase))
|
||||
parts.Add("analysis: detect dialect, statement kinds, destructive risk, and object dependencies");
|
||||
|
||||
if (parts.Count == 0)
|
||||
return capability.DisplayName;
|
||||
@@ -481,6 +487,8 @@ public static class CodeLanguageCatalog
|
||||
public static string BuildFallbackSummary(string? filePathOrExtension)
|
||||
{
|
||||
var capability = ResolveCapabilityFromKeyOrExtension(filePathOrExtension);
|
||||
if (capability != null && string.Equals(capability.Key, "sql", StringComparison.OrdinalIgnoreCase))
|
||||
return SqlAnalysisService.BuildFallbackSummary(filePathOrExtension);
|
||||
if (capability == null)
|
||||
return "정적 fallback: 확장자와 프로젝트 매니페스트를 먼저 확인하고 관련 build/test/lint 힌트를 따라 수동 검증하세요.";
|
||||
|
||||
|
||||
215
src/AxCopilot/Services/SqlAnalysisService.cs
Normal file
215
src/AxCopilot/Services/SqlAnalysisService.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
public sealed record SqlAnalysisReport(
|
||||
string Dialect,
|
||||
IReadOnlyList<string> StatementKinds,
|
||||
IReadOnlyList<string> Objects,
|
||||
IReadOnlyList<string> Risks,
|
||||
IReadOnlyList<string> SuggestedChecks)
|
||||
{
|
||||
public string ToFallbackSummary()
|
||||
{
|
||||
var parts = new List<string> { $"SQL static analysis: {Dialect}" };
|
||||
if (StatementKinds.Count > 0)
|
||||
parts.Add("statements: " + string.Join(", ", StatementKinds.Take(5)));
|
||||
if (Objects.Count > 0)
|
||||
parts.Add("objects: " + string.Join(", ", Objects.Take(6)));
|
||||
if (Risks.Count > 0)
|
||||
parts.Add("risks: " + string.Join(" | ", Risks.Take(3)));
|
||||
if (SuggestedChecks.Count > 0)
|
||||
parts.Add("next checks: " + string.Join(" | ", SuggestedChecks.Take(3)));
|
||||
return string.Join(Environment.NewLine, parts);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SqlAnalysisService
|
||||
{
|
||||
public static SqlAnalysisReport Analyze(string? sqlText, string? filePath = null)
|
||||
{
|
||||
var sql = sqlText ?? string.Empty;
|
||||
var dialect = SqlDialectDetector.DetectDialect(sql);
|
||||
var normalized = sql.Replace("\r", " ");
|
||||
var statements = SplitStatements(normalized);
|
||||
|
||||
var statementKinds = DetectStatementKinds(normalized);
|
||||
var objects = DetectObjects(normalized);
|
||||
var risks = DetectRisks(statements, normalized);
|
||||
var checks = BuildSuggestedChecks(statementKinds, risks, dialect, filePath);
|
||||
|
||||
return new SqlAnalysisReport(dialect, statementKinds, objects, risks, checks);
|
||||
}
|
||||
|
||||
public static string BuildFallbackSummary(string? filePathOrExtension)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(filePathOrExtension) && File.Exists(filePathOrExtension))
|
||||
{
|
||||
var sql = File.ReadAllText(filePathOrExtension);
|
||||
return Analyze(sql, filePathOrExtension).ToFallbackSummary();
|
||||
}
|
||||
|
||||
return new SqlAnalysisReport(
|
||||
"Generic SQL",
|
||||
["DDL/DML review"],
|
||||
[],
|
||||
["Review destructive statements and transaction boundaries before execution."],
|
||||
[
|
||||
"Confirm migration order, object dependencies, and rollback strategy",
|
||||
"Run the script in a disposable database before applying it to shared environments",
|
||||
"Review broad UPDATE/DELETE predicates, indexes, and constraint impact"
|
||||
]).ToFallbackSummary();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> DetectStatementKinds(string sql)
|
||||
{
|
||||
var kinds = new List<string>();
|
||||
AddKind(kinds, sql, @"\bcreate\s+table\b", "CREATE TABLE");
|
||||
AddKind(kinds, sql, @"\balter\s+table\b", "ALTER TABLE");
|
||||
AddKind(kinds, sql, @"\bdrop\s+table\b", "DROP TABLE");
|
||||
AddKind(kinds, sql, @"\btruncate\s+table\b", "TRUNCATE TABLE");
|
||||
AddKind(kinds, sql, @"\bcreate\s+(unique\s+)?index\b", "CREATE INDEX");
|
||||
AddKind(kinds, sql, @"\bcreate\s+view\b", "CREATE VIEW");
|
||||
AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?function\b", "CREATE FUNCTION");
|
||||
AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?procedure\b", "CREATE PROCEDURE");
|
||||
AddKind(kinds, sql, @"\binsert\s+into\b", "INSERT");
|
||||
AddKind(kinds, sql, @"\bupdate\b", "UPDATE");
|
||||
AddKind(kinds, sql, @"\bdelete\s+from\b", "DELETE");
|
||||
AddKind(kinds, sql, @"\bmerge\s+into\b", "MERGE");
|
||||
AddKind(kinds, sql, @"\bselect\b", "SELECT");
|
||||
return kinds;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> DetectObjects(string sql)
|
||||
{
|
||||
var patterns = new[]
|
||||
{
|
||||
@"\bcreate\s+table\s+(if\s+not\s+exists\s+)?(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\balter\s+table\s+(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\bdrop\s+table\s+(if\s+exists\s+)?(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\bcreate\s+view\s+(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\bupdate\s+(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\binsert\s+into\s+(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\bdelete\s+from\s+(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\bfrom\s+(?<name>[\[\]`\""\w\.]+)",
|
||||
@"\bjoin\s+(?<name>[\[\]`\""\w\.]+)"
|
||||
};
|
||||
|
||||
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
foreach (Match match in Regex.Matches(sql, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline))
|
||||
{
|
||||
var name = NormalizeObjectName(match.Groups["name"].Value);
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
names.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return names.Take(10).ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> DetectRisks(IReadOnlyList<string> statements, string fullSql)
|
||||
{
|
||||
var risks = new List<string>();
|
||||
if (Regex.IsMatch(fullSql, @"\bdrop\s+table\b|\bdrop\s+view\b", RegexOptions.IgnoreCase))
|
||||
risks.Add("Destructive DROP statement is present.");
|
||||
if (Regex.IsMatch(fullSql, @"\btruncate\s+table\b", RegexOptions.IgnoreCase))
|
||||
risks.Add("TRUNCATE will remove all rows and is hard to recover without backups.");
|
||||
if (Regex.IsMatch(fullSql, @"\balter\s+table\b[\s\S]{0,120}\bdrop\s+column\b", RegexOptions.IgnoreCase))
|
||||
risks.Add("ALTER TABLE DROP COLUMN can break downstream queries or code paths.");
|
||||
|
||||
foreach (var statement in statements)
|
||||
{
|
||||
if (Regex.IsMatch(statement, @"^\s*delete\s+from\b", RegexOptions.IgnoreCase) &&
|
||||
!Regex.IsMatch(statement, @"\bwhere\b", RegexOptions.IgnoreCase))
|
||||
{
|
||||
risks.Add("DELETE statement without WHERE clause was detected.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var statement in statements)
|
||||
{
|
||||
if (Regex.IsMatch(statement, @"^\s*update\b", RegexOptions.IgnoreCase) &&
|
||||
!Regex.IsMatch(statement, @"\bwhere\b", RegexOptions.IgnoreCase))
|
||||
{
|
||||
risks.Add("UPDATE statement without WHERE clause was detected.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Regex.IsMatch(fullSql, @"\b(select\s+\*)", RegexOptions.IgnoreCase))
|
||||
risks.Add("SELECT * is present and may widen downstream impact.");
|
||||
|
||||
var hasTransactionalWork = Regex.IsMatch(fullSql, @"\b(create|alter|drop|truncate|insert|update|delete|merge)\b", RegexOptions.IgnoreCase);
|
||||
var hasTransaction = Regex.IsMatch(fullSql, @"\bbegin\s+tran(saction)?\b|\bstart\s+transaction\b|\bcommit\b", RegexOptions.IgnoreCase);
|
||||
if (hasTransactionalWork && !hasTransaction)
|
||||
risks.Add("Explicit transaction boundaries are not visible in the script.");
|
||||
|
||||
return risks.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildSuggestedChecks(
|
||||
IReadOnlyList<string> statementKinds,
|
||||
IReadOnlyList<string> risks,
|
||||
string dialect,
|
||||
string? filePath)
|
||||
{
|
||||
var checks = new List<string>();
|
||||
if (statementKinds.Any(kind => kind is "CREATE TABLE" or "ALTER TABLE" or "DROP TABLE"))
|
||||
checks.Add("Verify migration order, dependent views, indexes, and foreign keys.");
|
||||
if (statementKinds.Any(kind => kind is "UPDATE" or "DELETE" or "MERGE"))
|
||||
checks.Add("Review predicates and expected row counts before running data-changing statements.");
|
||||
if (risks.Any())
|
||||
checks.Add("Run the script in a disposable database and confirm rollback strategy first.");
|
||||
|
||||
if (string.Equals(dialect, "SQL Server", StringComparison.OrdinalIgnoreCase))
|
||||
checks.Add("Review GO batch boundaries, transaction scope, and locking behavior.");
|
||||
else if (string.Equals(dialect, "PostgreSQL", StringComparison.OrdinalIgnoreCase))
|
||||
checks.Add("Confirm migration order, transactional DDL behavior, and dependent objects.");
|
||||
else if (string.Equals(dialect, "MySQL", StringComparison.OrdinalIgnoreCase))
|
||||
checks.Add("Review engine-specific behavior, delimiter usage, and online migration impact.");
|
||||
|
||||
var fileName = Path.GetFileName(filePath ?? string.Empty);
|
||||
if (fileName.Contains("seed", StringComparison.OrdinalIgnoreCase))
|
||||
checks.Add("Confirm the script is safe to rerun and that seed data remains idempotent.");
|
||||
if (fileName.Contains("migration", StringComparison.OrdinalIgnoreCase) ||
|
||||
fileName.Contains("schema", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
checks.Add("Validate forward-only execution order and any required rollback notes.");
|
||||
}
|
||||
|
||||
return checks.Distinct(StringComparer.OrdinalIgnoreCase).Take(5).ToList();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> SplitStatements(string sql)
|
||||
{
|
||||
return sql
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(statement => !string.IsNullOrWhiteSpace(statement))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static void AddKind(List<string> kinds, string sql, string pattern, string label)
|
||||
{
|
||||
if (Regex.IsMatch(sql, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline) &&
|
||||
!kinds.Contains(label, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
kinds.Add(label);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeObjectName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return string.Empty;
|
||||
|
||||
var normalized = name.Trim();
|
||||
normalized = normalized.Trim('[', ']', '`', '"');
|
||||
normalized = normalized.TrimEnd(',', ';');
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
37
src/AxCopilot/Services/SqlDialectDetector.cs
Normal file
37
src/AxCopilot/Services/SqlDialectDetector.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AxCopilot.Services;
|
||||
|
||||
public static class SqlDialectDetector
|
||||
{
|
||||
public static string DetectDialect(string? sqlText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sqlText))
|
||||
return "Generic SQL";
|
||||
|
||||
var sql = sqlText;
|
||||
var scores = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["PostgreSQL"] = 0,
|
||||
["MySQL"] = 0,
|
||||
["SQL Server"] = 0,
|
||||
["SQLite"] = 0,
|
||||
["Oracle"] = 0,
|
||||
};
|
||||
|
||||
AddScore(sql, scores, "PostgreSQL", 4, @"\bserial\b|\bbigserial\b|\breturning\b|\bilike\b|::\w+|\bjsonb\b|\blanguage\s+plpgsql\b");
|
||||
AddScore(sql, scores, "MySQL", 4, @"`[^`]+`|\bauto_increment\b|\bengine\s*=\s*\w+|\bunsigned\b|\bdelimiter\b");
|
||||
AddScore(sql, scores, "SQL Server", 4, @"\bgo\b|\bidentity\s*\(|\bnvarchar\b|\bgetdate\s*\(|\btop\s+\d+|\[dbo\]");
|
||||
AddScore(sql, scores, "SQLite", 4, @"\bautoincrement\b|\bwithout\s+rowid\b|\bpragma\b|\binsert\s+or\s+replace\b");
|
||||
AddScore(sql, scores, "Oracle", 4, @"\bvarchar2\b|\bnumber\s*\(|\bdual\b|\bsysdate\b|\bsequence\b");
|
||||
|
||||
var best = scores.OrderByDescending(pair => pair.Value).First();
|
||||
return best.Value <= 0 ? "Generic SQL" : best.Key;
|
||||
}
|
||||
|
||||
private static void AddScore(string sql, IDictionary<string, int> scores, string dialect, int score, string pattern)
|
||||
{
|
||||
if (Regex.IsMatch(sql, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline))
|
||||
scores[dialect] += score;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user