qgis-plugin-analyzer 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,388 @@
1
+ """HTML reporter for project analysis results.
2
+
3
+ This module provides functions to generate professional HTML reports
4
+ of the analysis findings, with a clean and responsive design.
5
+ """
6
+
7
+ import datetime
8
+ import pathlib
9
+ from typing import Any, Dict, List
10
+
11
+
12
+ def _get_html_styles() -> List[str]:
13
+ """Returns the CSS styles for the HTML report.
14
+
15
+ Returns:
16
+ A list of HTML <style> block lines.
17
+ """
18
+ return [
19
+ "<style>",
20
+ "body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; max-width: 1000px; margin: 0 auto; padding: 20px; background-color: #f4f7f9; }",
21
+ ".header { background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 30px; border-radius: 8px; margin-bottom: 30px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }",
22
+ ".card { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }",
23
+ ".score-container { display: flex; gap: 20px; margin-top: 20px; }",
24
+ ".score-box { flex: 1; text-align: center; padding: 15px; border-radius: 8px; background: #ecf0f1; border-bottom: 4px solid #bdc3c7; color: #2c3e50; }",
25
+ ".score-box.high { border-color: #27ae60; }",
26
+ ".score-box.medium { border-color: #f1c40f; }",
27
+ ".score-box.low { border-color: #e74c3c; }",
28
+ ".score-label { display: block; font-size: 0.9em; font-weight: bold; margin-bottom: 5px; }",
29
+ ".score-value { font-size: 2em; font-weight: bold; display: block; }",
30
+ ".issue { border-left: 4px solid #eee; padding-left: 15px; margin-bottom: 10px; }",
31
+ ".issue.high { border-color: #e74c3c; }",
32
+ ".issue.medium { border-color: #f1c40f; }",
33
+ ".severity { font-weight: bold; text-transform: uppercase; font-size: 0.8em; }",
34
+ ".file-path { color: #7f8c8d; font-size: 0.9em; }",
35
+ "code { background: #f8f9fa; padding: 2px 4px; border-radius: 4px; font-size: 0.9em; }",
36
+ "pre { background: #2d3436; color: #dfe6e9; padding: 15px; border-radius: 5px; overflow-x: auto; }",
37
+ "h2 { color: #2c3e50; border-bottom: 2px solid #eee; padding-bottom: 10px; }",
38
+ ".section { margin-bottom: 30px; }",
39
+ ".metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-top: 20px; }",
40
+ ".metric-card { background: #f0f4f7; padding: 15px; border-radius: 8px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }",
41
+ ".metric-label { display: block; font-size: 0.9em; color: #555; margin-bottom: 5px; }",
42
+ ".metric-value { font-size: 1.8em; font-weight: bold; color: #2c3e50; }",
43
+ "</style></head><body>",
44
+ ]
45
+
46
+
47
+ def _build_html_header(
48
+ project_name: str,
49
+ project_label: str,
50
+ now: str,
51
+ q_score: float,
52
+ m_score: float,
53
+ cls_q: str,
54
+ cls_m: str,
55
+ c_score: float,
56
+ cls_c: str,
57
+ project_type: str,
58
+ ) -> str:
59
+ """Builds the HTML header section including quality scores.
60
+
61
+ Args:
62
+ project_name: Name of the project.
63
+ project_label: Label describing project type.
64
+ now: Current timestamp string.
65
+ q_score: Module stability score.
66
+ m_score: Maintainability score.
67
+ cls_q: CSS class for quality score.
68
+ cls_m: CSS class for maintainability score.
69
+ c_score: QGIS compliance score.
70
+ cls_c: CSS class for compliance score.
71
+ project_type: Project type ID.
72
+
73
+ Returns:
74
+ The HTML string for the header.
75
+ """
76
+ html = [
77
+ "<!DOCTYPE html>",
78
+ f"<html><head><meta charset='utf-8'><title>Analysis Report - {project_name}</title>",
79
+ ]
80
+ html.extend(_get_html_styles())
81
+ html.extend(
82
+ [
83
+ "<div class='header'>",
84
+ f"<h1>šŸ“Š {project_label} Analysis: {project_name}</h1>",
85
+ f"<p>Generated on: {now}</p>",
86
+ "<div class='score-container'>",
87
+ f"<div class='score-box {cls_q}'><span class='score-label'>Module Stability</span><span class='score-value'>{q_score}/100</span></div>",
88
+ f"<div class='score-box {cls_m}'><span class='score-label'>Maintainability</span><span class='score-value'>{m_score}/100</span></div>",
89
+ ]
90
+ )
91
+
92
+ if project_type == "qgis":
93
+ html.append(
94
+ f"<div class='score-box {cls_c}'><span class='score-label'>QGIS Compliance</span><span class='score-value'>{c_score}/100</span></div>"
95
+ )
96
+
97
+ html.append("</div></div>")
98
+ return "".join(html)
99
+
100
+
101
+ def _build_html_qgis_findings(analyses: Dict[str, Any]) -> str:
102
+ """Builds the QGIS standard findings section for HTML.
103
+
104
+ Args:
105
+ analyses: The full analysis results dictionary.
106
+
107
+ Returns:
108
+ The HTML string for the findings section.
109
+ """
110
+ html = ["<div class='card'><h2>šŸ› ļø QGIS Standard Findings</h2>"]
111
+ compliance = analyses.get("qgis_compliance", {})
112
+ best_practices = compliance.get("best_practices", {})
113
+ issues = best_practices.get("issues", [])
114
+
115
+ if not issues:
116
+ html.append("<p>āœ… No major QGIS standard deviations found.</p>")
117
+ else:
118
+ for issue in issues:
119
+ severity = issue.get("severity", "medium")
120
+ html.append(f"<div class='issue {severity}'>")
121
+ html.append(f"<span class='severity'>{severity}</span> - {issue['message']}<br>")
122
+ html.append(f"<span class='file-path'>{issue['file']}:{issue['line']}</span>")
123
+ if issue.get("code"):
124
+ html.append(f"<pre>{issue['code']}</pre>")
125
+ html.append("</div>")
126
+
127
+ html.append("</div>")
128
+ return "".join(html)
129
+
130
+
131
+ def _build_html_semantic_section(semantic: Dict[str, Any]) -> str:
132
+ """Builds the semantic analysis HTML section (cycles, coupling).
133
+
134
+ Args:
135
+ semantic: The semantic analysis results dictionary.
136
+
137
+ Returns:
138
+ The HTML string for the semantic section.
139
+ """
140
+ html = ["<div class='card'><h2>🧠 Semantic Analysis</h2>"]
141
+
142
+ cycles = semantic.get("circular_dependencies", [])
143
+ if cycles:
144
+ html.append(
145
+ f"<div class='issue high'><b>Circular Dependencies Detected:</b> {len(cycles)}<br>"
146
+ )
147
+ html.append("<ul>")
148
+ for cycle in cycles:
149
+ html.append(f"<li><code>{' -> '.join(cycle)}</code></li>")
150
+ html.append("</ul></div>")
151
+
152
+ missing_res = semantic.get("missing_resources", [])
153
+ if missing_res:
154
+ html.append(
155
+ f"<div class='issue medium'><b>Missing Resources:</b> {len(missing_res)} (Defined in code but missing in QRC)<br>"
156
+ )
157
+ html.append("<ul>")
158
+ for res in missing_res[:10]:
159
+ html.append(f"<li>{res}</li>")
160
+ if len(missing_res) > 10:
161
+ html.append(f"<li>... ({len(missing_res) - 10} more)</li>")
162
+ html.append("</ul></div>")
163
+
164
+ metrics = semantic.get("coupling_metrics", {})
165
+ if metrics:
166
+ html.append("<h3>Module Coupling</h3>")
167
+ html.append(
168
+ "<table style='width:100%; border-collapse: collapse;'><thead><tr style='background:#eee;'><th>Module</th><th>Fan-In (Incoming)</th><th>Fan-Out (Outgoing)</th></tr></thead><tbody>"
169
+ )
170
+ for mod, vals in sorted(metrics.items(), key=lambda x: x[1]["fan_in"], reverse=True)[:10]:
171
+ html.append(
172
+ f"<tr><td style='border:1px solid #ddd; padding:8px;'>{mod}</td><td style='border:1px solid #ddd; padding:8px;'>{vals['fan_in']}</td><td style='border:1px solid #ddd; padding:8px;'>{vals['fan_out']}</td></tr>"
173
+ )
174
+ html.append("</tbody></table>")
175
+
176
+ html.append("</div>")
177
+ return "".join(html)
178
+
179
+
180
+ def _build_html_repo_compliance(repo_comp: Dict[str, Any]) -> str:
181
+ """Builds the repository compliance HTML section.
182
+
183
+ Args:
184
+ repo_comp: The repository compliance results dictionary.
185
+
186
+ Returns:
187
+ The HTML string for the repository compliance section.
188
+ """
189
+ is_compliant = repo_comp.get("is_compliant", False)
190
+ status_icon = "āœ…" if is_compliant else "āš ļø"
191
+ html = [f"<div class='card'><h2>{status_icon} Repository Compliance</h2>"]
192
+
193
+ binaries = repo_comp.get("binaries", [])
194
+ if binaries:
195
+ html.append(
196
+ f"<div class='issue high'><b>Prohibited Binaries Detected:</b> {len(binaries)}</div>"
197
+ )
198
+ html.append("<ul>")
199
+ for binary in binaries[:10]:
200
+ html.append(f"<li><code>{binary}</code></li>")
201
+ if len(binaries) > 10:
202
+ html.append(f"<li>... ({len(binaries) - 10} more)</li>")
203
+ html.append("</ul>")
204
+ else:
205
+ html.append("<div class='info'>āœ… No prohibited binaries found</div>")
206
+
207
+ package_size = repo_comp.get("package_size_mb", 0)
208
+ if package_size > 20:
209
+ html.append(
210
+ f"<div class='issue medium'><b>Package Size:</b> {package_size:.2f} MB (exceeds 20MB limit)</div>"
211
+ )
212
+ else:
213
+ html.append(f"<div class='info'><b>Package Size:</b> {package_size:.2f} MB</div>")
214
+
215
+ url_status = repo_comp.get("url_validation", {})
216
+ if url_status:
217
+ ok_count = sum(1 for status in url_status.values() if status == "ok")
218
+ total = len(url_status)
219
+ if ok_count == total:
220
+ html.append(
221
+ f"<div class='info'>āœ… <b>URL Validation:</b> All {total} links working</div>"
222
+ )
223
+ else:
224
+ html.append(
225
+ f"<div class='issue medium'><b>URL Validation:</b> {ok_count}/{total} links working</div>"
226
+ )
227
+ html.append("<ul>")
228
+ for url, status in url_status.items():
229
+ if status != "ok":
230
+ html.append(f"<li>{url}: <code>{status}</code></li>")
231
+ html.append("</ul>")
232
+
233
+ html.append("</div>")
234
+ return "".join(html)
235
+
236
+
237
+ def _build_html_ruff_findings(ruff_findings: List[Dict[str, Any]]) -> str:
238
+ """Builds the Ruff findings HTML section.
239
+
240
+ Args:
241
+ ruff_findings: List of Ruff finding dictionaries.
242
+
243
+ Returns:
244
+ The HTML string for the Ruff section.
245
+ """
246
+ html = ["<div class='card'><h2>šŸ Python Linting (Ruff)</h2>"]
247
+ for find in ruff_findings[:50]:
248
+ html.append("<div class='issue medium'>")
249
+ html.append(
250
+ f"<span class='severity'>{find.get('code', 'LINT')}</span> - {find.get('message')}<br>"
251
+ )
252
+ html.append(
253
+ f"<span class='file-path'>{find.get('filename')}:{find.get('location', {}).get('row', 0)}</span>"
254
+ )
255
+ html.append("</div>")
256
+ html.append("</div>")
257
+ return "".join(html)
258
+
259
+
260
+ def _build_html_research_section(research_summary: Dict[str, Any]) -> str:
261
+ """Builds the research-based metrics HTML section.
262
+
263
+ Args:
264
+ research_summary: The research summary dictionary.
265
+
266
+ Returns:
267
+ The HTML string for the modernization section.
268
+ """
269
+ if not research_summary:
270
+ return ""
271
+
272
+ styles = ", ".join(research_summary.get("detected_docstring_styles", [])) or "PEP 257 (Default)"
273
+ th_cov = research_summary.get("type_hint_coverage", 0)
274
+ ds_cov = research_summary.get("docstring_coverage", 0)
275
+ ret_cov = research_summary.get("return_hint_coverage", 0)
276
+
277
+ return f"""
278
+ <div class="section card">
279
+ <h2>šŸ”¬ Research-based Modernization</h2>
280
+ <p style="color: #7f8c8d; margin-bottom: 15px;">Metrics inspired by Google, Microsoft, and PSF standards.</p>
281
+ <div class="metrics-grid">
282
+ <div class="metric-card">
283
+ <span class="metric-label">Parameters Type Hints</span>
284
+ <span class="metric-value">{th_cov}%</span>
285
+ </div>
286
+ <div class="metric-card">
287
+ <span class="metric-label">Return Type Hints</span>
288
+ <span class="metric-value">{ret_cov}%</span>
289
+ </div>
290
+ <div class="metric-card">
291
+ <span class="metric-label">Docstring Coverage</span>
292
+ <span class="metric-value">{ds_cov}%</span>
293
+ </div>
294
+ <div class="metric-card">
295
+ <span class="metric-label">Detected Style</span>
296
+ <span class="metric-value" style="font-size: 1.2em;">{styles}</span>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ """
301
+
302
+
303
+ def _build_html_general_metrics(metrics: Dict[str, Any]) -> str:
304
+ """Builds the general metrics HTML section.
305
+
306
+ Args:
307
+ metrics: The general metrics dictionary.
308
+
309
+ Returns:
310
+ The HTML string for the general metrics section.
311
+ """
312
+ html = ["<div class='card'><h2>šŸ“ˆ General Metrics</h2><ul>"]
313
+ for k, v in metrics.items():
314
+ if k not in ["quality_score", "maintainability_score", "overall_score"]:
315
+ html.append(f"<li><b>{k.replace('_', ' ').title()}</b>: {v}</li>")
316
+ html.append("</ul></div>")
317
+ return "".join(html)
318
+
319
+
320
+ def generate_html_report(analyses: Dict[str, Any], output_path: pathlib.Path) -> None:
321
+ """Generates a professional HTML report.
322
+
323
+ Args:
324
+ analyses: The full analysis results dictionary.
325
+ output_path: Path where the HTML file will be saved.
326
+ """
327
+ metrics = analyses.get("metrics", {})
328
+ ruff_findings = analyses.get("ruff_findings", [])
329
+ project_type = analyses.get("project_type", "qgis")
330
+ project_label = "QGIS Plugin" if project_type == "qgis" else "Python"
331
+ project_name = analyses.get("project_name", project_label)
332
+ now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
333
+
334
+ q_score = metrics.get("quality_score", 0)
335
+ cls_q = "high" if q_score >= 80 else "medium" if q_score >= 50 else "low"
336
+
337
+ m_score = metrics.get("maintainability_score", 0)
338
+ cls_m = "high" if m_score >= 80 else "medium" if m_score >= 50 else "low"
339
+
340
+ c_score = 0
341
+ cls_c = "low"
342
+ if project_type == "qgis":
343
+ compliance = analyses.get("qgis_compliance", {})
344
+ c_score = compliance.get("compliance_score", 0)
345
+ cls_c = "high" if c_score >= 80 else "medium" if c_score >= 50 else "low"
346
+
347
+ # Build header and scores
348
+ html_body = _build_html_header(
349
+ project_name,
350
+ project_label,
351
+ now,
352
+ q_score,
353
+ m_score,
354
+ cls_q,
355
+ cls_m,
356
+ c_score,
357
+ cls_c,
358
+ project_type,
359
+ )
360
+
361
+ # QGIS Findings
362
+ if analyses.get("qgis_compliance"):
363
+ html_body += _build_html_qgis_findings(analyses)
364
+
365
+ # Add research section
366
+ html_body += _build_html_research_section(analyses.get("research_summary", {}))
367
+
368
+ # Semantic Section
369
+ semantic = analyses.get("semantic", {})
370
+ if semantic:
371
+ html_body += _build_html_semantic_section(semantic)
372
+
373
+ # Repository Compliance
374
+ repo_comp = analyses.get("repository_compliance", {})
375
+ if repo_comp:
376
+ html_body += _build_html_repo_compliance(repo_comp)
377
+
378
+ # Ruff Findings
379
+ if ruff_findings:
380
+ html_body += _build_html_ruff_findings(ruff_findings)
381
+
382
+ # General Metrics
383
+ html_body += _build_html_general_metrics(metrics)
384
+
385
+ # Footer
386
+ html_body += "</body></html>"
387
+
388
+ output_path.write_text(html_body, encoding="utf-8")
@@ -0,0 +1,212 @@
1
+ """Markdown reporter for project analysis results.
2
+
3
+ This module provides functions to generate professional Markdown summaries
4
+ of the analysis findings, including scores, metrics, and technical debt.
5
+ """
6
+
7
+ import datetime
8
+ import json
9
+ import pathlib
10
+ from typing import Any, Dict, List
11
+
12
+
13
+ def _build_markdown_header(
14
+ analyses: Dict[str, Any], module_score: float, maint_score: float, project_type: str
15
+ ) -> List[str]:
16
+ """Builds the markdown header section including quality indicators.
17
+
18
+ Args:
19
+ analyses: The full analysis results dictionary.
20
+ module_score: The calculated module stability score.
21
+ maint_score: The calculated maintainability score.
22
+ project_type: The type of project ("qgis" or "generic").
23
+
24
+ Returns:
25
+ A list of Markdown lines for the header.
26
+ """
27
+ project_label = "QGIS Plugin" if project_type == "qgis" else "Python Project"
28
+ lines = [
29
+ f"# šŸ“‹ Project Analysis Report: {analyses.get('project_name', project_label)}",
30
+ f"*Generated on: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*",
31
+ "",
32
+ "## šŸ“Š Quality Indicators",
33
+ f"- **Module Stability Score**: `{module_score}/100` (Based on file-level complexity)",
34
+ f"- **Code Maintainability Score**: `{maint_score}/100` (Based on function-level complexity)",
35
+ ]
36
+
37
+ if project_type == "qgis":
38
+ metrics = analyses.get("metrics", {})
39
+ overall = metrics.get("overall_score", 0)
40
+ lines.append(f"- **Overall Plugin Score**: `{overall}/100`")
41
+
42
+ compliance = analyses.get("qgis_compliance", {})
43
+ qgis_score = compliance.get("compliance_score", 0)
44
+ lines.append(f"- **QGIS Compliance**: `{qgis_score}/100`")
45
+
46
+ lines.append("")
47
+ return lines
48
+
49
+
50
+ def _build_markdown_qgis_findings(analyses: Dict[str, Any]) -> List[str]:
51
+ """Builds the QGIS findings section with icons and severities.
52
+
53
+ Args:
54
+ analyses: The full analysis results dictionary.
55
+
56
+ Returns:
57
+ A list of Markdown lines for the QGIS findings section.
58
+ """
59
+ lines = []
60
+ compliance = analyses.get("qgis_compliance", {})
61
+ best_practices = compliance.get("best_practices", {})
62
+ lines.append("## šŸ› ļø QGIS Standard Findings")
63
+ lines.append(f"Detected **{best_practices.get('issues_count', 0)}** technical deviations.")
64
+
65
+ for issue in best_practices.get("issues", []):
66
+ icon = "šŸ”“" if issue["severity"] == "high" else "🟔"
67
+ lines.append(f"- {icon} `{issue['file']}:{issue['line']}`: {issue['message']}")
68
+
69
+ repo_stats = compliance.get("repository_standards", {})
70
+ meta = repo_stats.get("metadata", {})
71
+ status_meta = "āœ… OK" if meta.get("is_valid") else "šŸ› ļø Needs Attention"
72
+ lines.append(f"- **Metadata (metadata.txt)**: {status_meta}")
73
+ if not meta.get("is_valid"):
74
+ lines.append(f" - Missing fields: `{', '.join(meta.get('missing', []))}`")
75
+
76
+ return lines
77
+
78
+
79
+ def _build_markdown_semantic_section(semantic: Dict[str, Any]) -> List[str]:
80
+ """Builds the semantic analysis section (circular imports, resources).
81
+
82
+ Args:
83
+ semantic: The semantic analysis dictionary.
84
+
85
+ Returns:
86
+ A list of Markdown lines for the semantic section.
87
+ """
88
+ lines = ["\n## 🧠 Semantic Analysis"]
89
+ cycles = semantic.get("circular_dependencies", [])
90
+ missing_res = semantic.get("missing_resources", [])
91
+
92
+ if cycles:
93
+ lines.append("šŸ”“ **Circular Import Cycles Detected:**")
94
+ for cycle in cycles:
95
+ lines.append(f" - `{' -> '.join(cycle)}`")
96
+ else:
97
+ lines.append("- No circular imports detected.")
98
+
99
+ if missing_res:
100
+ lines.append(
101
+ f"\n🟔 **Missing Resources**: {len(missing_res)} found (used in code but not in QRC)"
102
+ )
103
+ for res in missing_res[:10]:
104
+ lines.append(f" - `{res}`")
105
+ if len(missing_res) > 10:
106
+ lines.append(f" - ... ({len(missing_res) - 10} more)")
107
+ else:
108
+ lines.append("- All resource paths validated.")
109
+
110
+ return lines
111
+
112
+
113
+ def _build_markdown_repo_standards(analyses: Dict[str, Any]) -> List[str]:
114
+ """Builds the official repository standards compliance section.
115
+
116
+ Args:
117
+ analyses: The full analysis results dictionary.
118
+
119
+ Returns:
120
+ A list of Markdown lines for the repository standards section.
121
+ """
122
+ lines = ["\n## šŸ“¦ Official Repository Standards"]
123
+ compliance = analyses.get("qgis_compliance", {})
124
+ repo_stats = compliance.get("repository_standards", {})
125
+ struct = repo_stats.get("structure", {})
126
+ status = "āœ… OK" if struct.get("is_valid") else "āŒ Incomplete"
127
+ lines.append(f"- **File Structure**: {status}")
128
+
129
+ if not struct.get("is_valid"):
130
+ missing_files = [f for f, found in struct.get("files", {}).items() if not found]
131
+ if missing_files:
132
+ lines.append(f" - Missing: `{', '.join(missing_files)}`")
133
+ if not struct.get("has_class_factory"):
134
+ lines.append(" - Missing `classFactory` in `__init__.py`")
135
+
136
+ meta = repo_stats.get("metadata", {})
137
+ status_meta = "āœ… OK" if meta.get("is_valid") else "šŸ› ļø Needs Attention"
138
+ lines.append(f"- **Metadata (metadata.txt)**: {status_meta}")
139
+ if not meta.get("is_valid"):
140
+ lines.append(f" - Missing fields: `{', '.join(meta.get('missing', []))}`")
141
+
142
+ return lines
143
+
144
+
145
+ def generate_markdown_summary(analyses: Dict[str, Any], output_path: pathlib.Path) -> None:
146
+ """Generates a professional PROJECT_SUMMARY.md report.
147
+
148
+ Args:
149
+ analyses: The full analysis results dictionary.
150
+ output_path: Path where the Markdown file will be saved.
151
+ """
152
+ metrics = analyses["metrics"]
153
+ research = analyses.get("research_summary", {})
154
+ project_type = analyses.get("project_type", "qgis")
155
+
156
+ with open(output_path, "w", encoding="utf-8") as f:
157
+ # Re-use header and quality scores
158
+ f.write(
159
+ "\n".join(
160
+ _build_markdown_header(
161
+ analyses,
162
+ metrics.get("quality_score", 0),
163
+ metrics.get("maintainability_score", 0),
164
+ project_type,
165
+ )
166
+ )
167
+ )
168
+ f.write("\n")
169
+
170
+ if research:
171
+ f.write("\n## šŸ”¬ Research-based Metrics\n")
172
+ f.write(
173
+ f"- **Type Hint Coverage (Params)**: {research.get('type_hint_coverage')}% (Microsoft/Dropbox Std)\n"
174
+ )
175
+ f.write(
176
+ f"- **Type Hint Coverage (Returns)**: {research.get('return_hint_coverage')}% \n"
177
+ )
178
+ f.write(f"- **Docstring Coverage**: {research.get('docstring_coverage')}% (PEP 257)\n")
179
+ styles = ", ".join(research.get("detected_docstring_styles", [])) or "PEP 257 (Default)"
180
+ f.write(f"- **Detected Documentation Style**: {styles}\n")
181
+
182
+ f.write("\n## šŸ“Š General Metrics\n")
183
+ for k, v in metrics.items():
184
+ if k not in ["quality_score", "maintainability_score", "overall_score"]:
185
+ f.write(f"- **{k.replace('_', ' ').title()}**: {v}\n")
186
+
187
+ # QGIS-specific findings
188
+ if project_type == "qgis":
189
+ qgis_findings_lines = _build_markdown_qgis_findings(analyses)
190
+ f.write("\n".join(qgis_findings_lines))
191
+
192
+ # Semantic analysis
193
+ semantic = analyses.get("semantic", {})
194
+ if semantic:
195
+ semantic_lines = _build_markdown_semantic_section(semantic)
196
+ f.write("\n".join(semantic_lines))
197
+
198
+ # Repository standards (QGIS only)
199
+ if project_type == "qgis":
200
+ repo_standards_lines = _build_markdown_repo_standards(analyses)
201
+ f.write("\n".join(repo_standards_lines))
202
+
203
+
204
+ def save_json_context(analyses: Dict[str, Any], output_path: pathlib.Path) -> None:
205
+ """Saves the full context in JSON format.
206
+
207
+ Args:
208
+ analyses: The full analysis results dictionary.
209
+ output_path: Path where the JSON file will be saved.
210
+ """
211
+ with open(output_path, "w", encoding="utf-8") as f:
212
+ json.dump(analyses, f, indent=2, ensure_ascii=False)