HTML archetype·Excel dashboard·PPT 골든 회귀를 추가 고도화
- HtmlSkill에 board_report와 strategy_brief 구조화 섹션 타입을 추가해 이사회 보고형/전략 요약형 HTML 산출물 표현력을 확장 - ArtifactQualityReviewService HTML 리뷰에 board-report-panel, strategy-brief-panel 인식과 보완 포인트 규칙을 추가 - Excel dashboard sheet에 KPI, highlights, actions를 함께 렌더링해 executive dashboard 시트 밀도를 강화 - PptxSkillGoldenDeckTests에 strategy deck 회귀 샘플을 추가해 strong 전략 덱 품질 기준을 고정 - README.md와 docs/DEVELOPMENT.md에 2026-04-14 23:32 (KST) 기준 이력과 검증 명령을 반영 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_doc_next5\\ -p:IntermediateOutputPath=obj\\verify_doc_next5\\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter ArtifactQualityReviewServiceTests|ExcelSkillDashboardSummaryTests|HtmlSkillConsultingSectionsTests|HtmlSkillPrintFrameTests|DeckQualityReviewServiceTests|PptxSkillGoldenDeckTests|PptxSkillAutoRepairTests|PptxSkillConsultingDeckTests -p:OutputPath=bin\\verify_doc_next5_tests\\ -p:IntermediateOutputPath=obj\\verify_doc_next5_tests\\ : 통과 14
This commit is contained in:
@@ -128,6 +128,8 @@ public static class ArtifactQualityReviewService
|
||||
var matrixCount = Regex.Matches(html, @"matrix-grid|risk-matrix", RegexOptions.IgnoreCase).Count;
|
||||
var decisionCount = Regex.Matches(html, @"decision-summary", 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 placeholderCount = CountPlaceholders(html);
|
||||
|
||||
@@ -136,9 +138,11 @@ public static class ArtifactQualityReviewService
|
||||
if (printReady) strengths.Add("Includes print-ready CSS");
|
||||
if (hasPrintFrame) strengths.Add("Includes print header/footer frame");
|
||||
if (sectionCount >= 5) strengths.Add($"Contains {sectionCount} major sections");
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || decisionCount > 0 || evidenceCardCount > 0 || kpiCount > 0)
|
||||
if (tableCount > 0 || comparisonCount > 0 || roadmapCount > 0 || matrixCount > 0 || decisionCount > 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 (html.Length < 1800)
|
||||
issues.Add(new("Body content may be too short for an executive-quality document.", ArtifactReviewSeverity.Warning));
|
||||
@@ -146,7 +150,7 @@ public static class ArtifactQualityReviewService
|
||||
issues.Add(new("Major section count is low for a business report.", ArtifactReviewSeverity.Warning));
|
||||
if (paragraphCount < sectionCount * 2)
|
||||
issues.Add(new("Several sections may need more supporting paragraphs.", ArtifactReviewSeverity.Warning));
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + decisionCount + evidenceCardCount + kpiCount == 0)
|
||||
if (tableCount + comparisonCount + roadmapCount + matrixCount + decisionCount + 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));
|
||||
@@ -158,6 +162,10 @@ public static class ArtifactQualityReviewService
|
||||
issues.Add(new("Print-ready business report would benefit from decision summary or evidence cards.", ArtifactReviewSeverity.Warning));
|
||||
if (printReady && !hasCover && sectionCount >= 5)
|
||||
issues.Add(new("Print-ready report could benefit from a cover page.", 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 && comparisonCount + roadmapCount == 0)
|
||||
issues.Add(new("Strategy brief would be stronger with a comparison or roadmap block.", ArtifactReviewSeverity.Info));
|
||||
|
||||
return BuildReport("html", strengths, issues);
|
||||
}
|
||||
|
||||
@@ -618,7 +618,9 @@ public class ExcelSkill : IAgentTool
|
||||
AppendDecisionSummarySection(summarySheet, sheetData, merges, ref rowIndex);
|
||||
AppendScorecardSection(summarySheet, sheetData, ref rowIndex);
|
||||
AppendTrendSection(summarySheet, sheetData, ref rowIndex);
|
||||
AppendKpiSection(summarySheet, sheetData, merges, ref rowIndex);
|
||||
AppendSheetSummarySection(summarySheet, sheetData, ref rowIndex);
|
||||
AppendSummaryTextSection(summarySheet, "highlights", "Key Highlights", sheetData, merges, ref rowIndex);
|
||||
AppendSummaryTextSection(summarySheet, "actions", "Priority Actions", sheetData, merges, ref rowIndex);
|
||||
|
||||
if (detailSheetNames.Count > 0)
|
||||
@@ -789,6 +791,35 @@ public class ExcelSkill : IAgentTool
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
private static void AppendKpiSection(JsonElement summarySheet, SheetData sheetData, MergeCells merges, ref uint rowIndex)
|
||||
{
|
||||
if (!summarySheet.SafeTryGetProperty("kpis", out var kpis) || kpis.ValueKind != JsonValueKind.Array || kpis.GetArrayLength() == 0)
|
||||
return;
|
||||
|
||||
AppendMergedTextRow(sheetData, merges, rowIndex++, "A", "F", "Key KPIs", 1);
|
||||
var headerRow = new Row { RowIndex = rowIndex++ };
|
||||
headerRow.Append(CreateSummaryCell("A", headerRow.RowIndex!.Value, "Metric", 1));
|
||||
headerRow.Append(CreateSummaryCell("B", headerRow.RowIndex!.Value, "Value", 1));
|
||||
headerRow.Append(CreateSummaryCell("C", headerRow.RowIndex!.Value, "Trend", 1));
|
||||
headerRow.Append(CreateSummaryCell("D", headerRow.RowIndex!.Value, "Note", 1));
|
||||
sheetData.Append(headerRow);
|
||||
|
||||
var index = 0;
|
||||
foreach (var kpi in kpis.EnumerateArray())
|
||||
{
|
||||
var stripeStyle = index % 2 == 0 ? (uint)0 : (uint)2;
|
||||
var row = new Row { RowIndex = rowIndex++ };
|
||||
row.Append(CreateSummaryCell("A", row.RowIndex!.Value, kpi.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? string.Empty : string.Empty, 1));
|
||||
row.Append(CreateSummaryCell("B", row.RowIndex!.Value, kpi.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? string.Empty : string.Empty, 3));
|
||||
row.Append(CreateSummaryCell("C", row.RowIndex!.Value, kpi.SafeTryGetProperty("trend", out var trendEl) ? trendEl.SafeGetString() ?? string.Empty : string.Empty, stripeStyle));
|
||||
row.Append(CreateSummaryCell("D", row.RowIndex!.Value, kpi.SafeTryGetProperty("note", out var noteEl) ? noteEl.SafeGetString() ?? string.Empty : string.Empty, stripeStyle));
|
||||
sheetData.Append(row);
|
||||
index++;
|
||||
}
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
private static bool HasDashboardSheet(JsonElement summarySheet)
|
||||
{
|
||||
if (summarySheet.ValueKind != JsonValueKind.Object)
|
||||
|
||||
@@ -54,6 +54,8 @@ public class HtmlSkill : IAgentTool
|
||||
"'cards' {items:[{title, body, badge, icon}]}, " +
|
||||
"'decision_summary' {title, decision, rationale, actions:['...']}, " +
|
||||
"'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:['...']}, " +
|
||||
"'comparison' {title, items:[{name, summary, pros, cons, verdict}]}, " +
|
||||
"'roadmap' {title, phases:[{title, detail, timeline, owner}]}, " +
|
||||
"'matrix' {title, quadrants:[{title, items:['...']}]}, " +
|
||||
@@ -357,6 +359,12 @@ public class HtmlSkill : IAgentTool
|
||||
case "evidence_cards":
|
||||
sb.AppendLine(RenderEvidenceCards(section));
|
||||
break;
|
||||
case "board_report":
|
||||
sb.AppendLine(RenderBoardReport(section));
|
||||
break;
|
||||
case "strategy_brief":
|
||||
sb.AppendLine(RenderStrategyBrief(section));
|
||||
break;
|
||||
case "comparison":
|
||||
sb.AppendLine(RenderComparison(section));
|
||||
break;
|
||||
@@ -630,6 +638,111 @@ public class HtmlSkill : IAgentTool
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderBoardReport(JsonElement s)
|
||||
{
|
||||
var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : "Board Report";
|
||||
var decision = s.SafeTryGetProperty("decision", out var decisionEl) ? decisionEl.SafeGetString() : null;
|
||||
var recommendation = s.SafeTryGetProperty("recommendation", out var recommendationEl) ? recommendationEl.SafeGetString() : null;
|
||||
var rationale = s.SafeTryGetProperty("rationale", out var rationaleEl) ? rationaleEl.SafeGetString() : null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<section class=\"board-report-panel\" style=\"margin:1.5rem 0;padding:1.35rem 1.4rem;border:1px solid #dbe4f0;border-radius:18px;background:linear-gradient(180deg,#f8fbff,#ffffff);box-shadow:0 14px 32px rgba(15,23,42,.07);\">");
|
||||
sb.AppendLine($"<div style=\"font-size:1.05rem;font-weight:800;color:#0f172a;margin-bottom:.8rem\">{Escape(title ?? "Board Report")}</div>");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(decision))
|
||||
sb.AppendLine($"<div style=\"padding:.85rem 1rem;border-radius:14px;background:#eef2ff;color:#312e81;font-weight:700;margin-bottom:.75rem\">Decision Ask: {MarkdownToHtml(decision)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(recommendation))
|
||||
sb.AppendLine($"<div style=\"padding:.8rem 1rem;border-radius:14px;background:#ecfdf5;color:#166534;font-weight:700;margin-bottom:.85rem\">Recommendation: {MarkdownToHtml(recommendation)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(rationale))
|
||||
sb.AppendLine($"<div style=\"color:#334155;line-height:1.7;margin-bottom:.95rem\">{MarkdownToHtml(rationale)}</div>");
|
||||
|
||||
if (s.SafeTryGetProperty("metrics", out var metricsEl) && metricsEl.ValueKind == JsonValueKind.Array && metricsEl.GetArrayLength() > 0)
|
||||
{
|
||||
sb.AppendLine("<div style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.85rem;margin:1rem 0;\">");
|
||||
foreach (var metric in metricsEl.EnumerateArray())
|
||||
{
|
||||
var label = metric.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? string.Empty : string.Empty;
|
||||
var value = metric.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? string.Empty : string.Empty;
|
||||
var note = metric.SafeTryGetProperty("note", out var noteEl) ? noteEl.SafeGetString() ?? string.Empty : string.Empty;
|
||||
sb.AppendLine("<div style=\"border:1px solid #e2e8f0;border-radius:14px;padding:.9rem 1rem;background:#fff;\">");
|
||||
sb.AppendLine($"<div style=\"font-size:.78rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em\">{Escape(label)}</div>");
|
||||
sb.AppendLine($"<div style=\"font-size:1.35rem;font-weight:800;color:#0f172a;margin-top:.2rem\">{Escape(value)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(note))
|
||||
sb.AppendLine($"<div style=\"font-size:.82rem;color:#475569;margin-top:.3rem\">{MarkdownToHtml(note)}</div>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
|
||||
if (s.SafeTryGetProperty("risks", out var risksEl) && risksEl.ValueKind == JsonValueKind.Array && risksEl.GetArrayLength() > 0)
|
||||
{
|
||||
sb.AppendLine("<div class=\"board-risk-list\" style=\"margin:1rem 0;\">");
|
||||
sb.AppendLine("<div style=\"font-size:.86rem;font-weight:700;color:#7c2d12;margin-bottom:.35rem\">Key Risks</div>");
|
||||
sb.AppendLine("<ul style=\"margin:0;padding-left:1.2rem;color:#7c2d12;line-height:1.7;\">");
|
||||
foreach (var risk in risksEl.EnumerateArray())
|
||||
sb.AppendLine($"<li>{MarkdownToHtml(risk.SafeGetString() ?? string.Empty)}</li>");
|
||||
sb.AppendLine("</ul>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
|
||||
if (s.SafeTryGetProperty("next_steps", out var stepsEl) && stepsEl.ValueKind == JsonValueKind.Array && stepsEl.GetArrayLength() > 0)
|
||||
{
|
||||
sb.AppendLine("<div style=\"margin-top:1rem;\">");
|
||||
sb.AppendLine("<div style=\"font-size:.86rem;font-weight:700;color:#475569;margin-bottom:.35rem\">Immediate Next Steps</div>");
|
||||
sb.AppendLine("<ul style=\"margin:0;padding-left:1.2rem;color:#334155;line-height:1.7;\">");
|
||||
foreach (var step in stepsEl.EnumerateArray())
|
||||
sb.AppendLine($"<li>{MarkdownToHtml(step.SafeGetString() ?? string.Empty)}</li>");
|
||||
sb.AppendLine("</ul>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</section>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderStrategyBrief(JsonElement s)
|
||||
{
|
||||
var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : "Strategy Brief";
|
||||
var strategicQuestion = s.SafeTryGetProperty("strategic_question", out var questionEl) ? questionEl.SafeGetString() : null;
|
||||
var thesis = s.SafeTryGetProperty("thesis", out var thesisEl) ? thesisEl.SafeGetString() : null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("<section class=\"strategy-brief-panel\" style=\"margin:1.5rem 0;padding:1.3rem 1.4rem;border:1px solid #e2e8f0;border-radius:18px;background:linear-gradient(180deg,#ffffff,#f8fafc);box-shadow:0 12px 28px rgba(15,23,42,.06);\">");
|
||||
sb.AppendLine($"<div style=\"font-size:1.05rem;font-weight:800;color:#0f172a;margin-bottom:.8rem\">{Escape(title ?? "Strategy Brief")}</div>");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(strategicQuestion))
|
||||
sb.AppendLine($"<div style=\"padding:.8rem 1rem;border-radius:14px;background:#fff7ed;color:#9a3412;font-weight:700;margin-bottom:.75rem\">Strategic Question: {MarkdownToHtml(strategicQuestion)}</div>");
|
||||
if (!string.IsNullOrWhiteSpace(thesis))
|
||||
sb.AppendLine($"<div style=\"padding:.85rem 1rem;border-radius:14px;background:#eff6ff;color:#1d4ed8;font-weight:700;margin-bottom:.95rem\">Core Thesis: {MarkdownToHtml(thesis)}</div>");
|
||||
|
||||
if (s.SafeTryGetProperty("implications", out var implicationsEl) && implicationsEl.ValueKind == JsonValueKind.Array && implicationsEl.GetArrayLength() > 0)
|
||||
{
|
||||
sb.AppendLine("<div style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.9rem;margin:1rem 0;\">");
|
||||
foreach (var implication in implicationsEl.EnumerateArray())
|
||||
{
|
||||
sb.AppendLine("<div style=\"border:1px solid #dbe4f0;border-radius:14px;padding:.9rem 1rem;background:#fff;\">");
|
||||
sb.AppendLine($"<div style=\"font-size:.85rem;font-weight:700;color:#475569;margin-bottom:.25rem\">Implication</div>");
|
||||
sb.AppendLine($"<div style=\"color:#334155;line-height:1.65\">{MarkdownToHtml(implication.SafeGetString() ?? string.Empty)}</div>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
|
||||
if (s.SafeTryGetProperty("decisions", out var decisionsEl) && decisionsEl.ValueKind == JsonValueKind.Array && decisionsEl.GetArrayLength() > 0)
|
||||
{
|
||||
sb.AppendLine("<div style=\"margin-top:1rem;\">");
|
||||
sb.AppendLine("<div style=\"font-size:.86rem;font-weight:700;color:#475569;margin-bottom:.35rem\">Decisions to Align</div>");
|
||||
sb.AppendLine("<ol style=\"margin:0;padding-left:1.25rem;color:#334155;line-height:1.7;\">");
|
||||
foreach (var item in decisionsEl.EnumerateArray())
|
||||
sb.AppendLine($"<li>{MarkdownToHtml(item.SafeGetString() ?? string.Empty)}</li>");
|
||||
sb.AppendLine("</ol>");
|
||||
sb.AppendLine("</div>");
|
||||
}
|
||||
|
||||
sb.AppendLine("</section>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string RenderComparison(JsonElement s)
|
||||
{
|
||||
if (!s.SafeTryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array)
|
||||
|
||||
Reference in New Issue
Block a user