Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
lacvet 2a49b1da24 SQL 정적 분석과 PPT·HTML critic을 고도화하고 코드 탭 fallback 문맥을 보강
- SqlAnalysisService에 script intent, dependency, review focus 계산을 추가해 migration/seed/reporting SQL의 위험도와 검토 포인트를 더 정확히 안내하도록 개선했습니다.
- HtmlSkill에 decision_matrix, metric_strip 섹션을 추가하고 ArtifactQualityReviewService/ArtifactRepairGuideService에서 board·strategy 문서의 의사결정 구조와 KPI 연결 부족을 더 정밀하게 진단하도록 강화했습니다.
- DeckQualityReviewService와 DeckRepairGuideService를 확장해 executive summary headline, comparison trade-off, roadmap milestone, chart takeaway, KPI context 부족을 추가로 감지하고 보정 가이드를 반환하도록 정리했습니다.
- WorkspaceContextGenerator와 CodeLanguageCatalog를 업데이트해 SQL 저장소에서 SQL Review Focus와 확장된 workflow summary를 제공하도록 맞췄고, README/DEVELOPMENT/NEXT_ROADMAP에 2026-04-15 11:36 (KST) 기준 이력을 반영했습니다.

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_sql_doc_final\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc_final\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SqlDialectDetectorTests|SqlAnalysisServiceTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|HtmlSkillConsultingSectionsTests" -p:OutputPath=bin\\verify_code_sql_doc_final_tests\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc_final_tests\\ : 통과 62
2026-04-15 11:38:50 +09:00

361 lines
19 KiB
C#

using System.Text.RegularExpressions;
namespace AxCopilot.Services.Agent;
public enum ArtifactReviewSeverity
{
Info,
Warning,
Critical,
}
public sealed record ArtifactReviewIssue(string Message, ArtifactReviewSeverity Severity);
public sealed record ArtifactQualityReport(
string ArtifactType,
int Score,
IReadOnlyList<string> Strengths,
IReadOnlyList<ArtifactReviewIssue> Issues)
{
public string ToToolSummary()
{
var strengths = Strengths.Take(3).ToList();
var issues = Issues
.OrderByDescending(issue => issue.Severity)
.Take(3)
.Select(issue => issue.Message)
.ToList();
var parts = new List<string> { $"Quality score {Score}/100" };
if (strengths.Count > 0)
parts.Add("Strengths: " + string.Join(", ", strengths));
var slideAlertCount = Issues.Count(issue => issue.Message.StartsWith("Slide ", StringComparison.OrdinalIgnoreCase));
if (slideAlertCount > 0)
parts.Add($"Slide alerts: {slideAlertCount}");
if (issues.Count > 0)
parts.Add("Needs work: " + string.Join(", ", issues));
else
parts.Add("Needs work: none");
return string.Join(" | ", parts);
}
}
public sealed record StructuredDocumentReviewInput(
string Title,
int SectionCount,
int BodyCharacterCount,
int TableCount,
int ListCount,
int CalloutCount,
int HighlightCount,
int IconCount,
bool HasCoverPage,
bool HasTableOfContents,
bool HasTemplate,
bool HasHeaderFooter,
bool HasExecutiveSummarySection,
bool HasRecommendationSection,
bool HasAppendixSection);
public sealed record WorkbookReviewInput(
string Title,
int SheetCount,
int DetailSheetCount,
int DataRowCount,
int FormulaCount,
int HyperlinkCount,
int DataValidationCount,
int ConditionalFormattingCount,
bool HasSummarySheet,
bool HasDashboardSheet,
bool HasHighlightSection,
bool HasActionSection,
bool HasScorecardSection,
bool HasDecisionSection,
bool HasSheetSummarySection,
bool HasTrendSection,
bool HasVarianceSection,
bool HasDashboardTileSection);
public static class ArtifactQualityReviewService
{
private static readonly string[] ExecutiveKeywords =
[
"executive summary",
"summary",
"\uC694\uC57D",
"\uD575\uC2EC \uC694\uC57D",
];
private static readonly string[] RecommendationKeywords =
[
"recommendation",
"proposal",
"next steps",
"action plan",
"\uAD8C\uACE0",
"\uC81C\uC548",
"\uC2E4\uD589",
];
private static readonly string[] AppendixKeywords =
[
"appendix",
"reference",
"supplement",
"\uBD80\uB85D",
"\uCC38\uACE0",
];
public static ArtifactQualityReport ReviewHtml(
string title,
string html,
bool hasCover,
bool hasTableOfContents,
bool printReady,
bool hasPrintFrame = false)
{
title ??= string.Empty;
html ??= string.Empty;
var strengths = new List<string>();
var issues = new List<ArtifactReviewIssue>();
var sectionCount = Regex.Matches(html, @"<h2\b", RegexOptions.IgnoreCase).Count;
var paragraphCount = Regex.Matches(html, @"<p\b", RegexOptions.IgnoreCase).Count;
var tableCount = Regex.Matches(html, @"<table\b", RegexOptions.IgnoreCase).Count;
var calloutCount = Regex.Matches(html, @"callout-(info|warning|tip|danger)", RegexOptions.IgnoreCase).Count;
var comparisonCount = Regex.Matches(html, @"comparison-grid|comparison-card", RegexOptions.IgnoreCase).Count;
var decisionMatrixCount = Regex.Matches(html, @"decision-matrix", RegexOptions.IgnoreCase).Count;
var metricStripCount = Regex.Matches(html, @"metric-strip", RegexOptions.IgnoreCase).Count;
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;
var kpiCount = Regex.Matches(html, @"kpi-card|kpi-grid|metric-card", RegexOptions.IgnoreCase).Count;
var majorSectionEstimate = sectionCount
+ (boardPanelCount > 0 ? 1 : 0)
+ (strategyBriefCount > 0 ? 1 : 0)
+ (comparisonCount > 0 ? 1 : 0)
+ (decisionMatrixCount > 0 ? 1 : 0)
+ (metricStripCount > 0 ? 1 : 0)
+ (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
+ tableCount
+ calloutCount
+ comparisonCount
+ decisionMatrixCount
+ metricStripCount
+ roadmapCount
+ matrixCount
+ decisionCount
+ kpiPanelCount
+ evidenceCardCount
+ boardPanelCount
+ strategyBriefCount
+ (kpiCount > 0 ? 1 : 0);
var placeholderCount = CountPlaceholders(html);
if (hasCover) strengths.Add("Includes cover page");
if (hasTableOfContents) strengths.Add("Includes table of contents");
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 || decisionMatrixCount > 0 || metricStripCount > 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 (decisionMatrixCount > 0) strengths.Add("Includes decision matrix");
if (metricStripCount > 0) strengths.Add("Includes metric strip");
if (html.Length < 1800)
issues.Add(new("Body content may be too short for an executive-quality document.", ArtifactReviewSeverity.Warning));
if (majorSectionEstimate < 4)
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 + decisionMatrixCount + metricStripCount + 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));
if (!ContainsAny(html, ExecutiveKeywords))
issues.Add(new("Executive summary or summary section is missing.", ArtifactReviewSeverity.Warning));
if (printReady && !hasPrintFrame)
issues.Add(new("Print-ready export is missing a print header/footer frame.", ArtifactReviewSeverity.Info));
if (printReady && majorSectionEstimate >= 4 && decisionCount + evidenceCardCount == 0)
issues.Add(new("Print-ready business report would benefit from decision summary or evidence cards.", ArtifactReviewSeverity.Warning));
if (printReady && !hasCover && majorSectionEstimate >= 5)
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 (boardPanelCount > 0 && decisionMatrixCount == 0 && comparisonCount == 0)
issues.Add(new("Board-ready report would benefit from a comparison or decision matrix block.", 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 (strategyBriefCount > 0 && decisionMatrixCount == 0)
issues.Add(new("Strategy brief would be stronger with a decision matrix for option trade-offs.", 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));
if (metricStripCount > 0 && decisionCount + boardPanelCount == 0)
issues.Add(new("Metric strip should connect to an explicit decision, recommendation, or board summary block.", ArtifactReviewSeverity.Info));
return BuildReport("html", strengths, issues);
}
public static ArtifactQualityReport ReviewStructuredDocument(StructuredDocumentReviewInput input)
{
var strengths = new List<string>();
var issues = new List<ArtifactReviewIssue>();
if (input.HasTemplate) strengths.Add("Uses template-based styling");
if (input.HasCoverPage) strengths.Add("Includes cover page");
if (input.HasTableOfContents) strengths.Add("Includes table of contents");
if (input.HasHeaderFooter) strengths.Add("Includes header/footer");
if (input.SectionCount >= 5) strengths.Add($"Contains {input.SectionCount} major sections");
if (input.TableCount > 0) strengths.Add($"Includes {input.TableCount} table(s)");
if (input.CalloutCount + input.HighlightCount > 0) strengths.Add("Uses emphasis blocks");
if (input.ListCount > 0) strengths.Add("Uses structured list sections");
if (input.BodyCharacterCount < 1400)
issues.Add(new("Body content may be too short for an executive document.", ArtifactReviewSeverity.Warning));
if (input.SectionCount < 4)
issues.Add(new("Major section count is low for a business document.", ArtifactReviewSeverity.Warning));
if (input.SectionCount >= 5 && !input.HasCoverPage)
issues.Add(new("Client-facing document could benefit from a cover page.", ArtifactReviewSeverity.Info));
if (input.SectionCount >= 5 && !input.HasTableOfContents)
issues.Add(new("Long document could benefit from a table of contents.", ArtifactReviewSeverity.Info));
if (input.SectionCount >= 6 && !input.HasTemplate)
issues.Add(new("Template-based styling could improve consistency for a longer document.", ArtifactReviewSeverity.Info));
if (input.SectionCount >= 4 && !input.HasHeaderFooter)
issues.Add(new("Header or footer metadata is limited for a business document.", ArtifactReviewSeverity.Info));
if (!input.HasExecutiveSummarySection)
issues.Add(new("Executive Summary section is missing.", ArtifactReviewSeverity.Warning));
if (!input.HasRecommendationSection)
issues.Add(new("Recommendation or next-step section is missing.", ArtifactReviewSeverity.Warning));
if (input.SectionCount >= 6 && !input.HasAppendixSection)
issues.Add(new("Appendix or reference section is limited for a long document.", ArtifactReviewSeverity.Info));
if (input.SectionCount >= 5 && input.TableCount == 0)
issues.Add(new("Supporting evidence table is limited for a business document.", ArtifactReviewSeverity.Info));
if (input.SectionCount >= 5 && input.CalloutCount + input.HighlightCount == 0)
issues.Add(new("Key messages would benefit from callout or highlight blocks.", ArtifactReviewSeverity.Info));
if (input.TableCount + input.ListCount + input.CalloutCount + input.HighlightCount == 0)
issues.Add(new("Document structure is mostly plain paragraphs.", ArtifactReviewSeverity.Warning));
return BuildReport("docx", strengths, issues);
}
public static ArtifactQualityReport ReviewWorkbook(WorkbookReviewInput input)
{
var strengths = new List<string>();
var issues = new List<ArtifactReviewIssue>();
if (input.HasSummarySheet) strengths.Add("Includes summary sheet");
if (input.HasDashboardSheet) strengths.Add("Includes dashboard sheet");
if (input.DetailSheetCount > 1) strengths.Add($"Contains {input.DetailSheetCount} detail sheets");
if (input.FormulaCount > 0) strengths.Add($"Includes {input.FormulaCount} formula cell(s)");
if (input.HyperlinkCount > 0) strengths.Add("Includes navigation links");
if (input.DataValidationCount > 0) strengths.Add("Includes data validation rules");
if (input.ConditionalFormattingCount > 0) strengths.Add("Includes conditional formatting");
if (input.HasHighlightSection) strengths.Add("Includes highlight section");
if (input.HasActionSection) strengths.Add("Includes action section");
if (input.HasScorecardSection) strengths.Add("Includes KPI or scorecard section");
if (input.HasDecisionSection) strengths.Add("Includes decision summary");
if (input.HasSheetSummarySection) strengths.Add("Includes detail sheet summaries");
if (input.SheetCount > 1 && !input.HasSummarySheet)
issues.Add(new("Workbook has multiple sheets but no summary sheet.", ArtifactReviewSeverity.Warning));
if (input.DataRowCount >= 10 && input.FormulaCount == 0)
issues.Add(new("Workbook has enough data to benefit from more formulas or rollups.", ArtifactReviewSeverity.Warning));
if (input.HasSummarySheet && input.HyperlinkCount == 0)
issues.Add(new("Summary sheet does not link to detail sheets.", ArtifactReviewSeverity.Warning));
if (input.DataValidationCount == 0 && input.DataRowCount >= 5)
issues.Add(new("Input controls are limited for a workbook with editable data.", ArtifactReviewSeverity.Info));
if (input.ConditionalFormattingCount == 0 && input.DataRowCount >= 8)
issues.Add(new("Conditional formatting is limited for a workbook with enough data to prioritize or flag.", ArtifactReviewSeverity.Info));
if (input.HasSummarySheet && !input.HasScorecardSection && !input.HasDecisionSection && !input.HasHighlightSection)
issues.Add(new("Summary sheet could better surface KPIs, decisions, or highlights.", ArtifactReviewSeverity.Info));
if (input.HasSummarySheet && input.DetailSheetCount >= 2 && !input.HasDashboardSheet)
issues.Add(new("Workbook could benefit from a dashboard sheet to summarize multi-sheet trends.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && !input.HasScorecardSection && !input.HasDecisionSection)
issues.Add(new("Dashboard sheet lacks KPI, trend, or decision content.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && !input.HasHighlightSection && !input.HasActionSection)
issues.Add(new("Dashboard sheet could better call out highlights or actions.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && input.DetailSheetCount >= 2 && input.HyperlinkCount == 0)
issues.Add(new("Dashboard sheet should link to supporting detail sheets.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && input.DetailSheetCount >= 2 && input.FormulaCount < 3)
issues.Add(new("Dashboard sheet would benefit from more calculated trend or variance formulas.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && input.DetailSheetCount >= 2 && !input.HasTrendSection && !input.HasVarianceSection)
issues.Add(new("Dashboard sheet lacks trend or variance framing for the supporting sheets.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && input.DetailSheetCount >= 2 && !input.HasSheetSummarySection)
issues.Add(new("Dashboard workbook should summarize each supporting detail sheet.", ArtifactReviewSeverity.Info));
if (input.HasDecisionSection && !input.HasActionSection)
issues.Add(new("Decision summary is present but follow-up actions are limited.", ArtifactReviewSeverity.Info));
if (input.HasDashboardSheet && !input.HasDashboardTileSection && !input.HasHighlightSection)
issues.Add(new("Dashboard sheet would benefit from headline tiles or highlight callouts.", ArtifactReviewSeverity.Info));
return BuildReport("xlsx", strengths, issues);
}
public static bool ContainsBusinessKeyword(string text, params string[] keywords)
{
return ContainsAny(text, keywords);
}
private static ArtifactQualityReport BuildReport(
string artifactType,
List<string> strengths,
List<ArtifactReviewIssue> issues)
{
var score = 72;
score += Math.Min(18, strengths.Count * 4);
score -= issues.Sum(issue => issue.Severity switch
{
ArtifactReviewSeverity.Critical => 12,
ArtifactReviewSeverity.Warning => 6,
_ => 2,
});
score = Math.Clamp(score, 35, 98);
return new ArtifactQualityReport(artifactType, score, strengths, issues);
}
private static int CountPlaceholders(string text)
{
if (string.IsNullOrWhiteSpace(text))
return 0;
var patterns = new[]
{
@"\[(todo|placeholder|fill me)\]",
@"lorem ipsum",
@"\bTBD\b",
};
return patterns.Sum(pattern => Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count);
}
private static bool ContainsAny(string text, IEnumerable<string> keywords)
{
return keywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
}
}