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:
2026-04-15 11:19:55 +09:00
parent f283662d30
commit 93c3c647d9
19 changed files with 605 additions and 5 deletions

View File

@@ -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);
}

View File

@@ -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))

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)

View File

@@ -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)