diff --git a/README.md b/README.md index 83a1a50..475d32b 100644 --- a/README.md +++ b/README.md @@ -1996,3 +1996,12 @@ MIT License - 검증: - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_final_batch\\ -p:IntermediateOutputPath=obj\\verify_final_batch\\` 경고 0 / 오류 0 - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|ChatStorageServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_final_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_final_batch_tests\\` 통과 54 + +업데이트: 2026-04-15 11:17 (KST) +- SQL 전용 정적 분석 계층을 추가했습니다. 새 [SqlDialectDetector.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/SqlDialectDetector.cs)와 [SqlAnalysisService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/SqlAnalysisService.cs)는 PostgreSQL/MySQL/SQL Server/SQLite/Oracle 방언 추정, statement 분류, object 추출, destructive DDL·broad DML·transaction boundary 위험 감지, SQL 전용 fallback summary 생성을 담당합니다. +- [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)는 SQL용 manifest/build/test/lint 힌트와 workflow summary를 제공하고, no-LSP fallback 경로에서 일반 언어 설명 대신 SQL 분석 요약을 우선 반환하도록 보강했습니다. +- PPT/HTML 문서 품질도 더 올렸습니다. [DeckPlanningService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckPlanningService.cs)는 구조화된 `content` 슬라이드를 `kpi_dashboard`, `comparison`, `roadmap`, `chart`로 자동 승격하고, [DeckQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs>)와 [DeckRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs>)는 executive summary KPI 근거, comparison verdict, roadmap owner/timeline, KPI dashboard takeaway 부족을 별도 경고/보정 가이드로 다룹니다. +- [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs)는 새 `kpi_panel` 섹션을 지원하고, [ArtifactQualityReviewService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs>)와 [ArtifactRepairGuideService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs>)는 board/strategy 문서의 KPI/evidence/decision 연결 부족을 더 정확히 감지합니다. +- 검증: + - `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 diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index cc57e7e..7fb206d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1150,3 +1150,44 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾 - 검증: - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_final_batch\\ -p:IntermediateOutputPath=obj\\verify_final_batch\\` 경고 0 / 오류 0 - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|AgentLoopLlmRequestPreparationServiceTests|AgentLoopIterationPreparationServiceTests|AgentMessageInvariantHelperTests|AgentToolResultBudgetTests|ChatStorageServiceTests|HtmlSkillGoldenReportTests|PptxSkillGoldenDeckTests|DocxSkillGoldenDocumentTests|ExcelSkillGoldenWorkbookTests" -p:OutputPath=bin\\verify_final_batch_tests\\ -p:IntermediateOutputPath=obj\\verify_final_batch_tests\\` 통과 54 + +업데이트: 2026-04-15 11:17 (KST) +- SQL 전용 fallback 분석 추가: + - 새 `SqlDialectDetector.cs` + - PostgreSQL / MySQL / SQL Server / SQLite / Oracle 방언 휴리스틱 감지 + - 새 `SqlAnalysisService.cs` + - statement kind 분류: `CREATE TABLE`, `ALTER TABLE`, `DROP TABLE`, `TRUNCATE TABLE`, `CREATE INDEX`, `CREATE VIEW`, `CREATE FUNCTION`, `CREATE PROCEDURE`, `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `SELECT` + - object 추출: table/view/update/insert/delete/from/join 기반 상위 object 수집 + - 위험 감지: `DROP`, `TRUNCATE`, `ALTER TABLE DROP COLUMN`, `DELETE/UPDATE without WHERE`, `SELECT *`, transaction boundary 부재 + - SQL 전용 fallback summary 생성 +- 개발언어 카탈로그 확장: + - `CodeLanguageCatalog.cs` + - SQL manifest 힌트: `migrations/*.sql`, `schema.sql`, `seed.sql`, `*.sqlproj` + - SQL build/test/lint 힌트 강화 + - `BuildWorkflowSummary()`에 SQL 전용 `dialect/statement/risk/object dependency` 분석 요약 추가 + - `BuildFallbackSummary()`에서 SQL은 일반 fallback 대신 `SqlAnalysisService.BuildFallbackSummary()`를 사용 +- PPT/HTML 고도화: + - `DeckPlanningService.cs` + - 구조화된 `content` 슬라이드를 입력 데이터에 따라 `kpi_dashboard`, `comparison`, `roadmap`, `chart`로 자동 승격 + - `DeckQualityReviewService.cs` + - Executive Summary의 정량 근거 부족, comparison verdict 부재, roadmap owner/timeline 부족, KPI dashboard metric/takeaway 부족 진단 추가 + - `DeckRepairGuideService.cs` + - 위 PPT 진단 항목을 실제 보정 액션으로 매핑 + - `HtmlSkill.cs` + - 새 `kpi_panel` 섹션 타입 추가 + - `ArtifactQualityReviewService.cs` + - KPI panel을 major/supporting block 판단에 반영 + - board/strategy 문서에서 KPI/evidence/decision 연결 부족 이슈 추가 + - `ArtifactRepairGuideService.cs` + - KPI panel/metric strip, evidence support, decision/next-step 연결 보정 가이드 추가 +- 테스트: + - 새 `SqlDialectDetectorTests.cs` + - 새 `SqlAnalysisServiceTests.cs` + - `CodeLanguageCatalogTests.cs` + - `DeckPlanningServiceTests.cs` + - `ArtifactQualityReviewServiceTests.cs` + - `ArtifactRepairGuideServiceTests.cs` + - `HtmlSkillConsultingSectionsTests.cs` +- 검증: + - `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 diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index df3da7e..5187148 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -196,3 +196,10 @@ 4. 명령/스킬 합성 및 릴리즈 게이트 업데이트: 2026-04-15 10:57 (KST) - Agent loop 마감 작업으로 `도구 미호출 복구` 규칙을 서비스화했습니다. 남은 우선순위는 `iteration/tool dispatch 추가 분리`, `장기 세션 replacement state 완전 고정`, `문서 golden fixture 확대`, `릴리즈 체크리스트 닫기` 정도의 마감 품질 중심입니다. + +업데이트: 2026-04-15 11:17 (KST) + +### 추가 진행 메모 +1. SQL은 별도 정적 분석 계층까지 올라왔습니다. 현재도 no-LSP 환경에서 `dialect/statement/risk/next checks`를 직접 설명할 수 있고, 다음 단계로 더 간다면 dialect별 migration lint나 schema dependency graph 정도가 후보입니다. +2. PPT는 구조화된 슬라이드를 더 적극적으로 `comparison/roadmap/kpi_dashboard/chart`로 승격하고, KPI/evidence/verdict/owner 같은 컨설팅형 품질 기준을 slide critic에 반영했습니다. 큰 기능보다 golden fixture 확대 성격의 마감 작업이 남아 있습니다. +3. HTML은 `kpi_panel`이 새 핵심 블록으로 들어오면서 board/strategy 문서의 decision/evidence/KPI 연결성이 더 중요해졌습니다. 이후 작업은 목적형 bundled skill 확장과 print/export polish 쪽이 중심입니다. diff --git a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs index 0d97128..bcb2bc8 100644 --- a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs +++ b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs @@ -15,6 +15,7 @@ public class ArtifactQualityReviewServiceTests
Current state detail.
Evidence paragraph.
| Metric | Value |
|---|---|
| NPS | 61 |
Summary text.
Supporting paragraph.
+ + + + + +Recommendation text.
More detail.
+Reference detail.
More detail.
+ """; + + var review = ArtifactQualityReviewService.ReviewHtml("KPI Gap", html, hasCover: true, hasTableOfContents: true, printReady: true); + + review.Issues.Should().Contain(issue => issue.Message.Contains("KPI panel or metric strip", StringComparison.OrdinalIgnoreCase)); + review.Issues.Should().Contain(issue => issue.Message.Contains("KPI or evidence support blocks", StringComparison.OrdinalIgnoreCase)); + } + [Fact] public void ReviewStructuredDocument_ShouldRecommendEvidenceTableAndCallouts_ForLongBusinessDoc() { diff --git a/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs index 24b7cb0..9c108b6 100644 --- a/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs +++ b/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs @@ -36,12 +36,14 @@ public class ArtifactRepairGuideServiceTests ["Includes print-ready CSS"], [ new ArtifactReviewIssue("Board-ready report should include a decision summary block.", ArtifactReviewSeverity.Warning), + new ArtifactReviewIssue("Board-ready report would be stronger with a KPI panel or metric strip.", ArtifactReviewSeverity.Info), new ArtifactReviewIssue("Strategy brief should include explicit decisions or a decision summary block.", ArtifactReviewSeverity.Warning) ]); var guide = ArtifactRepairGuideService.BuildGuide(review); guide.Should().Contain("decision summary"); + guide.Should().Contain("KPI"); guide.Should().Contain("strategy brief"); } diff --git a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs index 19c7bc1..62f2df7 100644 --- a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs +++ b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs @@ -77,6 +77,7 @@ public class CodeLanguageCatalogTests CodeLanguageCatalog.GetBuildHints("go").Should().Contain("go build ./..."); CodeLanguageCatalog.GetTestHints("kotlin").Should().Contain("./gradlew test"); CodeLanguageCatalog.GetLintHints("javascript").Should().Contain("eslint ."); + CodeLanguageCatalog.GetManifestHints("sql").Should().Contain("schema.sql"); } [Fact] @@ -92,4 +93,15 @@ public class CodeLanguageCatalogTests summaries.Should().Contain(summary => summary.StartsWith("Go:")); summaries.Should().Contain(summary => summary.StartsWith("Rust:")); } + + [Fact] + public void BuildWorkflowSummary_ShouldExposeSqlSpecificWorkflowHints() + { + var summary = CodeLanguageCatalog.BuildWorkflowSummary("sql"); + + summary.Should().Contain("SQL"); + summary.Should().Contain("schema.sql"); + summary.Should().Contain("disposable database"); + summary.Should().Contain("dialect"); + } } diff --git a/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs b/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs index c43d167..6942020 100644 --- a/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs +++ b/src/AxCopilot.Tests/Services/DeckPlanningServiceTests.cs @@ -51,4 +51,51 @@ public class DeckPlanningServiceTests result.AutoRepairCount.Should().BeGreaterThan(0); result.Storyline.Should().NotBeEmpty(); } + + [Fact] + public void Prepare_ShouldPromoteStructuredContentSlides_ToBetterLayouts() + { + using var args = JsonDocument.Parse( + """ + { + "title": "Operating Review", + "slides": [ + { + "layout": "content", + "title": "KPI Snapshot", + "kpis": [ + { "label": "Margin", "value": "+4.2pt" }, + { "label": "Lead Time", "value": "-18%" }, + { "label": "Retention", "value": "92%" } + ] + }, + { + "layout": "content", + "title": "Options", + "options": [ + { "name": "Pilot", "pros": "Low risk", "cons": "Slow", "verdict": "Too cautious" }, + { "name": "Phased", "pros": "Balanced", "cons": "Needs PMO", "verdict": "Recommended" } + ] + }, + { + "layout": "content", + "title": "Execution", + "phases": [ + { "title": "Design", "detail": "Scope and governance" }, + { "title": "Launch", "detail": "Rollout wave" } + ] + } + ] + } + """); + + using var result = DeckPlanningService.Prepare(args.RootElement, args.RootElement.GetProperty("slides"), "Operating Review"); + var layouts = result.Slides.EnumerateArray() + .Select(slide => slide.GetProperty("layout").GetString()) + .ToList(); + + layouts.Should().Contain("kpi_dashboard"); + layouts.Should().Contain("comparison"); + layouts.Should().Contain("roadmap"); + } } diff --git a/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs b/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs index 4718280..8937944 100644 --- a/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs +++ b/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs @@ -60,6 +60,16 @@ public class HtmlSkillConsultingSectionsTests "rationale": "Retention and margin improved in the pilot region.", "actions": ["Approve budget", "Launch phase 2"] }, + { + "type": "kpi_panel", + "title": "KPI Panel", + "headline": "Pilot economics remain above threshold", + "items": [ + { "label": "Margin", "value": "+4.2pt", "trend": "up", "note": "pilot" }, + { "label": "Lead Time", "value": "-18%", "trend": "down", "note": "handoff" } + ], + "takeaway": "The pilot shows both service and margin improvement." + }, { "type": "evidence_cards", "title": "Evidence", @@ -101,6 +111,7 @@ public class HtmlSkillConsultingSectionsTests html.Should().Contain("roadmap-block"); html.Should().Contain("matrix-grid"); html.Should().Contain("decision-summary"); + html.Should().Contain("kpi-panel"); html.Should().Contain("evidence-cards"); html.Should().Contain("board-report-panel"); html.Should().Contain("strategy-brief-panel"); diff --git a/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs b/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs new file mode 100644 index 0000000..0d1bf91 --- /dev/null +++ b/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs @@ -0,0 +1,58 @@ +using System.IO; +using AxCopilot.Services; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class SqlAnalysisServiceTests +{ + [Fact] + public void Analyze_ShouldSurfaceStatementKindsObjectsAndRisks() + { + const string sql = + """ + BEGIN TRANSACTION; + CREATE TABLE orders (id SERIAL PRIMARY KEY, customer_id INT); + UPDATE orders SET customer_id = 0; + DROP TABLE legacy_orders; + COMMIT; + """; + + var report = SqlAnalysisService.Analyze(sql, "20260415_orders_migration.sql"); + + report.Dialect.Should().Be("PostgreSQL"); + report.StatementKinds.Should().Contain("CREATE TABLE"); + report.StatementKinds.Should().Contain("UPDATE"); + report.StatementKinds.Should().Contain("DROP TABLE"); + report.Objects.Should().Contain("orders"); + report.Objects.Should().Contain("legacy_orders"); + report.Risks.Should().Contain(risk => risk.Contains("UPDATE statement without WHERE", StringComparison.OrdinalIgnoreCase)); + report.Risks.Should().Contain(risk => risk.Contains("Destructive DROP", StringComparison.OrdinalIgnoreCase)); + report.SuggestedChecks.Should().Contain(check => check.Contains("rollback", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void BuildFallbackSummary_ShouldReadSqlFileAndReturnSqlSpecificSummary() + { + var path = Path.GetTempFileName() + ".sql"; + + try + { + File.WriteAllText(path, "CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT); DELETE FROM users;"); + + var summary = SqlAnalysisService.BuildFallbackSummary(path); + + summary.Should().Contain("SQL static analysis"); + summary.Should().Contain("PostgreSQL"); + summary.Should().Contain("CREATE TABLE"); + summary.Should().Contain("DELETE"); + summary.Should().Contain("users"); + } + finally + { + if (File.Exists(path)) + File.Delete(path); + } + } +} diff --git a/src/AxCopilot.Tests/Services/SqlDialectDetectorTests.cs b/src/AxCopilot.Tests/Services/SqlDialectDetectorTests.cs new file mode 100644 index 0000000..c333867 --- /dev/null +++ b/src/AxCopilot.Tests/Services/SqlDialectDetectorTests.cs @@ -0,0 +1,18 @@ +using AxCopilot.Services; +using FluentAssertions; +using Xunit; + +namespace AxCopilot.Tests.Services; + +public class SqlDialectDetectorTests +{ + [Theory] + [InlineData("CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);", "PostgreSQL")] + [InlineData("CREATE TABLE [dbo].[Users] (Id INT IDENTITY(1,1), Name NVARCHAR(100)); GO", "SQL Server")] + [InlineData("CREATE TABLE `users` (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100)) ENGINE=InnoDB;", "MySQL")] + [InlineData("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); PRAGMA foreign_keys=ON;", "SQLite")] + public void DetectDialect_ShouldRecognizeCommonDialects(string sql, string expected) + { + SqlDialectDetector.DetectDialect(sql).Should().Be(expected); + } +} diff --git a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs index 4de8368..5030c21 100644 --- a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs +++ b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs @@ -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); } diff --git a/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs b/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs index f07908f..3c42bcc 100644 --- a/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs +++ b/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs @@ -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)) diff --git a/src/AxCopilot/Services/Agent/DeckPlanningService.cs b/src/AxCopilot/Services/Agent/DeckPlanningService.cs index f41dcde..dd7072e 100644 --- a/src/AxCopilot/Services/Agent/DeckPlanningService.cs +++ b/src/AxCopilot/Services/Agent/DeckPlanningService.cs @@ -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) diff --git a/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs b/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs index 10d5af8..c85a5f0 100644 --- a/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs +++ b/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs @@ -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; diff --git a/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs b/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs index 77c3f73..a69a7e9 100644 --- a/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs +++ b/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs @@ -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) diff --git a/src/AxCopilot/Services/Agent/HtmlSkill.cs b/src/AxCopilot/Services/Agent/HtmlSkill.cs index 49daced..0aa0d28 100644 --- a/src/AxCopilot/Services/Agent/HtmlSkill.cs +++ b/src/AxCopilot/Services/Agent/HtmlSkill.cs @@ -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("