ArchitectureReportService.java
package com.taxonomy.architecture.service;
import com.taxonomy.diagram.DiagramModel;
import com.taxonomy.dto.*;
import com.taxonomy.export.DiagramProjectionService;
import com.taxonomy.export.DiagramViewMetadata;
import com.taxonomy.export.MermaidExportService;
import com.taxonomy.preferences.PreferencesService;
import com.taxonomy.shared.config.ExportConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import com.taxonomy.dto.ArchitectureRecommendation;
import com.taxonomy.dto.ArchitectureReport;
import com.taxonomy.dto.DetectedPattern;
import com.taxonomy.dto.GapAnalysisView;
import com.taxonomy.dto.IncompletePattern;
import com.taxonomy.dto.MissingRelation;
import com.taxonomy.dto.PatternDetectionView;
import com.taxonomy.dto.RecommendedElement;
import com.taxonomy.dto.RelationProposalDto;
import com.taxonomy.dto.RequirementAnchor;
import com.taxonomy.dto.RequirementArchitectureView;
import com.taxonomy.dto.SuggestedRelation;
import com.taxonomy.relations.service.RelationProposalService;
/**
* Aggregates data from existing services and produces an {@link ArchitectureReport},
* which can then be rendered as Markdown, HTML, or DOCX.
*/
@Service
public class ArchitectureReportService {
private static final Logger log = LoggerFactory.getLogger(ArchitectureReportService.class);
private static final int DEFAULT_MIN_SCORE = 20;
private final RequirementArchitectureViewService architectureViewService;
private final ArchitectureGapService gapService;
private final ArchitecturePatternService patternService;
private final ArchitectureRecommendationService recommendationService;
private final DiagramProjectionService diagramProjectionService;
private final MermaidExportService mermaidExportService;
private final RelationProposalService proposalService;
private final PreferencesService preferencesService;
public ArchitectureReportService(RequirementArchitectureViewService architectureViewService,
ArchitectureGapService gapService,
ArchitecturePatternService patternService,
ArchitectureRecommendationService recommendationService,
DiagramProjectionService diagramProjectionService,
MermaidExportService mermaidExportService,
RelationProposalService proposalService,
PreferencesService preferencesService) {
this.architectureViewService = architectureViewService;
this.gapService = gapService;
this.patternService = patternService;
this.recommendationService = recommendationService;
this.diagramProjectionService = diagramProjectionService;
this.mermaidExportService = mermaidExportService;
this.proposalService = proposalService;
this.preferencesService = preferencesService;
}
/**
* Generates a full architecture report by calling all existing analysis services.
*
* @param scores nodeCode → score map (0–100)
* @param businessText the business requirement text
* @param minScore minimum score threshold (0 → default 20)
* @return populated {@link ArchitectureReport}
*/
@Transactional(readOnly = true)
public ArchitectureReport generateReport(Map<String, Integer> scores,
String businessText, int minScore) {
ArchitectureReport report = new ArchitectureReport();
report.setBusinessText(businessText);
report.setScores(scores != null ? scores : Map.of());
report.setGeneratedAt(Instant.now());
int threshold = minScore > 0 ? minScore : DEFAULT_MIN_SCORE;
Map<String, Integer> safeScores = scores != null ? scores : Map.of();
// 1. Architecture View
RequirementArchitectureView archView = architectureViewService.build(
safeScores, businessText, 20);
DiagramViewMetadata meta = ExportConfig.resolveViewMetadata(preferencesService);
archView.setViewTitle(meta.viewTitle());
archView.setViewDescription(meta.viewDescription());
archView.setContainmentEnabled(meta.containmentEnabled());
archView.setActiveRules(meta.activeRules());
report.setArchitectureView(archView);
// 2. Gap Analysis
GapAnalysisView gaps = gapService.analyze(safeScores, businessText, threshold);
report.setGapAnalysis(gaps);
// 3. Pattern Detection
PatternDetectionView patterns = patternService.detectForScores(safeScores, threshold);
report.setPatternDetection(patterns);
// 4. Recommendation
ArchitectureRecommendation recommendation = recommendationService.recommend(
safeScores, businessText, threshold);
report.setRecommendation(recommendation);
// 5. Pending Proposals
List<RelationProposalDto> pending = proposalService.getPendingProposals();
report.setPendingProposals(pending);
// 6. Mermaid Diagram
String title = businessText != null && businessText.length() > 60
? businessText.substring(0, 57) + "..."
: (businessText != null ? businessText : "Report");
DiagramModel diagram = diagramProjectionService.project(archView, title);
String mermaid = mermaidExportService.export(diagram);
report.setMermaidDiagram(mermaid);
log.info("Report generated: {} anchors, {} gaps, {} patterns, {} recommendations",
archView.getTotalAnchors(),
gaps.getTotalGaps(),
patterns.getMatchedPatterns().size(),
recommendation.getConfirmedElements().size());
return report;
}
/**
* Renders an {@link ArchitectureReport} as Markdown text.
*/
public String renderMarkdown(ArchitectureReport report) {
StringBuilder md = new StringBuilder();
md.append("# Architecture Analysis Report\n\n");
// Metadata
md.append("**Generated:** ").append(formatTimestamp(report.getGeneratedAt())).append(" \n");
md.append("**Business Requirement:** ").append(
report.getBusinessText() != null ? report.getBusinessText() : "N/A").append("\n\n");
// Architecture Summary
md.append("## 1. Architecture Summary\n\n");
RequirementArchitectureView view = report.getArchitectureView();
if (view != null) {
md.append("| Metric | Value |\n|---|---|\n");
md.append("| Anchors | ").append(view.getTotalAnchors()).append(" |\n");
md.append("| Elements | ").append(view.getTotalElements()).append(" |\n");
md.append("| Relationships | ").append(view.getTotalRelationships()).append(" |\n");
md.append("| Max Hop Distance | ").append(view.getMaxHopDistance()).append(" |\n\n");
if (!view.getAnchors().isEmpty()) {
md.append("### Anchor Nodes\n\n");
md.append("| Node | Score | Reason |\n|---|---|---|\n");
for (RequirementAnchor anchor : view.getAnchors()) {
md.append("| `").append(anchor.getNodeCode()).append("` | ");
md.append(anchor.getDirectScore()).append("% | ");
md.append(anchor.getReason() != null ? anchor.getReason() : "—").append(" |\n");
}
md.append("\n");
}
}
// Gap Analysis
md.append("## 2. Gap Analysis\n\n");
GapAnalysisView gaps = report.getGapAnalysis();
if (gaps != null) {
md.append("**Total Gaps:** ").append(gaps.getTotalGaps()).append("\n\n");
if (!gaps.getMissingRelations().isEmpty()) {
md.append("### Missing Relations\n\n");
md.append("| Source | Expected Type | Expected Target Root | Description |\n|---|---|---|---|\n");
for (MissingRelation mr : gaps.getMissingRelations()) {
md.append("| `").append(mr.getSourceNodeCode()).append("` | ");
md.append(mr.getExpectedRelationType()).append(" | ");
md.append(mr.getExpectedTargetRoot()).append(" | ");
md.append(mr.getDescription() != null ? mr.getDescription() : "—").append(" |\n");
}
md.append("\n");
}
if (!gaps.getIncompletePatterns().isEmpty()) {
md.append("### Incomplete Patterns\n\n");
md.append("| Node | Pattern | Missing |\n|---|---|---|\n");
for (IncompletePattern ip : gaps.getIncompletePatterns()) {
md.append("| `").append(ip.getNodeCode()).append("` | ");
md.append(ip.getPatternDescription()).append(" | ");
md.append(ip.getMissingElement() != null ? ip.getMissingElement() : "—").append(" |\n");
}
md.append("\n");
}
}
// Pattern Detection
md.append("## 3. Detected Patterns\n\n");
PatternDetectionView patterns = report.getPatternDetection();
if (patterns != null) {
md.append("**Pattern Coverage:** ").append(
String.format("%.1f%%", patterns.getPatternCoverage() * 100)).append("\n\n");
if (!patterns.getMatchedPatterns().isEmpty()) {
md.append("### Matched Patterns\n\n");
for (DetectedPattern dp : patterns.getMatchedPatterns()) {
md.append("- **").append(dp.getPatternName()).append("** — ");
md.append(String.format("%.0f%%", dp.getCompleteness() * 100)).append(" complete");
if (!dp.getPresentSteps().isEmpty()) {
md.append(" (").append(String.join(", ", dp.getPresentSteps())).append(")");
}
md.append("\n");
}
md.append("\n");
}
if (!patterns.getIncompletePatterns().isEmpty()) {
md.append("### Incomplete Patterns\n\n");
for (DetectedPattern dp : patterns.getIncompletePatterns()) {
md.append("- **").append(dp.getPatternName()).append("** — ");
md.append(String.format("%.0f%%", dp.getCompleteness() * 100)).append(" complete, missing: ");
md.append(String.join(", ", dp.getMissingSteps()));
md.append("\n");
}
md.append("\n");
}
}
// Recommendations
md.append("## 4. Recommendations\n\n");
ArchitectureRecommendation rec = report.getRecommendation();
if (rec != null) {
md.append("**Confidence:** ").append(String.format("%.1f%%", rec.getConfidence())).append("\n\n");
if (!rec.getConfirmedElements().isEmpty()) {
md.append("### Confirmed Elements\n\n");
md.append("| Node | Title | Root | Score | Reasoning |\n|---|---|---|---|---|\n");
for (RecommendedElement el : rec.getConfirmedElements()) {
md.append("| `").append(el.getNodeCode()).append("` | ");
md.append(el.getTitle()).append(" | ");
md.append(el.getTaxonomyRoot()).append(" | ");
md.append(el.getScore()).append("% | ");
md.append(el.getReasoning() != null ? el.getReasoning() : "—").append(" |\n");
}
md.append("\n");
}
if (!rec.getProposedElements().isEmpty()) {
md.append("### Proposed Elements\n\n");
md.append("| Node | Title | Root | Reasoning |\n|---|---|---|---|\n");
for (RecommendedElement el : rec.getProposedElements()) {
md.append("| `").append(el.getNodeCode()).append("` | ");
md.append(el.getTitle()).append(" | ");
md.append(el.getTaxonomyRoot()).append(" | ");
md.append(el.getReasoning() != null ? el.getReasoning() : "—").append(" |\n");
}
md.append("\n");
}
if (!rec.getSuggestedRelations().isEmpty()) {
md.append("### Suggested Relations\n\n");
md.append("| Source | → | Target | Type | Reasoning |\n|---|---|---|---|---|\n");
for (SuggestedRelation sr : rec.getSuggestedRelations()) {
md.append("| `").append(sr.getSourceCode()).append("` | → | ");
md.append("`").append(sr.getTargetCode()).append("` | ");
md.append(sr.getRelationType()).append(" | ");
md.append(sr.getReasoning() != null ? sr.getReasoning() : "—").append(" |\n");
}
md.append("\n");
}
}
// Pending Proposals
if (!report.getPendingProposals().isEmpty()) {
md.append("## 5. Pending Relation Proposals\n\n");
md.append("| Source | Target | Type | Confidence | Status |\n|---|---|---|---|---|\n");
for (RelationProposalDto p : report.getPendingProposals()) {
md.append("| `").append(p.getSourceCode()).append("` | ");
md.append("`").append(p.getTargetCode()).append("` | ");
md.append(p.getRelationType()).append(" | ");
md.append(String.format("%.0f%%", p.getConfidence() * 100)).append(" | ");
md.append(p.getStatus()).append(" |\n");
}
md.append("\n");
}
// Mermaid Diagram
if (report.getMermaidDiagram() != null && !report.getMermaidDiagram().isBlank()) {
md.append("## ").append(report.getPendingProposals().isEmpty() ? "5" : "6")
.append(". Architecture Diagram\n\n");
md.append("```mermaid\n");
md.append(report.getMermaidDiagram());
if (!report.getMermaidDiagram().endsWith("\n")) {
md.append("\n");
}
md.append("```\n\n");
}
md.append("---\n*Generated by Taxonomy Architecture Analyzer*\n");
return md.toString();
}
/**
* Renders an {@link ArchitectureReport} as a self-contained HTML document.
*/
public String renderHtml(ArchitectureReport report) {
String markdown = renderMarkdown(report);
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
html.append(" <meta charset=\"UTF-8\">\n");
html.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n");
html.append(" <title>Architecture Analysis Report</title>\n");
html.append(" <style>\n");
html.append(" body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; ");
html.append("max-width: 960px; margin: 2rem auto; padding: 0 1rem; line-height: 1.6; color: #333; }\n");
html.append(" h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 0.5rem; }\n");
html.append(" h2 { color: #34495e; margin-top: 2rem; }\n");
html.append(" h3 { color: #7f8c8d; }\n");
html.append(" table { border-collapse: collapse; width: 100%; margin: 1rem 0; }\n");
html.append(" th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
html.append(" th { background-color: #f8f9fa; font-weight: 600; }\n");
html.append(" tr:nth-child(even) { background-color: #f2f2f2; }\n");
html.append(" code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }\n");
html.append(" pre { background: #f8f9fa; padding: 1rem; border-radius: 4px; overflow-x: auto; }\n");
html.append(" .mermaid { background: #fff; padding: 1rem; border: 1px solid #eee; border-radius: 4px; }\n");
html.append(" hr { border: none; border-top: 1px solid #eee; margin: 2rem 0; }\n");
html.append(" strong { color: #2c3e50; }\n");
html.append(" </style>\n");
html.append("</head>\n<body>\n");
// Convert markdown to simple HTML
html.append(markdownToHtml(markdown));
html.append("\n</body>\n</html>\n");
return html.toString();
}
/**
* Renders an {@link ArchitectureReport} as a DOCX byte array.
* Uses Apache POI which is already a project dependency.
*/
public byte[] renderDocx(ArchitectureReport report) {
String markdown = renderMarkdown(report);
try (org.apache.poi.xwpf.usermodel.XWPFDocument doc =
new org.apache.poi.xwpf.usermodel.XWPFDocument()) {
// Title
org.apache.poi.xwpf.usermodel.XWPFParagraph title = doc.createParagraph();
title.setStyle("Title");
org.apache.poi.xwpf.usermodel.XWPFRun titleRun = title.createRun();
titleRun.setText("Architecture Analysis Report");
titleRun.setBold(true);
titleRun.setFontSize(18);
// Parse markdown lines and add as paragraphs
for (String line : markdown.split("\n")) {
if (line.startsWith("# ") && !line.startsWith("# Architecture")) {
// Skip duplicate title
continue;
}
org.apache.poi.xwpf.usermodel.XWPFParagraph para = doc.createParagraph();
org.apache.poi.xwpf.usermodel.XWPFRun run = para.createRun();
if (line.startsWith("## ")) {
run.setText(line.substring(3));
run.setBold(true);
run.setFontSize(14);
} else if (line.startsWith("### ")) {
run.setText(line.substring(4));
run.setBold(true);
run.setFontSize(12);
} else if (line.startsWith("**") && line.contains(":**")) {
// Bold label
String text = line.replaceAll("\\*\\*", "");
run.setText(text);
run.setBold(true);
} else if (line.startsWith("| ") && !line.startsWith("|---")) {
// Table row rendered as tab-separated line
String text = line.replaceAll("\\|", "\t").replaceAll("`", "").trim();
run.setText(text);
run.setFontSize(9);
} else if (line.startsWith("- **")) {
String text = line.replaceAll("\\*\\*", "").substring(2);
run.setText(text);
} else if (line.startsWith("```") || line.startsWith("---") || line.startsWith("|---")) {
// Skip formatting lines
continue;
} else {
run.setText(line.replaceAll("`", "").replaceAll("\\*", ""));
}
}
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
doc.write(baos);
return baos.toByteArray();
} catch (Exception e) {
log.error("DOCX generation failed: {}", e.getMessage(), e);
throw new RuntimeException("Failed to generate DOCX report", e);
}
}
// ── Internal helpers ──────────────────────────────────────────────────────
private String formatTimestamp(Instant instant) {
if (instant == null) return "N/A";
return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss 'UTC'")
.withZone(ZoneOffset.UTC)
.format(instant);
}
/**
* Simple markdown-to-HTML converter for report rendering.
* Handles headings, tables, bold text, code, and lists.
*/
String markdownToHtml(String markdown) {
StringBuilder html = new StringBuilder();
boolean inTable = false;
boolean inCodeBlock = false;
for (String line : markdown.split("\n")) {
if (line.startsWith("```")) {
if (inCodeBlock) {
html.append("</pre>\n");
inCodeBlock = false;
} else {
String lang = line.length() > 3 ? line.substring(3) : "";
html.append("<pre class=\"").append(escapeHtml(lang)).append("\">\n");
inCodeBlock = true;
}
continue;
}
if (inCodeBlock) {
html.append(escapeHtml(line)).append("\n");
continue;
}
// Close table if no longer in table row
if (inTable && !line.startsWith("|")) {
html.append("</tbody></table>\n");
inTable = false;
}
if (line.startsWith("# ")) {
html.append("<h1>").append(inlineFormat(line.substring(2))).append("</h1>\n");
} else if (line.startsWith("## ")) {
html.append("<h2>").append(inlineFormat(line.substring(3))).append("</h2>\n");
} else if (line.startsWith("### ")) {
html.append("<h3>").append(inlineFormat(line.substring(4))).append("</h3>\n");
} else if (line.startsWith("|---") || line.startsWith("| ---")) {
// Skip separator row
} else if (line.startsWith("| ")) {
String[] cells = line.split("\\|");
if (!inTable) {
html.append("<table><thead><tr>");
for (int i = 1; i < cells.length; i++) {
html.append("<th>").append(inlineFormat(cells[i].trim())).append("</th>");
}
html.append("</tr></thead><tbody>\n");
inTable = true;
} else {
html.append("<tr>");
for (int i = 1; i < cells.length; i++) {
html.append("<td>").append(inlineFormat(cells[i].trim())).append("</td>");
}
html.append("</tr>\n");
}
} else if (line.startsWith("- ")) {
html.append("<li>").append(inlineFormat(line.substring(2))).append("</li>\n");
} else if (line.startsWith("---")) {
html.append("<hr>\n");
} else if (line.startsWith("*") && line.endsWith("*")) {
html.append("<p><em>").append(inlineFormat(line.substring(1, line.length() - 1)))
.append("</em></p>\n");
} else if (!line.isBlank()) {
html.append("<p>").append(inlineFormat(line)).append("</p>\n");
}
}
if (inTable) {
html.append("</tbody></table>\n");
}
if (inCodeBlock) {
html.append("</pre>\n");
}
return html.toString();
}
private String inlineFormat(String text) {
// Bold — possessive quantifier (++) prevents backtracking (ReDoS) on unclosed markers
text = text.replaceAll("\\*\\*([^*]++)\\*\\*", "<strong>$1</strong>");
// Inline code — possessive quantifier (++) prevents backtracking (ReDoS) on unclosed markers
text = text.replaceAll("`([^`]++)`", "<code>$1</code>");
return text;
}
private String escapeHtml(String text) {
if (text == null) return "";
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """);
}
}