문서 대시보드·DOCX 스타일맵·PPT 골든 회귀를 고도화하고 검증 추가

- ExcelSkill에 summary_sheet 기반 Dashboard 시트 생성을 추가해 Summary -> Dashboard -> Detail 시트 흐름을 지원
- ArtifactQualityReviewService workbook 리뷰에 dashboard 존재 여부를 반영하고 multi-sheet workbook 보완 포인트를 강화
- DocumentAssemblerTool style_map 범위를 cover_subtitle/callout/table_header까지 확장해 템플릿 기반 DOCX 조립 품질을 개선
- Excel/DOCX/PPT 회귀 테스트를 확장하고 PptxSkillGoldenDeckTests를 추가해 strong deck 품질 기준을 고정
- README.md와 docs/DEVELOPMENT.md에 2026-04-14 23:25 (KST) 기준 작업 이력과 검증 명령을 반영

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next4\\ -p:IntermediateOutputPath=obj\\verify_doc_next4\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ArtifactQualityReviewServiceTests|DocumentAssemblerStyleMapTests|DocumentAssemblerDocxFeaturesTests|DocumentAssemblerSemanticTests|ExcelSkillDashboardSummaryTests|ExcelSkillSummarySheetTests|ExcelSkillExecutiveSummaryLinkTests|ExcelSkillDataValidationTests|ExcelSkillConditionalFormattingTests|HtmlSkillPrintFrameTests|HtmlSkillConsultingSectionsTests|DeckQualityReviewServiceTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests|PptxSkillGoldenDeckTests -p:OutputPath=bin\\verify_doc_next4_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next4_tests\\ : 통과 20
This commit is contained in:
2026-04-14 23:26:59 +09:00
parent 2e36f2fef1
commit 116c420bf6
9 changed files with 369 additions and 35 deletions

View File

@@ -217,7 +217,8 @@ public class ExcelSkill : IAgentTool
conditionalFormattingCount,
false,
false,
summaryArg.ValueKind == JsonValueKind.Object,
false,
false,
false,
false,
false));
@@ -259,12 +260,17 @@ public class ExcelSkill : IAgentTool
stylesPart.Stylesheet.Save();
var wbSheets = workbookPart.Workbook.AppendChild(new Sheets());
var dashboardSheetName = GetDashboardSheetName(summarySheet);
var hasDashboardSheet = HasDashboardSheet(summarySheet);
var summaryLinkedSheets = hasDashboardSheet
? new List<string> { dashboardSheetName, sheetName }
: [sheetName];
var summaryName = summarySheet.SafeTryGetProperty("name", out var summaryNameEl)
? summaryNameEl.SafeGetString() ?? "Summary"
: "Summary";
var summaryPart = workbookPart.AddNewPart<WorksheetPart>();
WriteExecutiveSummarySheet(summaryPart, summarySheet, [sheetName]);
WriteExecutiveSummarySheet(summaryPart, summarySheet, summaryLinkedSheets);
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(summaryPart),
@@ -272,6 +278,19 @@ public class ExcelSkill : IAgentTool
Name = summaryName,
});
uint nextSheetId = 2;
if (hasDashboardSheet)
{
var dashboardPart = workbookPart.AddNewPart<WorksheetPart>();
WriteDashboardSheet(dashboardPart, summarySheet, [sheetName]);
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(dashboardPart),
SheetId = nextSheetId++,
Name = dashboardSheetName,
});
}
var worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet();
@@ -287,7 +306,7 @@ public class ExcelSkill : IAgentTool
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(worksheetPart),
SheetId = 2,
SheetId = nextSheetId,
Name = sheetName,
});
@@ -299,14 +318,15 @@ public class ExcelSkill : IAgentTool
numFmts.Count > 0, alignments.Count > 0, themeName);
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
summaryName,
2,
hasDashboardSheet ? 3 : 2,
1,
rowCount,
CountFormulaCells(rows),
1,
summaryLinkedSheets.Count,
validationCount,
conditionalFormattingCount,
true,
hasDashboardSheet,
HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"),
@@ -356,6 +376,8 @@ public class ExcelSkill : IAgentTool
var totalValidationCount = 0;
var totalConditionalFormattingCount = 0;
var detailSheetNames = new List<string>();
var dashboardSheetName = GetDashboardSheetName(summarySheet);
var hasDashboardSheet = HasDashboardSheet(summarySheet);
foreach (var sheetDef in sheetsArr.EnumerateArray())
{
@@ -371,13 +393,28 @@ public class ExcelSkill : IAgentTool
? summaryNameEl.SafeGetString() ?? "Summary"
: "Summary";
var summaryPart = workbookPart.AddNewPart<WorksheetPart>();
WriteExecutiveSummarySheet(summaryPart, summarySheet, detailSheetNames);
var summaryLinkedSheets = hasDashboardSheet
? new[] { dashboardSheetName }.Concat(detailSheetNames).ToList()
: detailSheetNames;
WriteExecutiveSummarySheet(summaryPart, summarySheet, summaryLinkedSheets);
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(summaryPart),
SheetId = sheetId++,
Name = summaryName,
});
if (hasDashboardSheet)
{
var dashboardPart = workbookPart.AddNewPart<WorksheetPart>();
WriteDashboardSheet(dashboardPart, summarySheet, detailSheetNames);
wbSheets.Append(new Sheet
{
Id = workbookPart.GetIdOfPart(dashboardPart),
SheetId = sheetId++,
Name = dashboardSheetName,
});
}
}
foreach (var sheetDef in sheetsArr.EnumerateArray())
@@ -425,14 +462,15 @@ public class ExcelSkill : IAgentTool
workbookPart.Workbook.Save();
var review = ArtifactQualityReviewService.ReviewWorkbook(new WorkbookReviewInput(
fullPath,
totalSheets + (summarySheet.ValueKind == JsonValueKind.Object ? 1 : 0),
totalSheets + (summarySheet.ValueKind == JsonValueKind.Object ? 1 : 0) + (hasDashboardSheet ? 1 : 0),
totalSheets,
totalRows,
totalFormulaCount,
summarySheet.ValueKind == JsonValueKind.Object ? detailSheetNames.Count : 0,
summarySheet.ValueKind == JsonValueKind.Object ? detailSheetNames.Count + (hasDashboardSheet ? 1 : 0) : 0,
totalValidationCount,
totalConditionalFormattingCount,
summarySheet.ValueKind == JsonValueKind.Object,
hasDashboardSheet,
HasSummaryItems(summarySheet, "highlights"),
HasSummaryItems(summarySheet, "actions"),
HasSummaryItems(summarySheet, "scorecards") || HasSummaryItems(summarySheet, "cards") || HasSummaryItems(summarySheet, "kpis") || HasSummaryItems(summarySheet, "trend_series"),
@@ -544,6 +582,72 @@ public class ExcelSkill : IAgentTool
}
}
private static void WriteDashboardSheet(WorksheetPart worksheetPart, JsonElement summarySheet, IReadOnlyList<string> detailSheetNames)
{
worksheetPart.Worksheet = new Worksheet();
var columns = new Columns(
new Column { Min = 1, Max = 1, Width = 22, CustomWidth = true },
new Column { Min = 2, Max = 2, Width = 18, CustomWidth = true },
new Column { Min = 3, Max = 3, Width = 18, CustomWidth = true },
new Column { Min = 4, Max = 4, Width = 22, CustomWidth = true },
new Column { Min = 5, Max = 5, Width = 18, CustomWidth = true },
new Column { Min = 6, Max = 6, Width = 30, CustomWidth = true });
worksheetPart.Worksheet.Append(columns);
var sheetData = new SheetData();
worksheetPart.Worksheet.Append(sheetData);
var merges = new MergeCells();
var hyperlinks = new Hyperlinks();
uint rowIndex = 1;
var baseTitle = summarySheet.SafeTryGetProperty("title", out var titleEl)
? titleEl.SafeGetString() ?? "Executive Summary"
: "Executive Summary";
var subtitle = summarySheet.SafeTryGetProperty("subtitle", out var subtitleEl)
? subtitleEl.SafeGetString() ?? string.Empty
: string.Empty;
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", $"{baseTitle} Dashboard", 1);
if (!string.IsNullOrWhiteSpace(subtitle))
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", subtitle, 3);
rowIndex++;
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
AppendTrendSection(summarySheet, sheetData, ref rowIndex);
AppendSheetSummarySection(summarySheet, sheetData, ref rowIndex);
AppendSummaryTextSection(summarySheet, "actions", "Priority Actions", sheetData, merges, ref rowIndex);
if (detailSheetNames.Count > 0)
{
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Linked Detail Sheets", 1);
var startRow = rowIndex;
foreach (var detailSheetName in detailSheetNames)
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", $"Open: {detailSheetName}", 0);
for (var i = 0; i < detailSheetNames.Count; i++)
{
hyperlinks.Append(new Hyperlink
{
Reference = $"A{startRow + (uint)i}",
Location = $"'{detailSheetNames[i]}'!A1",
Display = detailSheetNames[i]
});
}
}
if (merges.HasChildren)
worksheetPart.Worksheet.InsertAfter(merges, sheetData);
if (hyperlinks.HasChildren)
{
var anchor = merges.HasChildren ? (OpenXmlElement)merges : sheetData;
worksheetPart.Worksheet.InsertAfter(hyperlinks, anchor);
}
}
private static void AppendDecisionSummarySection(JsonElement summarySheet, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty("decision_summary", out var decisionSummary))
@@ -685,6 +789,31 @@ public class ExcelSkill : IAgentTool
rowIndex++;
}
private static bool HasDashboardSheet(JsonElement summarySheet)
{
if (summarySheet.ValueKind != JsonValueKind.Object)
return false;
if (summarySheet.SafeTryGetProperty("dashboard_sheet_name", out var dashboardNameEl)
&& dashboardNameEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(dashboardNameEl.SafeGetString()))
return true;
return summarySheet.SafeTryGetProperty("trend_series", out var trendSeries)
&& trendSeries.ValueKind == JsonValueKind.Array
&& trendSeries.GetArrayLength() > 0;
}
private static string GetDashboardSheetName(JsonElement summarySheet)
{
if (summarySheet.SafeTryGetProperty("dashboard_sheet_name", out var dashboardNameEl)
&& dashboardNameEl.ValueKind == JsonValueKind.String
&& !string.IsNullOrWhiteSpace(dashboardNameEl.SafeGetString()))
return dashboardNameEl.SafeGetString()!;
return "Dashboard";
}
private static void AppendSummaryTextSection(JsonElement summarySheet, string key, string heading, SheetData sheetData, MergeCells merges, ref uint rowIndex)
{
if (!summarySheet.SafeTryGetProperty(key, out var items) || items.ValueKind != JsonValueKind.Array || items.GetArrayLength() == 0)