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 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 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 정도가 후보입니다.
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 마감 중심입니다.

View File

@@ -16,7 +16,9 @@ public class ArtifactQualityReviewServiceTests
<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>
<div class="kpi-panel"></div>
<div class="metric-strip"></div>
<div class="comparison-grid"></div>
<div class="decision-matrix"></div>
<div class="roadmap-block"></div>
<div class="decision-summary"></div>
<section class="board-report-panel"></section>
@@ -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 =
"""
<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 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]

View File

@@ -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");
}
}

View File

@@ -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));
}
}

View File

@@ -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");

View File

@@ -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));
}
}

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]
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 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 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 matrixCount = Regex.Matches(html, @"matrix-grid|risk-matrix", 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)
+ (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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

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

View File

@@ -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)을 지원합니다.
/// </summary>
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($"<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)
{
if (!s.SafeTryGetProperty("phases", out var phases) || phases.ValueKind != JsonValueKind.Array)

View File

@@ -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)
{

View File

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

View File

@@ -6,22 +6,31 @@ namespace AxCopilot.Services;
public sealed record SqlAnalysisReport(
string Dialect,
string ScriptIntent,
IReadOnlyList<string> StatementKinds,
IReadOnlyList<string> Objects,
IReadOnlyList<string> Dependencies,
IReadOnlyList<string> Risks,
IReadOnlyList<string> SuggestedChecks)
IReadOnlyList<string> SuggestedChecks,
IReadOnlyList<string> ReviewNotes)
{
public string ToFallbackSummary()
{
var parts = new List<string> { $"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<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)
{
var risks = new List<string>();
@@ -152,6 +233,26 @@ public static class SqlAnalysisService
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(
IReadOnlyList<string> statementKinds,
IReadOnlyList<string> risks,
@@ -185,6 +286,57 @@ public static class SqlAnalysisService
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)
{
return sql