- 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
361 lines
19 KiB
C#
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));
|
|
}
|
|
}
|