feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a8d79e4484
commit
6a7632146e
490 changed files with 213249 additions and 2 deletions
211
plugins/ms-ai-architect/scripts/export-pdf.py
Executable file
211
plugins/ms-ai-architect/scripts/export-pdf.py
Executable file
|
|
@ -0,0 +1,211 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a professional PDF from a markdown file.
|
||||
|
||||
Requirements:
|
||||
pip install markdown weasyprint
|
||||
|
||||
Usage:
|
||||
python scripts/export-pdf.py <input.md> [output.pdf]
|
||||
|
||||
If output is not specified, uses the same name as input with .pdf extension.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import markdown
|
||||
from weasyprint import HTML
|
||||
|
||||
# --- CSS ---
|
||||
|
||||
CSS = """
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 25mm 20mm 25mm 20mm;
|
||||
|
||||
@bottom-center {
|
||||
content: counter(page);
|
||||
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||
font-size: 8pt;
|
||||
color: #718096;
|
||||
}
|
||||
}
|
||||
|
||||
@page :first {
|
||||
@bottom-center { content: none; }
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 10.5pt;
|
||||
line-height: 1.6;
|
||||
color: #1a202c;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20pt;
|
||||
font-weight: 700;
|
||||
color: #1a365d;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 12px;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 15pt;
|
||||
font-weight: 700;
|
||||
color: #1a365d;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 12pt;
|
||||
font-weight: 600;
|
||||
color: #2b6cb0;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 8px;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 10.5pt;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0 20px 0;
|
||||
font-size: 9pt;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
thead { display: table-header-group; }
|
||||
tr { page-break-inside: avoid; }
|
||||
|
||||
th {
|
||||
background-color: #2b6cb0;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
font-size: 8.5pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 7px 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr:nth-child(even) td { background-color: #f7fafc; }
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid #2b6cb0;
|
||||
margin: 12px 0;
|
||||
padding: 8px 16px;
|
||||
background: #ebf8ff;
|
||||
color: #2a4365;
|
||||
font-size: 10pt;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #edf2f7;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 9pt;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
ul, ol { margin: 8px 0 12px 0; padding-left: 24px; }
|
||||
li { margin-bottom: 4px; }
|
||||
strong { font-weight: 600; color: #1a202c; }
|
||||
a { color: #2b6cb0; text-decoration: none; }
|
||||
p { margin: 8px 0; }
|
||||
|
||||
.section-break { page-break-before: always; }
|
||||
.score-high { color: #276749; font-weight: 700; }
|
||||
.score-medium { color: #d69e2e; font-weight: 700; }
|
||||
.score-low { color: #c53030; font-weight: 700; }
|
||||
"""
|
||||
|
||||
|
||||
def postprocess_html(html: str) -> str:
|
||||
"""Add CSS classes for scores and risk levels."""
|
||||
# Section breaks on h2 (except first)
|
||||
h2_count = 0
|
||||
|
||||
def add_section_break(match: re.Match) -> str:
|
||||
nonlocal h2_count
|
||||
h2_count += 1
|
||||
if h2_count > 1:
|
||||
return f'<h2 class="section-break">{match.group(1)}</h2>'
|
||||
return match.group(0)
|
||||
|
||||
html = re.sub(r"<h2>(.*?)</h2>", add_section_break, html)
|
||||
|
||||
# Score coloring: 4/5, 5/5 green; 3/5 yellow; 1/5, 2/5 red
|
||||
html = re.sub(r"<td>([45])/5</td>", r'<td><span class="score-high">\1/5</span></td>', html)
|
||||
html = re.sub(r"<td>3/5</td>", '<td><span class="score-medium">3/5</span></td>', html)
|
||||
html = re.sub(r"<td>([12])/5</td>", r'<td><span class="score-low">\1/5</span></td>', html)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python export-pdf.py <input.md> [output.pdf]")
|
||||
sys.exit(1)
|
||||
|
||||
input_path = Path(sys.argv[1])
|
||||
if not input_path.exists():
|
||||
print(f"Error: {input_path} not found")
|
||||
sys.exit(1)
|
||||
|
||||
output_path = Path(sys.argv[2]) if len(sys.argv) > 2 else input_path.with_suffix(".pdf")
|
||||
|
||||
md_text = input_path.read_text(encoding="utf-8")
|
||||
body_html = markdown.markdown(md_text, extensions=["tables", "smarty", "sane_lists"])
|
||||
body_html = postprocess_html(body_html)
|
||||
|
||||
full_html = f"""<!DOCTYPE html>
|
||||
<html lang="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>{CSS}</style>
|
||||
</head>
|
||||
<body>
|
||||
{body_html}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
HTML(string=full_html).write_pdf(str(output_path))
|
||||
print(f"PDF generated: {output_path}")
|
||||
print(f"Size: {output_path.stat().st_size / 1024:.1f} KB")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue