SQL 정적 분석과 PPT·HTML critic을 고도화하고 코드 탭 fallback 문맥을 보강

- SqlAnalysisService에 script intent, dependency, review focus 계산을 추가해 migration/seed/reporting SQL의 위험도와 검토 포인트를 더 정확히 안내하도록 개선했습니다.
- HtmlSkill에 decision_matrix, metric_strip 섹션을 추가하고 ArtifactQualityReviewService/ArtifactRepairGuideService에서 board·strategy 문서의 의사결정 구조와 KPI 연결 부족을 더 정밀하게 진단하도록 강화했습니다.
- DeckQualityReviewService와 DeckRepairGuideService를 확장해 executive summary headline, comparison trade-off, roadmap milestone, chart takeaway, KPI context 부족을 추가로 감지하고 보정 가이드를 반환하도록 정리했습니다.
- WorkspaceContextGenerator와 CodeLanguageCatalog를 업데이트해 SQL 저장소에서 SQL Review Focus와 확장된 workflow summary를 제공하도록 맞췄고, README/DEVELOPMENT/NEXT_ROADMAP에 2026-04-15 11:36 (KST) 기준 이력을 반영했습니다.

검증 결과
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_code_sql_doc_final\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc_final\\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SqlDialectDetectorTests|SqlAnalysisServiceTests|CodeLanguageCatalogTests|WorkspaceContextGeneratorTests|ArtifactQualityReviewServiceTests|ArtifactRepairGuideServiceTests|DeckQualityReviewServiceTests|HtmlSkillConsultingSectionsTests" -p:OutputPath=bin\\verify_code_sql_doc_final_tests\\ -p:IntermediateOutputPath=obj\\verify_code_sql_doc_final_tests\\ : 통과 62
This commit is contained in:
2026-04-15 11:38:50 +09:00
parent 93c3c647d9
commit 2a49b1da24
18 changed files with 538 additions and 8 deletions

View File

@@ -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 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 - `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

View File

@@ -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 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 - `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

View File

@@ -203,3 +203,10 @@
1. SQL은 별도 정적 분석 계층까지 올라왔습니다. 현재도 no-LSP 환경에서 `dialect/statement/risk/next checks`를 직접 설명할 수 있고, 다음 단계로 더 간다면 dialect별 migration lint나 schema dependency graph 정도가 후보입니다. 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 확대 성격의 마감 작업이 남아 있습니다. 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 쪽이 중심입니다. 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 마감 중심입니다.

View File

@@ -16,7 +16,9 @@ public class ArtifactQualityReviewServiceTests
<div class="callout-info">Important message</div> <div class="callout-info">Important message</div>
<table><tr><th>Metric</th><th>Value</th></tr><tr><td>NPS</td><td>61</td></tr></table> <table><tr><th>Metric</th><th>Value</th></tr><tr><td>NPS</td><td>61</td></tr></table>
<div class="kpi-panel"></div> <div class="kpi-panel"></div>
<div class="metric-strip"></div>
<div class="comparison-grid"></div> <div class="comparison-grid"></div>
<div class="decision-matrix"></div>
<div class="roadmap-block"></div> <div class="roadmap-block"></div>
<div class="decision-summary"></div> <div class="decision-summary"></div>
<section class="board-report-panel"></section> <section class="board-report-panel"></section>
@@ -32,6 +34,7 @@ public class ArtifactQualityReviewServiceTests
strength.Contains("cover", StringComparison.OrdinalIgnoreCase) || strength.Contains("cover", StringComparison.OrdinalIgnoreCase) ||
strength.Contains("contents", 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("board-ready", StringComparison.OrdinalIgnoreCase));
review.Strengths.Should().Contain(strength => strength.Contains("decision matrix", StringComparison.OrdinalIgnoreCase));
review.Issues.Should().NotContain(issue => issue.Severity == ArtifactReviewSeverity.Critical); 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("evidence table", StringComparison.OrdinalIgnoreCase));
review.Issues.Should().Contain(issue => issue.Message.Contains("callout or highlight blocks", StringComparison.OrdinalIgnoreCase)); review.Issues.Should().Contain(issue => issue.Message.Contains("callout or highlight blocks", StringComparison.OrdinalIgnoreCase));
} }
[Fact]
public void ReviewHtml_ShouldRecommendDecisionMatrix_WhenTradeoffViewIsMissing()
{
var html =
"""
<h2>Executive Summary</h2><p>Summary text.</p><p>Supporting paragraph.</p>
<section class="board-report-panel"></section>
<section class="strategy-brief-panel"></section>
<div class="decision-summary"></div>
<div class="metric-strip"></div>
<h2>Recommendation</h2><p>Recommendation text.</p><p>More detail.</p>
<h2>Appendix</h2><p>Reference detail.</p><p>More detail.</p>
""";
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));
}
} }

View File

@@ -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 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("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); var guide = ArtifactRepairGuideService.BuildGuide(review);
@@ -45,6 +46,7 @@ public class ArtifactRepairGuideServiceTests
guide.Should().Contain("decision summary"); guide.Should().Contain("decision summary");
guide.Should().Contain("KPI"); guide.Should().Contain("KPI");
guide.Should().Contain("strategy brief"); guide.Should().Contain("strategy brief");
guide.Should().Contain("decision matrix");
} }
[Fact] [Fact]

View File

@@ -103,5 +103,7 @@ public class CodeLanguageCatalogTests
summary.Should().Contain("schema.sql"); summary.Should().Contain("schema.sql");
summary.Should().Contain("disposable database"); summary.Should().Contain("disposable database");
summary.Should().Contain("dialect"); summary.Should().Contain("dialect");
summary.Should().Contain("migration order");
summary.Should().Contain("dependencies");
} }
} }

View File

@@ -125,4 +125,46 @@ public class DeckQualityReviewServiceTests
review.Issues.Should().Contain(issue => issue.Message.Contains("supporting rationale or next steps", StringComparison.OrdinalIgnoreCase)); 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));
}
} }

View File

@@ -38,6 +38,23 @@ public class HtmlSkillConsultingSectionsTests
{ "name": "Option A", "summary": "Fast rollout", "pros": "Low setup effort", "cons": "Limited scale", "verdict": "Fast" } { "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", "type": "roadmap",
"title": "Delivery Roadmap", "title": "Delivery Roadmap",
@@ -108,6 +125,8 @@ public class HtmlSkillConsultingSectionsTests
result.Output.Should().Contain("Repair guide:"); result.Output.Should().Contain("Repair guide:");
var html = File.ReadAllText(Path.Combine(workDir, "consulting.html")); var html = File.ReadAllText(Path.Combine(workDir, "consulting.html"));
html.Should().Contain("comparison-grid"); html.Should().Contain("comparison-grid");
html.Should().Contain("decision-matrix");
html.Should().Contain("metric-strip");
html.Should().Contain("roadmap-block"); html.Should().Contain("roadmap-block");
html.Should().Contain("matrix-grid"); html.Should().Contain("matrix-grid");
html.Should().Contain("decision-summary"); html.Should().Contain("decision-summary");

View File

@@ -14,6 +14,7 @@ public class SqlAnalysisServiceTests
""" """
BEGIN TRANSACTION; BEGIN TRANSACTION;
CREATE TABLE orders (id SERIAL PRIMARY KEY, customer_id INT); 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; UPDATE orders SET customer_id = 0;
DROP TABLE legacy_orders; DROP TABLE legacy_orders;
COMMIT; COMMIT;
@@ -22,14 +23,17 @@ public class SqlAnalysisServiceTests
var report = SqlAnalysisService.Analyze(sql, "20260415_orders_migration.sql"); var report = SqlAnalysisService.Analyze(sql, "20260415_orders_migration.sql");
report.Dialect.Should().Be("PostgreSQL"); report.Dialect.Should().Be("PostgreSQL");
report.ScriptIntent.Should().Be("schema migration");
report.StatementKinds.Should().Contain("CREATE TABLE"); report.StatementKinds.Should().Contain("CREATE TABLE");
report.StatementKinds.Should().Contain("UPDATE"); report.StatementKinds.Should().Contain("UPDATE");
report.StatementKinds.Should().Contain("DROP TABLE"); report.StatementKinds.Should().Contain("DROP TABLE");
report.Objects.Should().Contain("orders"); report.Objects.Should().Contain("orders");
report.Objects.Should().Contain("legacy_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("UPDATE statement without WHERE", StringComparison.OrdinalIgnoreCase));
report.Risks.Should().Contain(risk => risk.Contains("Destructive DROP", StringComparison.OrdinalIgnoreCase)); report.Risks.Should().Contain(risk => risk.Contains("Destructive DROP", StringComparison.OrdinalIgnoreCase));
report.SuggestedChecks.Should().Contain(check => check.Contains("rollback", StringComparison.OrdinalIgnoreCase)); report.SuggestedChecks.Should().Contain(check => check.Contains("rollback", StringComparison.OrdinalIgnoreCase));
report.ReviewNotes.Should().Contain(note => note.Contains("rollback", StringComparison.OrdinalIgnoreCase));
} }
[Fact] [Fact]
@@ -48,6 +52,8 @@ public class SqlAnalysisServiceTests
summary.Should().Contain("CREATE TABLE"); summary.Should().Contain("CREATE TABLE");
summary.Should().Contain("DELETE"); summary.Should().Contain("DELETE");
summary.Should().Contain("users"); summary.Should().Contain("users");
summary.Should().Contain("script:");
summary.Should().Contain("review focus:");
} }
finally finally
{ {
@@ -55,4 +61,24 @@ public class SqlAnalysisServiceTests
File.Delete(path); 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));
}
} }

View File

@@ -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] [Fact]
public void DetectLanguageWorkflowHints_ShouldPreferSelectedLanguage() public void DetectLanguageWorkflowHints_ShouldPreferSelectedLanguage()
{ {

View File

@@ -127,6 +127,8 @@ public static class ArtifactQualityReviewService
var tableCount = Regex.Matches(html, @"<table\b", RegexOptions.IgnoreCase).Count; var tableCount = Regex.Matches(html, @"<table\b", RegexOptions.IgnoreCase).Count;
var calloutCount = Regex.Matches(html, @"callout-(info|warning|tip|danger)", RegexOptions.IgnoreCase).Count; var calloutCount = Regex.Matches(html, @"callout-(info|warning|tip|danger)", RegexOptions.IgnoreCase).Count;
var comparisonCount = Regex.Matches(html, @"comparison-grid|comparison-card", RegexOptions.IgnoreCase).Count; var comparisonCount = Regex.Matches(html, @"comparison-grid|comparison-card", RegexOptions.IgnoreCase).Count;
var decisionMatrixCount = Regex.Matches(html, @"decision-matrix", RegexOptions.IgnoreCase).Count;
var metricStripCount = Regex.Matches(html, @"metric-strip", RegexOptions.IgnoreCase).Count;
var roadmapCount = Regex.Matches(html, @"roadmap(-block|-phase)?", RegexOptions.IgnoreCase).Count; var roadmapCount = Regex.Matches(html, @"roadmap(-block|-phase)?", RegexOptions.IgnoreCase).Count;
var matrixCount = Regex.Matches(html, @"matrix-grid|risk-matrix", 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 decisionCount = Regex.Matches(html, @"decision-summary", RegexOptions.IgnoreCase).Count;
@@ -139,6 +141,8 @@ public static class ArtifactQualityReviewService
+ (boardPanelCount > 0 ? 1 : 0) + (boardPanelCount > 0 ? 1 : 0)
+ (strategyBriefCount > 0 ? 1 : 0) + (strategyBriefCount > 0 ? 1 : 0)
+ (comparisonCount > 0 ? 1 : 0) + (comparisonCount > 0 ? 1 : 0)
+ (decisionMatrixCount > 0 ? 1 : 0)
+ (metricStripCount > 0 ? 1 : 0)
+ (roadmapCount > 0 ? 1 : 0) + (roadmapCount > 0 ? 1 : 0)
+ (matrixCount > 0 ? 1 : 0) + (matrixCount > 0 ? 1 : 0)
+ (decisionCount > 0 ? 1 : 0) + (decisionCount > 0 ? 1 : 0)
@@ -149,6 +153,8 @@ public static class ArtifactQualityReviewService
+ tableCount + tableCount
+ calloutCount + calloutCount
+ comparisonCount + comparisonCount
+ decisionMatrixCount
+ metricStripCount
+ roadmapCount + roadmapCount
+ matrixCount + matrixCount
+ decisionCount + decisionCount
@@ -164,12 +170,14 @@ public static class ArtifactQualityReviewService
if (printReady) strengths.Add("Includes print-ready CSS"); if (printReady) strengths.Add("Includes print-ready CSS");
if (hasPrintFrame) strengths.Add("Includes print header/footer frame"); if (hasPrintFrame) strengths.Add("Includes print header/footer frame");
if (majorSectionEstimate >= 5) strengths.Add($"Contains {majorSectionEstimate} major sections"); 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"); strengths.Add("Uses structured business blocks");
if (calloutCount > 0) strengths.Add("Uses callout blocks for emphasis"); if (calloutCount > 0) strengths.Add("Uses callout blocks for emphasis");
if (boardPanelCount > 0) strengths.Add("Includes board-ready summary panel"); if (boardPanelCount > 0) strengths.Add("Includes board-ready summary panel");
if (strategyBriefCount > 0) strengths.Add("Includes strategy brief panel"); if (strategyBriefCount > 0) strengths.Add("Includes strategy brief panel");
if (kpiPanelCount > 0) strengths.Add("Includes KPI 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) if (html.Length < 1800)
issues.Add(new("Body content may be too short for an executive-quality document.", ArtifactReviewSeverity.Warning)); 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)); issues.Add(new("Major section count is low for a business report.", ArtifactReviewSeverity.Warning));
if (supportingBlockEstimate < Math.Max(4, majorSectionEstimate * 2)) if (supportingBlockEstimate < Math.Max(4, majorSectionEstimate * 2))
issues.Add(new("Several sections may need more supporting paragraphs.", ArtifactReviewSeverity.Warning)); 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)); issues.Add(new("Structured visual blocks are limited.", ArtifactReviewSeverity.Warning));
if (placeholderCount > 0) if (placeholderCount > 0)
issues.Add(new($"Found {placeholderCount} placeholder or unfinished marker(s).", ArtifactReviewSeverity.Critical)); 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)); 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) if (boardPanelCount > 0 && evidenceCardCount == 0 && tableCount == 0)
issues.Add(new("Board-ready report would benefit from evidence cards or a supporting table.", ArtifactReviewSeverity.Info)); 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) if (strategyBriefCount > 0 && decisionCount == 0)
issues.Add(new("Strategy brief should include explicit decisions or a decision summary block.", ArtifactReviewSeverity.Warning)); issues.Add(new("Strategy brief should include explicit decisions or a decision summary block.", ArtifactReviewSeverity.Warning));
if (strategyBriefCount > 0 && comparisonCount + roadmapCount == 0) if (strategyBriefCount > 0 && comparisonCount + roadmapCount == 0)
issues.Add(new("Strategy brief would be stronger with a comparison or roadmap block.", ArtifactReviewSeverity.Info)); issues.Add(new("Strategy brief would be stronger with a comparison or roadmap block.", ArtifactReviewSeverity.Info));
if (strategyBriefCount > 0 && evidenceCardCount + kpiPanelCount == 0) if (strategyBriefCount > 0 && evidenceCardCount + kpiPanelCount == 0)
issues.Add(new("Strategy brief would benefit from KPI or evidence support blocks.", ArtifactReviewSeverity.Info)); 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) if (kpiPanelCount > 0 && decisionCount == 0 && boardPanelCount == 0)
issues.Add(new("KPI panel should roll into an explicit decision or next-step block.", ArtifactReviewSeverity.Info)); 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); return BuildReport("html", strengths, issues);
} }

View File

@@ -22,7 +22,7 @@ public static class ArtifactRepairGuideService
if (!string.IsNullOrWhiteSpace(action) && !actions.Contains(action, StringComparer.OrdinalIgnoreCase)) if (!string.IsNullOrWhiteSpace(action) && !actions.Contains(action, StringComparer.OrdinalIgnoreCase))
actions.Add(action); actions.Add(action);
if (actions.Count >= 3) if (actions.Count >= 4)
break; 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"; 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)) if (message.Contains("decision summary or evidence cards", StringComparison.OrdinalIgnoreCase))
return "Add a decision summary and evidence cards near the recommendation section"; 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)) if (message.Contains("Strategy brief should include explicit decisions", StringComparison.OrdinalIgnoreCase))
return "Add an explicit decisions block or decision summary to the strategy brief"; return "Add an explicit decisions block or decision summary to the strategy brief";
if (message.Contains("KPI or evidence support blocks", StringComparison.OrdinalIgnoreCase)) 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"; 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)) if (message.Contains("cover page", StringComparison.OrdinalIgnoreCase))
return "Add a cover page for print-ready or board-facing reports"; return "Add a cover page for print-ready or board-facing reports";
if (message.Contains("table of contents", StringComparison.OrdinalIgnoreCase)) 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"; return "Use comparison, roadmap, matrix, KPI, or evidence blocks instead of plain text only";
if (message.Contains("Executive summary", StringComparison.OrdinalIgnoreCase)) if (message.Contains("Executive summary", StringComparison.OrdinalIgnoreCase))
return "Add an Executive Summary section near the top of the report"; 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; return null;
} }

View File

@@ -135,6 +135,8 @@ public static class DeckQualityReviewService
strengths.Add("Contains evidence-oriented comparison, table, or chart slides"); strengths.Add("Contains evidence-oriented comparison, table, or chart slides");
if (layoutCounts.Count >= 4) if (layoutCounts.Count >= 4)
strengths.Add($"Uses {layoutCounts.Count} distinct layouts"); 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) if (autoRepairCount > 0)
strengths.Add($"Auto-repair applied {autoRepairCount} time(s)"); strengths.Add($"Auto-repair applied {autoRepairCount} time(s)");
if (slideAlertCount == 0) 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)); issues.Add(new($"Slide {slideNumber}: Executive Summary should include KPI cards or quantified evidence.", DeckReviewSeverity.Info));
added++; 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; break;
case "recommendation": case "recommendation":
if (string.IsNullOrWhiteSpace(ReadString(slide, "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)); issues.Add(new($"Slide {slideNumber}: comparison slide should highlight a clear verdict.", DeckReviewSeverity.Info));
added++; 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; break;
case "roadmap": case "roadmap":
if (ReadJsonArrayCount(slide, "phases") == 0) 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)); issues.Add(new($"Slide {slideNumber}: roadmap slide would be stronger with timeline and owner labels.", DeckReviewSeverity.Info));
added++; 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; break;
case "chart": case "chart":
if (ReadJsonArrayCount(slide, "chart_labels") == 0 || ReadJsonArrayCount(slide, "chart_values") == 0) 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)); issues.Add(new($"Slide {slideNumber}: chart slide is missing chart data.", DeckReviewSeverity.Critical));
added++; 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; break;
case "table": case "table":
if (ReadJsonArrayCount(slide, "headers") == 0 || ReadJsonArrayCount(slide, "rows") == 0) 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)); issues.Add(new($"Slide {slideNumber}: KPI dashboard needs takeaway points, not just metric tiles.", DeckReviewSeverity.Info));
added++; 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; break;
} }

View File

@@ -18,6 +18,8 @@ public static class DeckRepairGuideService
=> "Add or strengthen the Executive Summary with 2-3 evidence-backed takeaways", => "Add or strengthen the Executive Summary with 2-3 evidence-backed takeaways",
var message when message.Contains("KPI cards or quantified evidence", StringComparison.OrdinalIgnoreCase) 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", => "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) var message when message.Contains("supporting rationale or next steps", StringComparison.OrdinalIgnoreCase)
=> "Add supporting rationale or next steps so the recommendation turns into action", => "Add supporting rationale or next steps so the recommendation turns into action",
var message when message.Contains("Recommendation", StringComparison.OrdinalIgnoreCase) 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", => "Add a roadmap slide with phases, owners, and timing",
var message when message.Contains("clear verdict", StringComparison.OrdinalIgnoreCase) var message when message.Contains("clear verdict", StringComparison.OrdinalIgnoreCase)
=> "Label the preferred option explicitly so the comparison slide lands a decision", => "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) var message when message.Contains("timeline and owner labels", StringComparison.OrdinalIgnoreCase)
=> "Add timeline and owner labels so the roadmap reads like an execution plan", => "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) var message when message.Contains("Appendix or evidence slide", StringComparison.OrdinalIgnoreCase)
=> "Add appendix or evidence slides with supporting data, assumptions, or source tables", => "Add appendix or evidence slides with supporting data, assumptions, or source tables",
var message when message.Contains("duplicate headline", StringComparison.OrdinalIgnoreCase) 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", => "Expand the comparison slide to show at least two real options and a verdict",
var message when message.Contains("chart slide", StringComparison.OrdinalIgnoreCase) var message when message.Contains("chart slide", StringComparison.OrdinalIgnoreCase)
=> "Provide chart labels and values or replace the slide with a comparison/evidence layout", => "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) var message when message.Contains("table slide", StringComparison.OrdinalIgnoreCase)
=> "Provide complete table headers and rows or simplify the slide to key callouts", => "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) 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", => "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) 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", => "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) var message when message.Contains("text-heavy", StringComparison.OrdinalIgnoreCase)
=> "Convert text-heavy slides into message-led visuals with fewer bullets", => "Convert text-heavy slides into message-led visuals with fewer bullets",
var message when message.Contains("Evidence slides", StringComparison.OrdinalIgnoreCase) var message when message.Contains("Evidence slides", StringComparison.OrdinalIgnoreCase)

View File

@@ -9,7 +9,7 @@ namespace AxCopilot.Services.Agent;
/// HTML (.html) 보고서를 생성하는 내장 스킬. /// HTML (.html) 보고서를 생성하는 내장 스킬.
/// 테마 무드(mood)를 선택하면 TemplateService에서 해당 CSS를 가져와 적용합니다. /// 테마 무드(mood)를 선택하면 TemplateService에서 해당 CSS를 가져와 적용합니다.
/// TOC, 커버 페이지, 섹션 번호 등 고급 문서 기능을 지원합니다. /// 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)을 지원합니다.
/// </summary> /// </summary>
public class HtmlSkill : IAgentTool 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:['...']}, " + "'board_report' {title, decision, recommendation, rationale, metrics:[{label,value,note}], risks:['...'], next_steps:['...']}, " +
"'strategy_brief' {title, strategic_question, thesis, implications:['...'], decisions:['...']}, " + "'strategy_brief' {title, strategic_question, thesis, implications:['...'], decisions:['...']}, " +
"'comparison' {title, items:[{name, summary, pros, cons, verdict}]}, " + "'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}]}, " + "'roadmap' {title, phases:[{title, detail, timeline, owner}]}, " +
"'matrix' {title, quadrants:[{title, items:['...']}]}, " + "'matrix' {title, quadrants:[{title, items:['...']}]}, " +
"'list' {style:'bullet|number', items:['item', ' - sub-item']}, " + "'list' {style:'bullet|number', items:['item', ' - sub-item']}, " +
@@ -373,6 +375,12 @@ public class HtmlSkill : IAgentTool
case "comparison": case "comparison":
sb.AppendLine(RenderComparison(section)); sb.AppendLine(RenderComparison(section));
break; break;
case "decision_matrix":
sb.AppendLine(RenderDecisionMatrix(section));
break;
case "metric_strip":
sb.AppendLine(RenderMetricStrip(section));
break;
case "roadmap": case "roadmap":
sb.AppendLine(RenderRoadmap(section)); sb.AppendLine(RenderRoadmap(section));
break; break;
@@ -804,6 +812,87 @@ public class HtmlSkill : IAgentTool
return sb.ToString(); 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($"<h3>{Escape(title)}</h3>");
sb.AppendLine("<div class=\"decision-matrix\" style=\"overflow-x:auto;margin:1.25rem 0;\">");
sb.AppendLine("<table>");
sb.Append("<thead><tr><th>Option</th>");
foreach (var criterion in criteria)
sb.Append($"<th>{Escape(criterion)}</th>");
sb.AppendLine("<th>Verdict</th><th>Notes</th></tr></thead>");
sb.AppendLine("<tbody>");
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("<tr>");
sb.Append($"<td><strong>{Escape(name)}</strong></td>");
foreach (var criterion in criteria)
{
var score = scoresEl.ValueKind == JsonValueKind.Object &&
scoresEl.SafeTryGetProperty(criterion, out var scoreEl)
? scoreEl.ToString()
: "-";
sb.Append($"<td>{Escape(score)}</td>");
}
sb.Append($"<td>{MarkdownToHtml(verdict ?? string.Empty)}</td>");
sb.Append($"<td>{MarkdownToHtml(notes ?? string.Empty)}</td>");
sb.AppendLine("</tr>");
}
sb.AppendLine("</tbody>");
sb.AppendLine("</table>");
sb.AppendLine("</div>");
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($"<h3>{Escape(title)}</h3>");
sb.AppendLine("<div class=\"metric-strip\" style=\"display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:.85rem;margin:1rem 0 1.35rem;\">");
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("<div style=\"border:1px solid #dbe4f0;border-radius:14px;padding:.9rem 1rem;background:#fff;box-shadow:0 8px 24px rgba(15,23,42,.05);\">");
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(trend))
sb.AppendLine($"<div style=\"font-size:.8rem;font-weight:700;color:#1d4ed8;margin-top:.28rem\">{Escape(trend)}</div>");
if (!string.IsNullOrWhiteSpace(note))
sb.AppendLine($"<div style=\"font-size:.82rem;color:#475569;margin-top:.28rem;line-height:1.55\">{MarkdownToHtml(note)}</div>");
sb.AppendLine("</div>");
}
sb.AppendLine("</div>");
return sb.ToString();
}
private static string RenderRoadmap(JsonElement s) private static string RenderRoadmap(JsonElement s)
{ {
if (!s.SafeTryGetProperty("phases", out var phases) || phases.ValueKind != JsonValueKind.Array) if (!s.SafeTryGetProperty("phases", out var phases) || phases.ValueKind != JsonValueKind.Array)

View File

@@ -104,6 +104,16 @@ internal static class WorkspaceContextGenerator
sb.AppendLine(); 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); var contextFiles = DetectContextFiles(workFolder);
if (contextFiles.Count > 0) if (contextFiles.Count > 0)
{ {

View File

@@ -442,7 +442,7 @@ public static class CodeLanguageCatalog
if (!string.IsNullOrWhiteSpace(primaryGuidance)) if (!string.IsNullOrWhiteSpace(primaryGuidance))
parts.Add("focus: " + primaryGuidance); parts.Add("focus: " + primaryGuidance);
if (string.Equals(capability.Key, "sql", StringComparison.OrdinalIgnoreCase)) 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) if (parts.Count == 0)
return capability.DisplayName; return capability.DisplayName;

View File

@@ -6,22 +6,31 @@ namespace AxCopilot.Services;
public sealed record SqlAnalysisReport( public sealed record SqlAnalysisReport(
string Dialect, string Dialect,
string ScriptIntent,
IReadOnlyList<string> StatementKinds, IReadOnlyList<string> StatementKinds,
IReadOnlyList<string> Objects, IReadOnlyList<string> Objects,
IReadOnlyList<string> Dependencies,
IReadOnlyList<string> Risks, IReadOnlyList<string> Risks,
IReadOnlyList<string> SuggestedChecks) IReadOnlyList<string> SuggestedChecks,
IReadOnlyList<string> ReviewNotes)
{ {
public string ToFallbackSummary() public string ToFallbackSummary()
{ {
var parts = new List<string> { $"SQL static analysis: {Dialect}" }; var parts = new List<string> { $"SQL static analysis: {Dialect}" };
if (!string.IsNullOrWhiteSpace(ScriptIntent))
parts.Add("script: " + ScriptIntent);
if (StatementKinds.Count > 0) if (StatementKinds.Count > 0)
parts.Add("statements: " + string.Join(", ", StatementKinds.Take(5))); parts.Add("statements: " + string.Join(", ", StatementKinds.Take(5)));
if (Objects.Count > 0) if (Objects.Count > 0)
parts.Add("objects: " + string.Join(", ", Objects.Take(6))); parts.Add("objects: " + string.Join(", ", Objects.Take(6)));
if (Dependencies.Count > 0)
parts.Add("dependencies: " + string.Join(", ", Dependencies.Take(6)));
if (Risks.Count > 0) if (Risks.Count > 0)
parts.Add("risks: " + string.Join(" | ", Risks.Take(3))); parts.Add("risks: " + string.Join(" | ", Risks.Take(3)));
if (SuggestedChecks.Count > 0) if (SuggestedChecks.Count > 0)
parts.Add("next checks: " + string.Join(" | ", SuggestedChecks.Take(3))); 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); return string.Join(Environment.NewLine, parts);
} }
} }
@@ -36,11 +45,14 @@ public static class SqlAnalysisService
var statements = SplitStatements(normalized); var statements = SplitStatements(normalized);
var statementKinds = DetectStatementKinds(normalized); var statementKinds = DetectStatementKinds(normalized);
var scriptIntent = ClassifyScriptIntent(statementKinds, filePath, normalized);
var objects = DetectObjects(normalized); var objects = DetectObjects(normalized);
var dependencies = DetectDependencies(normalized, objects);
var risks = DetectRisks(statements, normalized); var risks = DetectRisks(statements, normalized);
var checks = BuildSuggestedChecks(statementKinds, risks, dialect, filePath); 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) public static string BuildFallbackSummary(string? filePathOrExtension)
@@ -53,13 +65,20 @@ public static class SqlAnalysisService
return new SqlAnalysisReport( return new SqlAnalysisReport(
"Generic SQL", "Generic SQL",
"generic script",
["DDL/DML review"], ["DDL/DML review"],
[], [],
[],
["Review destructive statements and transaction boundaries before execution."], ["Review destructive statements and transaction boundaries before execution."],
[ [
"Confirm migration order, object dependencies, and rollback strategy", "Confirm migration order, object dependencies, and rollback strategy",
"Run the script in a disposable database before applying it to shared environments", "Run the script in a disposable database before applying it to shared environments",
"Review broad UPDATE/DELETE predicates, indexes, and constraint impact" "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(); ]).ToFallbackSummary();
} }
@@ -72,6 +91,7 @@ public static class SqlAnalysisService
AddKind(kinds, sql, @"\btruncate\s+table\b", "TRUNCATE TABLE"); 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+(unique\s+)?index\b", "CREATE INDEX");
AddKind(kinds, sql, @"\bcreate\s+view\b", "CREATE VIEW"); 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+)?function\b", "CREATE FUNCTION");
AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?procedure\b", "CREATE PROCEDURE"); AddKind(kinds, sql, @"\bcreate\s+(or\s+replace\s+)?procedure\b", "CREATE PROCEDURE");
AddKind(kinds, sql, @"\binsert\s+into\b", "INSERT"); AddKind(kinds, sql, @"\binsert\s+into\b", "INSERT");
@@ -111,6 +131,67 @@ public static class SqlAnalysisService
return names.Take(10).ToList(); return names.Take(10).ToList();
} }
private static IReadOnlyList<string> DetectDependencies(string sql, IReadOnlyList<string> objects)
{
var dependencies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var ownedObjects = DetectOwnedObjects(sql);
var patterns = new[]
{
@"\breferences\s+(?<name>[\[\]`\""\w\.]+)",
@"\bjoin\s+(?<name>[\[\]`\""\w\.]+)",
@"\bfrom\s+(?<name>[\[\]`\""\w\.]+)",
@"\binto\s+(?<name>[\[\]`\""\w\.]+)",
@"\bupdate\s+(?<name>[\[\]`\""\w\.]+)",
@"\bexec(?:ute)?\s+(?<name>[\[\]`\""\w\.]+)",
@"\bon\s+(?<name>[\[\]`\""\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<string> DetectOwnedObjects(string sql)
{
var patterns = new[]
{
@"\bcreate\s+table\s+(if\s+not\s+exists\s+)?(?<name>[\[\]`\""\w\.]+)",
@"\balter\s+table\s+(?<name>[\[\]`\""\w\.]+)",
@"\bdrop\s+table\s+(if\s+exists\s+)?(?<name>[\[\]`\""\w\.]+)",
@"\bcreate\s+view\s+(?<name>[\[\]`\""\w\.]+)",
@"\bcreate\s+(or\s+replace\s+)?function\s+(?<name>[\[\]`\""\w\.]+)",
@"\bcreate\s+(or\s+replace\s+)?procedure\s+(?<name>[\[\]`\""\w\.]+)",
@"\bcreate\s+(unique\s+)?index\s+(?<name>[\[\]`\""\w\.]+)",
@"\binsert\s+into\s+(?<name>[\[\]`\""\w\.]+)",
@"\bupdate\s+(?<name>[\[\]`\""\w\.]+)",
@"\bdelete\s+from\s+(?<name>[\[\]`\""\w\.]+)"
};
var names = new HashSet<string>(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<string> DetectRisks(IReadOnlyList<string> statements, string fullSql) private static IReadOnlyList<string> DetectRisks(IReadOnlyList<string> statements, string fullSql)
{ {
var risks = new List<string>(); var risks = new List<string>();
@@ -152,6 +233,26 @@ public static class SqlAnalysisService
return risks.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); return risks.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
} }
private static string ClassifyScriptIntent(IReadOnlyList<string> 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<string> BuildSuggestedChecks( private static IReadOnlyList<string> BuildSuggestedChecks(
IReadOnlyList<string> statementKinds, IReadOnlyList<string> statementKinds,
IReadOnlyList<string> risks, IReadOnlyList<string> risks,
@@ -185,6 +286,57 @@ public static class SqlAnalysisService
return checks.Distinct(StringComparer.OrdinalIgnoreCase).Take(5).ToList(); return checks.Distinct(StringComparer.OrdinalIgnoreCase).Take(5).ToList();
} }
private static IReadOnlyList<string> BuildReviewNotes(
IReadOnlyList<string> statementKinds,
IReadOnlyList<string> risks,
IReadOnlyList<string> dependencies,
string dialect,
string? filePath,
string scriptIntent)
{
var notes = new List<string>();
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<string> SplitStatements(string sql) private static IReadOnlyList<string> SplitStatements(string sql)
{ {
return sql return sql