diff --git a/README.md b/README.md
index 475d32b..2425008 100644
--- a/README.md
+++ b/README.md
@@ -2005,3 +2005,20 @@ MIT License
- 검증:
- `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
+
+업데이트: 2026-04-15 11:36 (KST)
+- SQL 정적 분석을 한 단계 더 깊게 만들었습니다. [SqlAnalysisService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/SqlAnalysisService.cs)는 이제 dialect/statement/risk뿐 아니라 `script intent`, `dependency`, `review focus`를 함께 계산해 migration, seed, reporting query, generic script를 구분하고 rollback·transaction·의존 객체 확인 포인트까지 fallback summary에 포함합니다.
+- 코드 탭과 워크스페이스 문맥도 SQL 전용 관점을 더 강하게 반영합니다. [CodeLanguageCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/CodeLanguageCatalog.cs)는 SQL workflow summary에 `script intent`, `migration order`, `dependency` 힌트를 포함하고, [WorkspaceContextGenerator.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs)는 SQL 저장소에서 `## SQL Review Focus` 섹션을 생성해 destructive DDL, broad DML, transaction scope, rollback 점검을 바로 안내합니다.
+- HTML 문서는 의사결정형 구조를 더 직접적으로 표현할 수 있게 됐습니다. [HtmlSkill.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/HtmlSkill.cs)는 `decision_matrix`, `metric_strip` 섹션을 새로 지원하고, [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 문서에서 trade-off 설명과 KPI 요약 연결이 약할 때 decision matrix나 recommendation 연결을 구체적으로 제안합니다.
+- PPT deck critic도 더 날카로워졌습니다. [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 headline, comparison trade-off, roadmap phase milestone, chart takeaway, KPI trend/note context 부족을 추가로 감지하고 바로 보정 문장으로 돌려줍니다.
+- 테스트:
+ - [SqlAnalysisServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs)
+ - [CodeLanguageCatalogTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs)
+ - [WorkspaceContextGeneratorTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs)
+ - [HtmlSkillConsultingSectionsTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs)
+ - [ArtifactQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs)
+ - [ArtifactRepairGuideServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs)
+ - [DeckQualityReviewServiceTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs)
+- 검증:
+ - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_sql_doc\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc\\` 경고 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_tests\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc_tests\\` 통과 62
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 7fb206d..b7049e5 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -1191,3 +1191,59 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
- 검증:
- `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
+
+업데이트: 2026-04-15 11:36 (KST)
+
+### SQL fallback 분석 심화
+- `SqlAnalysisService.cs`
+ - `SqlAnalysisReport`에 `ScriptIntent`, `Dependencies`, `ReviewNotes`를 추가했습니다.
+ - `Analyze()`가 dialect/statement/risk/object 외에 `schema migration`, `schema change`, `seed / reference data`, `query / reporting` intent를 계산합니다.
+ - dependency 감지는 script가 직접 생성/수정하는 owned object와 참조 dependency를 분리해, view/reporting query가 의존하는 테이블과 lookup source를 별도로 잡습니다.
+ - review note는 rollback, dependency impact, transaction scope, dialect-specific validation 포인트를 함께 생성합니다.
+ - fallback summary는 `script`, `dependencies`, `review focus`까지 포함하도록 확장했습니다.
+- `CodeLanguageCatalog.cs`
+ - SQL workflow summary를 `dialect/statement/risk/object dependency` 수준에서 `script intent/migration order/dependency`까지 보이도록 강화했습니다.
+- `WorkspaceContextGenerator.cs`
+ - SQL 저장소에서 `## SQL Review Focus` 섹션을 생성하도록 확장했습니다.
+ - migration/seed/reporting query 분류, destructive DDL·broad DML·transaction scope·rollback 점검을 문맥에 직접 넣습니다.
+
+### HTML 의사결정형 문서 강화
+- `HtmlSkill.cs`
+ - 새 섹션 타입 `decision_matrix`, `metric_strip`를 추가했습니다.
+ - `decision_matrix`는 option/criteria/verdict/notes를 한 표로 렌더링합니다.
+ - `metric_strip`은 KPI headline, trend, note를 가로 카드형 요약으로 렌더링합니다.
+- `ArtifactQualityReviewService.cs`
+ - HTML 품질 평가에 `decision_matrix`, `metric_strip` 존재를 반영합니다.
+ - board report에서 comparison/decision matrix 부재, strategy brief에서 trade-off matrix 부재, metric strip이 recommendation과 연결되지 않는 경우를 별도 이슈로 판정합니다.
+- `ArtifactRepairGuideService.cs`
+ - decision matrix 추가, comparison/decision block 보강, metric strip을 recommendation/board summary와 연결하는 수리 가이드를 추가했습니다.
+ - HTML repair action 상한을 4개로 넓혀 품질 피드백 손실을 줄였습니다.
+
+### PPT 슬라이드 품질 critic 세분화
+- `DeckQualityReviewService.cs`
+ - Executive Summary의 headline 선명도 부족을 더 엄격히 감지합니다.
+ - Comparison slide의 trade-off 설명 부족, roadmap phase milestone 부족, chart takeaway 부재, KPI dashboard trend/note context 부족을 별도 경고로 판정합니다.
+ - executive/recommendation/comparison(or chart)/roadmap가 갖춰진 deck에 `decision-ready consulting storyline` 강점 신호를 추가했습니다.
+- `DeckRepairGuideService.cs`
+ - 위 추가 이슈를 각각 headline 압축, trade-off 보강, phase milestone 명시, chart takeaway 추가, KPI trend/note 보강 액션으로 변환합니다.
+
+### 테스트
+- `SqlAnalysisServiceTests.cs`
+ - migration intent/dependency/rollback note 검증 추가
+ - seed/reference data intent, transaction risk, lookup dependency 검증 추가
+- `CodeLanguageCatalogTests.cs`
+ - SQL workflow summary에 `migration order`, `dependencies`가 포함되는지 검증
+- `WorkspaceContextGeneratorTests.cs`
+ - SQL 저장소에서 `## SQL Review Focus` 섹션 생성 검증
+- `HtmlSkillConsultingSectionsTests.cs`
+ - `decision_matrix`, `metric_strip` 렌더링 회귀 추가
+- `ArtifactQualityReviewServiceTests.cs`
+ - decision matrix를 강점으로 인식하고, trade-off view 부재 시 별도 경고를 반환하는지 검증
+- `ArtifactRepairGuideServiceTests.cs`
+ - HTML repair guide에 decision matrix 보강 액션이 포함되는지 검증
+- `DeckQualityReviewServiceTests.cs`
+ - headline/trade-off/KPI context 추가 경고 회귀 검증
+
+### 검증
+- `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_sql_doc\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc\\` 경고 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_tests\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc_tests\\` 통과 62
diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md
index 5187148..3926459 100644
--- a/docs/NEXT_ROADMAP.md
+++ b/docs/NEXT_ROADMAP.md
@@ -203,3 +203,10 @@
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 쪽이 중심입니다.
+
+업데이트: 2026-04-15 11:36 (KST)
+
+### 추가 진행 메모
+1. SQL fallback은 `dialect/statement/risk` 수준을 넘어 `script intent/dependency/review focus`까지 확장됐습니다. 다음 선택지는 dialect별 migration lint, schema dependency graph, rollback 시뮬레이션처럼 더 깊은 전용 리뷰 계층입니다.
+2. HTML 문서는 `decision_matrix`와 `metric_strip`이 들어오면서 board/strategy 보고서의 의사결정 구조를 더 직접적으로 표현할 수 있게 됐습니다. 남은 작업은 bundled skill을 목적형으로 더 쪼개고 print/export polish를 마감하는 수준입니다.
+3. PPT critic은 headline, trade-off, phase milestone, chart takeaway, KPI context를 세밀하게 보기 시작했습니다. 이후 남는 작업은 finance/sales/board fixture 확대와 slide-level auto-repair 정교화처럼 golden 마감 중심입니다.
diff --git a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs
index bcb2bc8..b1c2c06 100644
--- a/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs
+++ b/src/AxCopilot.Tests/Services/ArtifactQualityReviewServiceTests.cs
@@ -16,7 +16,9 @@ public class ArtifactQualityReviewServiceTests
Important message
+
+
@@ -32,6 +34,7 @@ public class ArtifactQualityReviewServiceTests
strength.Contains("cover", StringComparison.OrdinalIgnoreCase) ||
strength.Contains("contents", StringComparison.OrdinalIgnoreCase));
review.Strengths.Should().Contain(strength => strength.Contains("board-ready", StringComparison.OrdinalIgnoreCase));
+ review.Strengths.Should().Contain(strength => strength.Contains("decision matrix", StringComparison.OrdinalIgnoreCase));
review.Issues.Should().NotContain(issue => issue.Severity == ArtifactReviewSeverity.Critical);
}
@@ -183,4 +186,24 @@ public class ArtifactQualityReviewServiceTests
review.Issues.Should().Contain(issue => issue.Message.Contains("evidence table", StringComparison.OrdinalIgnoreCase));
review.Issues.Should().Contain(issue => issue.Message.Contains("callout or highlight blocks", StringComparison.OrdinalIgnoreCase));
}
+
+ [Fact]
+ public void ReviewHtml_ShouldRecommendDecisionMatrix_WhenTradeoffViewIsMissing()
+ {
+ var html =
+ """
+ Executive Summary
Summary text.
Supporting paragraph.
+
+
+
+
+ Recommendation
Recommendation text.
More detail.
+ Appendix
Reference detail.
More detail.
+ """;
+
+ var review = ArtifactQualityReviewService.ReviewHtml("Tradeoff Gap", html, hasCover: true, hasTableOfContents: true, printReady: true);
+
+ review.Issues.Should().Contain(issue => issue.Message.Contains("comparison or decision matrix", StringComparison.OrdinalIgnoreCase));
+ review.Issues.Should().Contain(issue => issue.Message.Contains("decision matrix", StringComparison.OrdinalIgnoreCase));
+ }
}
diff --git a/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs b/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs
index 9c108b6..7e9b39e 100644
--- a/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs
+++ b/src/AxCopilot.Tests/Services/ArtifactRepairGuideServiceTests.cs
@@ -37,7 +37,8 @@ public class ArtifactRepairGuideServiceTests
[
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)
+ new ArtifactReviewIssue("Strategy brief should include explicit decisions or a decision summary block.", ArtifactReviewSeverity.Warning),
+ new ArtifactReviewIssue("Board-ready report would benefit from a comparison or decision matrix block.", ArtifactReviewSeverity.Info)
]);
var guide = ArtifactRepairGuideService.BuildGuide(review);
@@ -45,6 +46,7 @@ public class ArtifactRepairGuideServiceTests
guide.Should().Contain("decision summary");
guide.Should().Contain("KPI");
guide.Should().Contain("strategy brief");
+ guide.Should().Contain("decision matrix");
}
[Fact]
diff --git a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs
index 62f2df7..496e8a0 100644
--- a/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs
+++ b/src/AxCopilot.Tests/Services/CodeLanguageCatalogTests.cs
@@ -103,5 +103,7 @@ public class CodeLanguageCatalogTests
summary.Should().Contain("schema.sql");
summary.Should().Contain("disposable database");
summary.Should().Contain("dialect");
+ summary.Should().Contain("migration order");
+ summary.Should().Contain("dependencies");
}
}
diff --git a/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs b/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs
index 0b85673..ee328b6 100644
--- a/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs
+++ b/src/AxCopilot.Tests/Services/DeckQualityReviewServiceTests.cs
@@ -125,4 +125,46 @@ public class DeckQualityReviewServiceTests
review.Issues.Should().Contain(issue => issue.Message.Contains("supporting rationale or next steps", StringComparison.OrdinalIgnoreCase));
}
+
+ [Fact]
+ public void ReviewDeck_ShouldFlagComparisonTradeoffsAndKpiContext()
+ {
+ using var slides = JsonDocument.Parse(
+ """
+ [
+ { "layout": "title", "title": "Board Review" },
+ {
+ "layout": "executive_summary",
+ "title": "Executive Summary",
+ "summary_points": ["A", "B"],
+ "recommendation": "Proceed"
+ },
+ {
+ "layout": "comparison",
+ "title": "Options",
+ "headline": "Choose a path",
+ "options": [
+ { "name": "A", "verdict": "Recommended" },
+ { "name": "B", "verdict": "Fallback" }
+ ]
+ },
+ {
+ "layout": "kpi_dashboard",
+ "title": "KPIs",
+ "headline": "Performance snapshot",
+ "kpis": [
+ { "label": "Margin", "value": "+2.4pt" },
+ { "label": "Lead Time", "value": "-12%" },
+ { "label": "Quality", "value": "96%" }
+ ]
+ }
+ ]
+ """);
+
+ var review = DeckQualityReviewService.ReviewDeck("Thin Signals", slides.RootElement, hasTemplate: false, autoRepairCount: 0);
+
+ review.Issues.Should().Contain(issue => issue.Message.Contains("sharper single-message headline", StringComparison.OrdinalIgnoreCase));
+ review.Issues.Should().Contain(issue => issue.Message.Contains("trade-offs behind the verdict", StringComparison.OrdinalIgnoreCase));
+ review.Issues.Should().Contain(issue => issue.Message.Contains("trend or note context", StringComparison.OrdinalIgnoreCase));
+ }
}
diff --git a/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs b/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs
index 8937944..535dfa1 100644
--- a/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs
+++ b/src/AxCopilot.Tests/Services/HtmlSkillConsultingSectionsTests.cs
@@ -38,6 +38,23 @@ public class HtmlSkillConsultingSectionsTests
{ "name": "Option A", "summary": "Fast rollout", "pros": "Low setup effort", "cons": "Limited scale", "verdict": "Fast" }
]
},
+ {
+ "type": "decision_matrix",
+ "title": "Decision Matrix",
+ "criteria": ["Speed", "Risk", "Margin"],
+ "options": [
+ { "name": "Option A", "verdict": "Fast", "notes": "Low setup effort", "scores": { "Speed": 5, "Risk": 2, "Margin": 3 } },
+ { "name": "Option B", "verdict": "Recommended", "notes": "Balanced path", "scores": { "Speed": 4, "Risk": 4, "Margin": 4 } }
+ ]
+ },
+ {
+ "type": "metric_strip",
+ "title": "Top-Line Metrics",
+ "items": [
+ { "label": "Margin", "value": "+4.2pt", "trend": "up", "note": "pilot mix" },
+ { "label": "Lead Time", "value": "-18%", "trend": "down", "note": "handoff reduction" }
+ ]
+ },
{
"type": "roadmap",
"title": "Delivery Roadmap",
@@ -108,6 +125,8 @@ public class HtmlSkillConsultingSectionsTests
result.Output.Should().Contain("Repair guide:");
var html = File.ReadAllText(Path.Combine(workDir, "consulting.html"));
html.Should().Contain("comparison-grid");
+ html.Should().Contain("decision-matrix");
+ html.Should().Contain("metric-strip");
html.Should().Contain("roadmap-block");
html.Should().Contain("matrix-grid");
html.Should().Contain("decision-summary");
diff --git a/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs b/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs
index 0d1bf91..7b17365 100644
--- a/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs
+++ b/src/AxCopilot.Tests/Services/SqlAnalysisServiceTests.cs
@@ -14,6 +14,7 @@ public class SqlAnalysisServiceTests
"""
BEGIN TRANSACTION;
CREATE TABLE orders (id SERIAL PRIMARY KEY, customer_id INT);
+ CREATE VIEW order_summary AS SELECT customer_id FROM customer_dim;
UPDATE orders SET customer_id = 0;
DROP TABLE legacy_orders;
COMMIT;
@@ -22,14 +23,17 @@ public class SqlAnalysisServiceTests
var report = SqlAnalysisService.Analyze(sql, "20260415_orders_migration.sql");
report.Dialect.Should().Be("PostgreSQL");
+ report.ScriptIntent.Should().Be("schema migration");
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.Dependencies.Should().Contain("customer_dim");
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));
+ report.ReviewNotes.Should().Contain(note => note.Contains("rollback", StringComparison.OrdinalIgnoreCase));
}
[Fact]
@@ -48,6 +52,8 @@ public class SqlAnalysisServiceTests
summary.Should().Contain("CREATE TABLE");
summary.Should().Contain("DELETE");
summary.Should().Contain("users");
+ summary.Should().Contain("script:");
+ summary.Should().Contain("review focus:");
}
finally
{
@@ -55,4 +61,24 @@ public class SqlAnalysisServiceTests
File.Delete(path);
}
}
+
+ [Fact]
+ public void Analyze_ShouldDetectSeedIntentDependenciesAndTransactionRisk()
+ {
+ const string sql =
+ """
+ INSERT INTO seed_status (code, label) VALUES ('ACTIVE', 'Active');
+ UPDATE users
+ SET status_code = map.code
+ FROM account_status_map map
+ WHERE users.status_code IS NULL;
+ """;
+
+ var report = SqlAnalysisService.Analyze(sql, "20260415_seed_status.sql");
+
+ report.ScriptIntent.Should().Be("seed / reference data");
+ report.Dependencies.Should().Contain("account_status_map");
+ report.ReviewNotes.Should().Contain(note => note.Contains("idempotent", StringComparison.OrdinalIgnoreCase));
+ report.Risks.Should().Contain(risk => risk.Contains("transaction", StringComparison.OrdinalIgnoreCase));
+ }
}
diff --git a/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs b/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs
index 0a1a465..b5b67d0 100644
--- a/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs
+++ b/src/AxCopilot.Tests/Services/WorkspaceContextGeneratorTests.cs
@@ -339,6 +339,29 @@ public class WorkspaceContextGeneratorTests
}
}
+ [Fact]
+ public async Task GenerateAsync_IncludesSqlReviewFocus_WhenWorkspaceContainsSqlScripts()
+ {
+ var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(tempDir);
+ try
+ {
+ File.WriteAllText(Path.Combine(tempDir, "001_init.sql"), "CREATE TABLE users (id INT);");
+ File.WriteAllText(Path.Combine(tempDir, "002_seed.sql"), "INSERT INTO users (id) VALUES (1);");
+ File.WriteAllText(Path.Combine(tempDir, "003_view.sql"), "CREATE VIEW active_users AS SELECT * FROM users;");
+
+ var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
+
+ result.Should().Contain("## SQL Review Focus");
+ result.Should().Contain("migration, seed, reporting query");
+ result.Should().Contain("rollback");
+ }
+ finally
+ {
+ Directory.Delete(tempDir, recursive: true);
+ }
+ }
+
[Fact]
public void DetectLanguageWorkflowHints_ShouldPreferSelectedLanguage()
{
diff --git a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
index 5030c21..5ab7c3e 100644
--- a/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
+++ b/src/AxCopilot/Services/Agent/ArtifactQualityReviewService.cs
@@ -127,6 +127,8 @@ public static class ArtifactQualityReviewService
var tableCount = Regex.Matches(html, @" 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)
@@ -149,6 +153,8 @@ public static class ArtifactQualityReviewService
+ tableCount
+ calloutCount
+ comparisonCount
+ + decisionMatrixCount
+ + metricStripCount
+ roadmapCount
+ matrixCount
+ decisionCount
@@ -164,12 +170,14 @@ 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 || kpiPanelCount > 0 || evidenceCardCount > 0 || boardPanelCount > 0 || strategyBriefCount > 0 || kpiCount > 0)
+ 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));
@@ -177,7 +185,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 + kpiPanelCount + evidenceCardCount + boardPanelCount + strategyBriefCount + kpiCount == 0)
+ 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));
@@ -195,14 +203,20 @@ public static class ArtifactQualityReviewService
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);
}
diff --git a/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs b/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs
index 3c42bcc..bebd246 100644
--- a/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs
+++ b/src/AxCopilot/Services/Agent/ArtifactRepairGuideService.cs
@@ -22,7 +22,7 @@ public static class ArtifactRepairGuideService
if (!string.IsNullOrWhiteSpace(action) && !actions.Contains(action, StringComparer.OrdinalIgnoreCase))
actions.Add(action);
- if (actions.Count >= 3)
+ if (actions.Count >= 4)
break;
}
@@ -40,10 +40,14 @@ public static class ArtifactRepairGuideService
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("comparison or decision matrix", StringComparison.OrdinalIgnoreCase))
+ return "Add a comparison or decision matrix block so the board pack shows trade-offs before the ask";
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("decision matrix", StringComparison.OrdinalIgnoreCase))
+ return "Add a decision matrix so the strategy brief makes option trade-offs explicit";
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))
@@ -58,6 +62,8 @@ public static class ArtifactRepairGuideService
return "Use comparison, roadmap, matrix, KPI, or evidence blocks instead of plain text only";
if (message.Contains("Executive summary", StringComparison.OrdinalIgnoreCase))
return "Add an Executive Summary section near the top of the report";
+ if (message.Contains("Metric strip should connect", StringComparison.OrdinalIgnoreCase))
+ return "Connect the metric strip to an explicit recommendation, next step, or board summary block";
return null;
}
diff --git a/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs b/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs
index c85a5f0..4c44f14 100644
--- a/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs
+++ b/src/AxCopilot/Services/Agent/DeckQualityReviewService.cs
@@ -135,6 +135,8 @@ public static class DeckQualityReviewService
strengths.Add("Contains evidence-oriented comparison, table, or chart slides");
if (layoutCounts.Count >= 4)
strengths.Add($"Uses {layoutCounts.Count} distinct layouts");
+ if (executiveSlides > 0 && recommendationSlides > 0 && (comparisonSlides > 0 || chartSlides > 0) && roadmapSlides > 0)
+ strengths.Add("Reads like a decision-ready consulting storyline");
if (autoRepairCount > 0)
strengths.Add($"Auto-repair applied {autoRepairCount} time(s)");
if (slideAlertCount == 0)
@@ -217,6 +219,12 @@ public static class DeckQualityReviewService
issues.Add(new($"Slide {slideNumber}: Executive Summary should include KPI cards or quantified evidence.", DeckReviewSeverity.Info));
added++;
}
+ if (string.IsNullOrWhiteSpace(ReadString(slide, "headline")) &&
+ ReadStringList(slide, "summary_points").Count < 3)
+ {
+ issues.Add(new($"Slide {slideNumber}: Executive Summary should land a sharper single-message headline.", DeckReviewSeverity.Info));
+ added++;
+ }
break;
case "recommendation":
if (string.IsNullOrWhiteSpace(ReadString(slide, "recommendation")))
@@ -242,6 +250,12 @@ public static class DeckQualityReviewService
issues.Add(new($"Slide {slideNumber}: comparison slide should highlight a clear verdict.", DeckReviewSeverity.Info));
added++;
}
+ if (!slide.GetRawText().Contains("\"summary\"", StringComparison.OrdinalIgnoreCase) &&
+ !slide.GetRawText().Contains("\"pros\"", StringComparison.OrdinalIgnoreCase))
+ {
+ issues.Add(new($"Slide {slideNumber}: comparison slide should explain the trade-offs behind the verdict.", DeckReviewSeverity.Info));
+ added++;
+ }
break;
case "roadmap":
if (ReadJsonArrayCount(slide, "phases") == 0)
@@ -260,6 +274,11 @@ public static class DeckQualityReviewService
issues.Add(new($"Slide {slideNumber}: roadmap slide would be stronger with timeline and owner labels.", DeckReviewSeverity.Info));
added++;
}
+ if (!slide.GetRawText().Contains("\"detail\"", StringComparison.OrdinalIgnoreCase))
+ {
+ issues.Add(new($"Slide {slideNumber}: roadmap slide should describe the outcome or milestone for each phase.", DeckReviewSeverity.Info));
+ added++;
+ }
break;
case "chart":
if (ReadJsonArrayCount(slide, "chart_labels") == 0 || ReadJsonArrayCount(slide, "chart_values") == 0)
@@ -267,6 +286,13 @@ public static class DeckQualityReviewService
issues.Add(new($"Slide {slideNumber}: chart slide is missing chart data.", DeckReviewSeverity.Critical));
added++;
}
+ else if (string.IsNullOrWhiteSpace(ReadString(slide, "headline")) &&
+ ReadStringList(slide, "summary_points").Count == 0 &&
+ string.IsNullOrWhiteSpace(ReadString(slide, "body")))
+ {
+ issues.Add(new($"Slide {slideNumber}: chart slide should include a takeaway, not just visual data.", DeckReviewSeverity.Info));
+ added++;
+ }
break;
case "table":
if (ReadJsonArrayCount(slide, "headers") == 0 || ReadJsonArrayCount(slide, "rows") == 0)
@@ -287,6 +313,12 @@ public static class DeckQualityReviewService
issues.Add(new($"Slide {slideNumber}: KPI dashboard needs takeaway points, not just metric tiles.", DeckReviewSeverity.Info));
added++;
}
+ if (!slide.GetRawText().Contains("\"trend\"", StringComparison.OrdinalIgnoreCase) &&
+ !slide.GetRawText().Contains("\"note\"", StringComparison.OrdinalIgnoreCase))
+ {
+ issues.Add(new($"Slide {slideNumber}: KPI dashboard should include trend or note context for each metric.", DeckReviewSeverity.Info));
+ added++;
+ }
break;
}
diff --git a/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs b/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs
index a69a7e9..51f6ee9 100644
--- a/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs
+++ b/src/AxCopilot/Services/Agent/DeckRepairGuideService.cs
@@ -18,6 +18,8 @@ public static class DeckRepairGuideService
=> "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("sharper single-message headline", StringComparison.OrdinalIgnoreCase)
+ => "Rewrite the Executive Summary headline so the slide lands one decision-ready message",
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)
@@ -26,8 +28,12 @@ public static class DeckRepairGuideService
=> "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("trade-offs behind the verdict", StringComparison.OrdinalIgnoreCase)
+ => "Add short trade-off summaries so the comparison slide explains why the preferred option wins",
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("milestone for each phase", StringComparison.OrdinalIgnoreCase)
+ => "Add milestone detail for each roadmap phase so execution outcomes are explicit",
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)
@@ -46,12 +52,16 @@ public static class DeckRepairGuideService
=> "Expand the comparison slide to show at least two real options and a verdict",
var message when message.Contains("chart slide", StringComparison.OrdinalIgnoreCase)
=> "Provide chart labels and values or replace the slide with a comparison/evidence layout",
+ var message when message.Contains("takeaway, not just visual data", StringComparison.OrdinalIgnoreCase)
+ => "Add a takeaway statement so each chart translates data into a decision message",
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("trend or note context for each metric", StringComparison.OrdinalIgnoreCase)
+ => "Add trend or note context for each KPI so the dashboard explains why the number matters",
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 0aa0d28..dc1ef59 100644
--- a/src/AxCopilot/Services/Agent/HtmlSkill.cs
+++ b/src/AxCopilot/Services/Agent/HtmlSkill.cs
@@ -9,7 +9,7 @@ namespace AxCopilot.Services.Agent;
/// HTML (.html) 보고서를 생성하는 내장 스킬.
/// 테마 무드(mood)를 선택하면 TemplateService에서 해당 CSS를 가져와 적용합니다.
/// TOC, 커버 페이지, 섹션 번호 등 고급 문서 기능을 지원합니다.
-/// sections 파라미터로 구조화된 콘텐츠 블록(heading/paragraph/callout/table/chart/cards/list/quote/divider/kpi/comparison/roadmap/matrix)을 지원합니다.
+/// sections 파라미터로 구조화된 콘텐츠 블록(heading/paragraph/callout/table/chart/cards/list/quote/divider/kpi/comparison/roadmap/matrix/decision_matrix/metric_strip)을 지원합니다.
///
public class HtmlSkill : IAgentTool
{
@@ -58,6 +58,8 @@ public class HtmlSkill : IAgentTool
"'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}]}, " +
+ "'decision_matrix' {title, criteria:['...'], options:[{name, verdict, notes, scores:{criterion:number}}]}, " +
+ "'metric_strip' {title, items:[{label,value,trend,note}]}, " +
"'roadmap' {title, phases:[{title, detail, timeline, owner}]}, " +
"'matrix' {title, quadrants:[{title, items:['...']}]}, " +
"'list' {style:'bullet|number', items:['item', ' - sub-item']}, " +
@@ -373,6 +375,12 @@ public class HtmlSkill : IAgentTool
case "comparison":
sb.AppendLine(RenderComparison(section));
break;
+ case "decision_matrix":
+ sb.AppendLine(RenderDecisionMatrix(section));
+ break;
+ case "metric_strip":
+ sb.AppendLine(RenderMetricStrip(section));
+ break;
case "roadmap":
sb.AppendLine(RenderRoadmap(section));
break;
@@ -804,6 +812,87 @@ public class HtmlSkill : IAgentTool
return sb.ToString();
}
+ private static string RenderDecisionMatrix(JsonElement s)
+ {
+ if (!s.SafeTryGetProperty("options", out var options) || options.ValueKind != JsonValueKind.Array)
+ return "";
+
+ var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : null;
+ var criteria = s.SafeTryGetProperty("criteria", out var criteriaEl) && criteriaEl.ValueKind == JsonValueKind.Array
+ ? criteriaEl.EnumerateArray().Select(item => item.SafeGetString() ?? string.Empty).Where(item => !string.IsNullOrWhiteSpace(item)).ToList()
+ : [];
+
+ var sb = new StringBuilder();
+ if (!string.IsNullOrWhiteSpace(title))
+ sb.AppendLine($"{Escape(title)}
");
+
+ sb.AppendLine("");
+ sb.AppendLine("
");
+ sb.Append("| Option | ");
+ foreach (var criterion in criteria)
+ sb.Append($"{Escape(criterion)} | ");
+ sb.AppendLine("Verdict | Notes |
");
+ sb.AppendLine("");
+
+ foreach (var option in options.EnumerateArray())
+ {
+ var name = option.SafeTryGetProperty("name", out var nameEl) ? nameEl.SafeGetString() ?? "Option" : "Option";
+ var verdict = option.SafeTryGetProperty("verdict", out var verdictEl) ? verdictEl.SafeGetString() : null;
+ var notes = option.SafeTryGetProperty("notes", out var notesEl) ? notesEl.SafeGetString() : null;
+ option.SafeTryGetProperty("scores", out var scoresEl);
+
+ sb.Append("");
+ sb.Append($"| {Escape(name)} | ");
+ foreach (var criterion in criteria)
+ {
+ var score = scoresEl.ValueKind == JsonValueKind.Object &&
+ scoresEl.SafeTryGetProperty(criterion, out var scoreEl)
+ ? scoreEl.ToString()
+ : "-";
+ sb.Append($"{Escape(score)} | ");
+ }
+ sb.Append($"{MarkdownToHtml(verdict ?? string.Empty)} | ");
+ sb.Append($"{MarkdownToHtml(notes ?? string.Empty)} | ");
+ sb.AppendLine("
");
+ }
+
+ sb.AppendLine("");
+ sb.AppendLine("
");
+ sb.AppendLine("
");
+ return sb.ToString();
+ }
+
+ private static string RenderMetricStrip(JsonElement s)
+ {
+ if (!s.SafeTryGetProperty("items", out var items) || items.ValueKind != JsonValueKind.Array)
+ return "";
+
+ var title = s.SafeTryGetProperty("title", out var titleEl) ? titleEl.SafeGetString() : null;
+ var sb = new StringBuilder();
+ if (!string.IsNullOrWhiteSpace(title))
+ sb.AppendLine($"{Escape(title)}
");
+
+ sb.AppendLine("");
+ foreach (var item in items.EnumerateArray())
+ {
+ var label = item.SafeTryGetProperty("label", out var labelEl) ? labelEl.SafeGetString() ?? string.Empty : string.Empty;
+ var value = item.SafeTryGetProperty("value", out var valueEl) ? valueEl.SafeGetString() ?? string.Empty : string.Empty;
+ var trend = item.SafeTryGetProperty("trend", out var trendEl) ? trendEl.SafeGetString() : null;
+ var note = item.SafeTryGetProperty("note", out var noteEl) ? noteEl.SafeGetString() : null;
+
+ sb.AppendLine("
");
+ sb.AppendLine($"
{Escape(label)}
");
+ sb.AppendLine($"
{Escape(value)}
");
+ if (!string.IsNullOrWhiteSpace(trend))
+ sb.AppendLine($"
{Escape(trend)}
");
+ if (!string.IsNullOrWhiteSpace(note))
+ sb.AppendLine($"
{MarkdownToHtml(note)}
");
+ sb.AppendLine("
");
+ }
+ sb.AppendLine("
");
+ return sb.ToString();
+ }
+
private static string RenderRoadmap(JsonElement s)
{
if (!s.SafeTryGetProperty("phases", out var phases) || phases.ValueKind != JsonValueKind.Array)
diff --git a/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs b/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs
index 0eb7376..972d742 100644
--- a/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs
+++ b/src/AxCopilot/Services/Agent/WorkspaceContextGenerator.cs
@@ -104,6 +104,16 @@ internal static class WorkspaceContextGenerator
sb.AppendLine();
}
+ if (string.Equals(primaryLang.Key, ".sql", StringComparison.OrdinalIgnoreCase) ||
+ extDist.Any(entry => string.Equals(entry.Key, ".sql", StringComparison.OrdinalIgnoreCase) && entry.Value >= 3))
+ {
+ sb.AppendLine("## SQL Review Focus");
+ sb.AppendLine("- Classify each script as migration, seed, reporting query, or operational patch before execution.");
+ sb.AppendLine("- Review destructive DDL, broad UPDATE/DELETE statements, transaction scope, and dependency order.");
+ sb.AppendLine("- Validate scripts in a disposable database and capture rollback notes for production changes.");
+ sb.AppendLine();
+ }
+
var contextFiles = DetectContextFiles(workFolder);
if (contextFiles.Count > 0)
{
diff --git a/src/AxCopilot/Services/CodeLanguageCatalog.cs b/src/AxCopilot/Services/CodeLanguageCatalog.cs
index 35076d4..8b458e6 100644
--- a/src/AxCopilot/Services/CodeLanguageCatalog.cs
+++ b/src/AxCopilot/Services/CodeLanguageCatalog.cs
@@ -442,7 +442,7 @@ public static class CodeLanguageCatalog
if (!string.IsNullOrWhiteSpace(primaryGuidance))
parts.Add("focus: " + primaryGuidance);
if (string.Equals(capability.Key, "sql", StringComparison.OrdinalIgnoreCase))
- parts.Add("analysis: detect dialect, statement kinds, destructive risk, and object dependencies");
+ parts.Add("analysis: detect dialect, script intent, destructive risk, migration order, and object dependencies");
if (parts.Count == 0)
return capability.DisplayName;
diff --git a/src/AxCopilot/Services/SqlAnalysisService.cs b/src/AxCopilot/Services/SqlAnalysisService.cs
index 237e8d5..7cc3c59 100644
--- a/src/AxCopilot/Services/SqlAnalysisService.cs
+++ b/src/AxCopilot/Services/SqlAnalysisService.cs
@@ -6,22 +6,31 @@ namespace AxCopilot.Services;
public sealed record SqlAnalysisReport(
string Dialect,
+ string ScriptIntent,
IReadOnlyList StatementKinds,
IReadOnlyList Objects,
+ IReadOnlyList Dependencies,
IReadOnlyList Risks,
- IReadOnlyList SuggestedChecks)
+ IReadOnlyList SuggestedChecks,
+ IReadOnlyList ReviewNotes)
{
public string ToFallbackSummary()
{
var parts = new List { $"SQL static analysis: {Dialect}" };
+ if (!string.IsNullOrWhiteSpace(ScriptIntent))
+ parts.Add("script: " + ScriptIntent);
if (StatementKinds.Count > 0)
parts.Add("statements: " + string.Join(", ", StatementKinds.Take(5)));
if (Objects.Count > 0)
parts.Add("objects: " + string.Join(", ", Objects.Take(6)));
+ if (Dependencies.Count > 0)
+ parts.Add("dependencies: " + string.Join(", ", Dependencies.Take(6)));
if (Risks.Count > 0)
parts.Add("risks: " + string.Join(" | ", Risks.Take(3)));
if (SuggestedChecks.Count > 0)
parts.Add("next checks: " + string.Join(" | ", SuggestedChecks.Take(3)));
+ if (ReviewNotes.Count > 0)
+ parts.Add("review focus: " + string.Join(" | ", ReviewNotes.Take(3)));
return string.Join(Environment.NewLine, parts);
}
}
@@ -36,11 +45,14 @@ public static class SqlAnalysisService
var statements = SplitStatements(normalized);
var statementKinds = DetectStatementKinds(normalized);
+ var scriptIntent = ClassifyScriptIntent(statementKinds, filePath, normalized);
var objects = DetectObjects(normalized);
+ var dependencies = DetectDependencies(normalized, objects);
var risks = DetectRisks(statements, normalized);
var checks = BuildSuggestedChecks(statementKinds, risks, dialect, filePath);
+ var reviewNotes = BuildReviewNotes(statementKinds, risks, dependencies, dialect, filePath, scriptIntent);
- return new SqlAnalysisReport(dialect, statementKinds, objects, risks, checks);
+ return new SqlAnalysisReport(dialect, scriptIntent, statementKinds, objects, dependencies, risks, checks, reviewNotes);
}
public static string BuildFallbackSummary(string? filePathOrExtension)
@@ -53,13 +65,20 @@ public static class SqlAnalysisService
return new SqlAnalysisReport(
"Generic SQL",
+ "generic script",
["DDL/DML review"],
[],
+ [],
["Review destructive statements and transaction boundaries before execution."],
[
"Confirm migration order, object dependencies, and rollback strategy",
"Run the script in a disposable database before applying it to shared environments",
"Review broad UPDATE/DELETE predicates, indexes, and constraint impact"
+ ],
+ [
+ "Identify whether the script is a migration, seed, or ad-hoc query before execution",
+ "Check dependency order for tables, views, indexes, and procedures",
+ "Prepare a rollback or recovery note for destructive or high-impact changes"
]).ToFallbackSummary();
}
@@ -72,6 +91,7 @@ public static class SqlAnalysisService
AddKind(kinds, sql, @"\btruncate\s+table\b", "TRUNCATE TABLE");
AddKind(kinds, sql, @"\bcreate\s+(unique\s+)?index\b", "CREATE INDEX");
AddKind(kinds, sql, @"\bcreate\s+view\b", "CREATE VIEW");
+ AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?trigger\b", "CREATE TRIGGER");
AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?function\b", "CREATE FUNCTION");
AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?procedure\b", "CREATE PROCEDURE");
AddKind(kinds, sql, @"\binsert\s+into\b", "INSERT");
@@ -111,6 +131,67 @@ public static class SqlAnalysisService
return names.Take(10).ToList();
}
+ private static IReadOnlyList DetectDependencies(string sql, IReadOnlyList objects)
+ {
+ var dependencies = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var ownedObjects = DetectOwnedObjects(sql);
+ var patterns = new[]
+ {
+ @"\breferences\s+(?[\[\]`\""\w\.]+)",
+ @"\bjoin\s+(?[\[\]`\""\w\.]+)",
+ @"\bfrom\s+(?[\[\]`\""\w\.]+)",
+ @"\binto\s+(?[\[\]`\""\w\.]+)",
+ @"\bupdate\s+(?[\[\]`\""\w\.]+)",
+ @"\bexec(?:ute)?\s+(?[\[\]`\""\w\.]+)",
+ @"\bon\s+(?[\[\]`\""\w\.]+)"
+ };
+
+ foreach (var pattern in patterns)
+ {
+ foreach (Match match in Regex.Matches(sql, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline))
+ {
+ var name = NormalizeObjectName(match.Groups["name"].Value);
+ if (!string.IsNullOrWhiteSpace(name))
+ dependencies.Add(name);
+ }
+ }
+
+ foreach (var ownedObject in ownedObjects)
+ dependencies.Remove(ownedObject);
+
+ return dependencies.Take(10).ToList();
+ }
+
+ private static IReadOnlyList DetectOwnedObjects(string sql)
+ {
+ var patterns = new[]
+ {
+ @"\bcreate\s+table\s+(if\s+not\s+exists\s+)?(?[\[\]`\""\w\.]+)",
+ @"\balter\s+table\s+(?[\[\]`\""\w\.]+)",
+ @"\bdrop\s+table\s+(if\s+exists\s+)?(?[\[\]`\""\w\.]+)",
+ @"\bcreate\s+view\s+(?[\[\]`\""\w\.]+)",
+ @"\bcreate\s+(or\s+replace\s+)?function\s+(?[\[\]`\""\w\.]+)",
+ @"\bcreate\s+(or\s+replace\s+)?procedure\s+(?[\[\]`\""\w\.]+)",
+ @"\bcreate\s+(unique\s+)?index\s+(?[\[\]`\""\w\.]+)",
+ @"\binsert\s+into\s+(?[\[\]`\""\w\.]+)",
+ @"\bupdate\s+(?[\[\]`\""\w\.]+)",
+ @"\bdelete\s+from\s+(?[\[\]`\""\w\.]+)"
+ };
+
+ var names = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var pattern in patterns)
+ {
+ foreach (Match match in Regex.Matches(sql, pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline))
+ {
+ var name = NormalizeObjectName(match.Groups["name"].Value);
+ if (!string.IsNullOrWhiteSpace(name))
+ names.Add(name);
+ }
+ }
+
+ return names.ToList();
+ }
+
private static IReadOnlyList DetectRisks(IReadOnlyList statements, string fullSql)
{
var risks = new List();
@@ -152,6 +233,26 @@ public static class SqlAnalysisService
return risks.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
}
+ private static string ClassifyScriptIntent(IReadOnlyList statementKinds, string? filePath, string sql)
+ {
+ var fileName = Path.GetFileName(filePath ?? string.Empty);
+ if (fileName.Contains("seed", StringComparison.OrdinalIgnoreCase))
+ return "seed / reference data";
+ if (fileName.Contains("migration", StringComparison.OrdinalIgnoreCase) ||
+ fileName.Contains("schema", StringComparison.OrdinalIgnoreCase) ||
+ fileName.Contains("ddl", StringComparison.OrdinalIgnoreCase))
+ return "schema migration";
+ if (statementKinds.Any(kind => kind.StartsWith("CREATE", StringComparison.OrdinalIgnoreCase) ||
+ kind.StartsWith("ALTER", StringComparison.OrdinalIgnoreCase) ||
+ kind.StartsWith("DROP", StringComparison.OrdinalIgnoreCase)))
+ return "schema change";
+ if (statementKinds.Any(kind => kind is "INSERT" or "UPDATE" or "DELETE" or "MERGE"))
+ return "data change";
+ if (Regex.IsMatch(sql, @"\bselect\b", RegexOptions.IgnoreCase))
+ return "query / reporting";
+ return "generic script";
+ }
+
private static IReadOnlyList BuildSuggestedChecks(
IReadOnlyList statementKinds,
IReadOnlyList risks,
@@ -185,6 +286,57 @@ public static class SqlAnalysisService
return checks.Distinct(StringComparer.OrdinalIgnoreCase).Take(5).ToList();
}
+ private static IReadOnlyList BuildReviewNotes(
+ IReadOnlyList statementKinds,
+ IReadOnlyList risks,
+ IReadOnlyList dependencies,
+ string dialect,
+ string? filePath,
+ string scriptIntent)
+ {
+ var notes = new List();
+
+ if (string.Equals(scriptIntent, "schema migration", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(scriptIntent, "schema change", StringComparison.OrdinalIgnoreCase))
+ {
+ notes.Add("Review forward migration order and any missing rollback note.");
+ }
+
+ if (dependencies.Count > 0)
+ notes.Add("Confirm downstream impact on referenced tables, views, procedures, and joins.");
+
+ if (statementKinds.Any(kind => kind is "CREATE VIEW" or "CREATE FUNCTION" or "CREATE PROCEDURE" or "CREATE TRIGGER"))
+ notes.Add("Validate dependent objects and deployment order for derived database objects.");
+
+ if (statementKinds.Any(kind => kind is "CREATE INDEX" or "ALTER TABLE"))
+ notes.Add("Review index, constraint, and lock impact before applying changes.");
+
+ if (risks.Any(risk => risk.Contains("DELETE statement without WHERE", StringComparison.OrdinalIgnoreCase) ||
+ risk.Contains("UPDATE statement without WHERE", StringComparison.OrdinalIgnoreCase)))
+ {
+ notes.Add("Require expected row-count validation before running broad data changes.");
+ }
+
+ if (!risks.Any(risk => risk.Contains("transaction", StringComparison.OrdinalIgnoreCase)) &&
+ statementKinds.Any(kind => kind is "INSERT" or "UPDATE" or "DELETE" or "MERGE" or "ALTER TABLE" or "DROP TABLE"))
+ {
+ notes.Add("Document transaction scope and failure recovery path explicitly.");
+ }
+
+ if (string.Equals(dialect, "SQL Server", StringComparison.OrdinalIgnoreCase))
+ notes.Add("Check GO batch boundaries and locking behavior.");
+ else if (string.Equals(dialect, "PostgreSQL", StringComparison.OrdinalIgnoreCase))
+ notes.Add("Check transactional DDL behavior and dependent object invalidation.");
+ else if (string.Equals(dialect, "MySQL", StringComparison.OrdinalIgnoreCase))
+ notes.Add("Check engine-specific DDL behavior and online migration limits.");
+
+ var fileName = Path.GetFileName(filePath ?? string.Empty);
+ if (fileName.Contains("seed", StringComparison.OrdinalIgnoreCase))
+ notes.Add("Verify the script is idempotent before rerunning seed data.");
+
+ return notes.Distinct(StringComparer.OrdinalIgnoreCase).Take(5).ToList();
+ }
+
private static IReadOnlyList SplitStatements(string sql)
{
return sql