bmad-plus 0.12.1 → 0.13.0

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.
Files changed (176) hide show
  1. package/CHANGELOG.md +637 -580
  2. package/README.md +123 -120
  3. package/osint-agent-package/agents/osint-investigator.md +12 -0
  4. package/osint-agent-package/skills/bmad-osint-investigate/osint/SKILL.md +491 -482
  5. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/dossier-template.md +126 -126
  6. package/osint-agent-package/skills/bmad-osint-investigate/osint/assets/lawful-basis-record.md +48 -48
  7. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/content-extraction.md +100 -100
  8. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/gdpr-osint.md +48 -48
  9. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/platforms.md +130 -130
  10. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/psychoprofile.md +69 -69
  11. package/osint-agent-package/skills/bmad-osint-investigate/osint/references/tools.md +281 -281
  12. package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/mcp-client.py +136 -136
  13. package/package.json +106 -91
  14. package/readme-international/README.de.md +625 -594
  15. package/readme-international/README.es.md +642 -611
  16. package/readme-international/README.fr.md +640 -609
  17. package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +2 -0
  18. package/src/bmad-plus/agents/agent-shadow/SKILL.md +18 -0
  19. package/src/bmad-plus/data/role-triggers.yaml +52 -0
  20. package/src/bmad-plus/module.yaml +333 -283
  21. package/src/bmad-plus/packs/pack-animated/animated-website-agent.md +325 -325
  22. package/src/bmad-plus/packs/pack-animated/templates/animated-website-workflow.md +55 -55
  23. package/src/bmad-plus/packs/pack-backup/backup-agent.md +71 -71
  24. package/src/bmad-plus/packs/pack-backup/templates/backup-workflow.md +51 -51
  25. package/src/bmad-plus/packs/pack-dev-studio/README.md +162 -162
  26. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +73 -73
  27. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +61 -61
  28. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +95 -95
  29. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +95 -95
  30. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +134 -134
  31. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +80 -80
  32. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +73 -73
  33. package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +95 -95
  34. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +73 -73
  35. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +73 -73
  36. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +92 -92
  37. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +80 -80
  38. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +90 -90
  39. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +153 -153
  40. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +173 -173
  41. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +224 -224
  42. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +329 -329
  43. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +318 -318
  44. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +359 -359
  45. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +379 -379
  46. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +361 -361
  47. package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +81 -81
  48. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +67 -67
  49. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +85 -85
  50. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +35 -35
  51. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +49 -49
  52. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +131 -131
  53. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +89 -89
  54. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +300 -300
  55. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +428 -428
  56. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +73 -73
  57. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +80 -80
  58. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +484 -484
  59. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +193 -193
  60. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +175 -175
  61. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +110 -110
  62. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +1511 -1511
  63. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +298 -298
  64. package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +296 -296
  65. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +29 -29
  66. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +74 -74
  67. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +29 -29
  68. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +73 -73
  69. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +89 -89
  70. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +73 -73
  71. package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +29 -29
  72. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +141 -141
  73. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +37 -37
  74. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +75 -75
  75. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +6 -6
  76. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +110 -110
  77. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +176 -176
  78. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +67 -67
  79. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +86 -86
  80. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +179 -179
  81. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +66 -66
  82. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +127 -127
  83. package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +105 -105
  84. package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +120 -120
  85. package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +12 -12
  86. package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +328 -328
  87. package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +32 -32
  88. package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +81 -81
  89. package/src/bmad-plus/packs/pack-seo/SKILL.md +171 -171
  90. package/src/bmad-plus/packs/pack-seo/checklist.md +140 -140
  91. package/src/bmad-plus/packs/pack-seo/pagespeed-playbook.md +320 -320
  92. package/src/bmad-plus/packs/pack-seo/ref/audit-schema.json +187 -187
  93. package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +87 -87
  94. package/src/bmad-plus/packs/pack-seo/ref/eeat-criteria.md +123 -123
  95. package/src/bmad-plus/packs/pack-seo/ref/geo-signals.md +167 -167
  96. package/src/bmad-plus/packs/pack-seo/ref/hreflang-rules.md +153 -153
  97. package/src/bmad-plus/packs/pack-seo/ref/quality-gates.md +133 -133
  98. package/src/bmad-plus/packs/pack-seo/ref/schema-catalog.md +91 -91
  99. package/src/bmad-plus/packs/pack-seo/ref/schema-templates.json +356 -356
  100. package/src/bmad-plus/packs/pack-seo/requirements.txt +17 -0
  101. package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +456 -0
  102. package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +359 -0
  103. package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +303 -0
  104. package/src/bmad-plus/packs/pack-seo/scripts/seo_parse.py +255 -0
  105. package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +409 -0
  106. package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +202 -0
  107. package/src/bmad-plus/packs/pack-seo/seo-chief.md +294 -294
  108. package/src/bmad-plus/packs/pack-seo/seo-judge.md +241 -241
  109. package/src/bmad-plus/packs/pack-seo/seo-scout.md +171 -171
  110. package/src/bmad-plus/packs/pack-seo/templates/seo-audit-workflow.md +241 -241
  111. package/src/bmad-plus/packs/pack-shield/README.md +6 -6
  112. package/src/bmad-plus/packs/pack-shield/SKILL.md +2 -2
  113. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/csrd-agent.md +11 -11
  114. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/section508-agent.md +11 -11
  115. package/src/bmad-plus/packs/pack-shield/categories/accessibility-esg/wcag-agent.md +11 -11
  116. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/eu-ai-act-agent.md +11 -11
  117. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/iso42001-agent.md +11 -11
  118. package/src/bmad-plus/packs/pack-shield/categories/ai-governance/nist-ai-rmf-agent.md +11 -11
  119. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/cis-controls-agent.md +11 -11
  120. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/ism-agent.md +11 -11
  121. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/iso27001-agent.md +11 -11
  122. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nis2-agent.md +11 -11
  123. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-800-53-agent.md +11 -11
  124. package/src/bmad-plus/packs/pack-shield/categories/cybersecurity/nist-csf-agent.md +11 -11
  125. package/src/bmad-plus/packs/pack-shield/categories/defense-export/cmmc-agent.md +11 -11
  126. package/src/bmad-plus/packs/pack-shield/categories/defense-export/ear-agent.md +11 -11
  127. package/src/bmad-plus/packs/pack-shield/categories/defense-export/itar-agent.md +11 -11
  128. package/src/bmad-plus/packs/pack-shield/categories/defense-export/tsa-agent.md +11 -11
  129. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/dora-agent.md +11 -11
  130. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/fedramp-agent.md +11 -11
  131. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/hipaa-agent.md +11 -11
  132. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/pci-dss-agent.md +11 -11
  133. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/soc2-agent.md +11 -11
  134. package/src/bmad-plus/packs/pack-shield/categories/industry-compliance/swift-csp-agent.md +11 -11
  135. package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +1 -1
  136. package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +1 -1
  137. package/tools/bmad-plus-npx.js +4 -2
  138. package/tools/build/adapters.config.js +60 -51
  139. package/tools/build/check-counts.js +626 -0
  140. package/tools/build/check-install-contract.js +298 -0
  141. package/tools/build/generate-adapters.js +252 -56
  142. package/tools/build/generate.js +187 -10
  143. package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
  144. package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
  145. package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
  146. package/tools/build/generated-adapters/AGENTS.md +20 -7
  147. package/tools/build/generated-adapters/CLAUDE.md +20 -7
  148. package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
  149. package/tools/build/generated-adapters/GEMINI.md +20 -7
  150. package/tools/build/module.template.yaml +82 -0
  151. package/tools/cli/bmad-plus-cli.js +16 -1
  152. package/tools/cli/commands/autoconfig.js +470 -470
  153. package/tools/cli/commands/doctor.js +205 -233
  154. package/tools/cli/commands/install.js +543 -501
  155. package/tools/cli/commands/memory-journal-cmd.js +311 -311
  156. package/tools/cli/commands/memory.js +195 -195
  157. package/tools/cli/commands/scan.js +348 -348
  158. package/tools/cli/commands/uninstall.js +209 -101
  159. package/tools/cli/commands/update-check.js +31 -0
  160. package/tools/cli/commands/update-policy.js +39 -0
  161. package/tools/cli/commands/update.js +123 -134
  162. package/tools/cli/i18n.js +905 -845
  163. package/tools/cli/lib/README-memory-journal.md +125 -125
  164. package/tools/cli/lib/ide-config.js +10 -259
  165. package/tools/cli/lib/install-manifest.js +17 -0
  166. package/tools/cli/lib/installed-adapters.js +89 -0
  167. package/tools/cli/lib/npm-runner.js +177 -0
  168. package/tools/cli/lib/pack-copy.js +62 -66
  169. package/tools/cli/lib/packs.js +437 -3
  170. package/tools/cli/lib/python-provision.js +508 -508
  171. package/tools/cli/lib/stack-detect.js +102 -102
  172. package/tools/cli/lib/update-check.js +153 -0
  173. package/tools/cli/lib/update-dispatch.js +182 -0
  174. package/tools/cli/lib/update-policy.js +90 -0
  175. package/tools/cli/lib/update-transaction.js +334 -0
  176. package/tools/cli/lib/validate.js +50 -50
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SEO Parse — HTML parser for SEO element extraction.
4
+
5
+ Extracts: title, meta tags, canonicals, headings, images, links (internal/external),
6
+ schema (JSON-LD), Open Graph, Twitter Cards, hreflang, word count, text/code ratio.
7
+
8
+ Author: Laurent Rochetta
9
+ License: MIT
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import os
15
+ import re
16
+ import sys
17
+ from typing import Optional
18
+ from urllib.parse import urljoin, urlparse
19
+
20
+ try:
21
+ from bs4 import BeautifulSoup
22
+ except ImportError:
23
+ print("Error: beautifulsoup4 required. Install: pip install beautifulsoup4", file=sys.stderr)
24
+ sys.exit(1)
25
+
26
+ # Use lxml if available for speed, fallback to html.parser
27
+ try:
28
+ import lxml # noqa: F401
29
+ HTML_PARSER = "lxml"
30
+ except ImportError:
31
+ HTML_PARSER = "html.parser"
32
+
33
+
34
+ def parse_html(html: str, base_url: Optional[str] = None) -> dict:
35
+ """
36
+ Parse HTML and extract all SEO-relevant elements.
37
+
38
+ Args:
39
+ html: Raw HTML content
40
+ base_url: Base URL for resolving relative links
41
+
42
+ Returns:
43
+ Comprehensive dictionary of SEO data
44
+ """
45
+ soup = BeautifulSoup(html, HTML_PARSER)
46
+
47
+ result = {
48
+ "title": None,
49
+ "title_length": 0,
50
+ "meta_description": None,
51
+ "meta_description_length": 0,
52
+ "meta_robots": None,
53
+ "meta_viewport": None,
54
+ "canonical": None,
55
+ "headings": {"h1": [], "h2": [], "h3": [], "h4": []},
56
+ "images": [],
57
+ "links": {"internal": [], "external": [], "broken_candidates": []},
58
+ "schema_blocks": [],
59
+ "open_graph": {},
60
+ "twitter_card": {},
61
+ "hreflang": [],
62
+ "word_count": 0,
63
+ "html_size_bytes": len(html.encode("utf-8")),
64
+ "text_ratio": 0.0,
65
+ "has_lang_attr": False,
66
+ "lang": None,
67
+ "scripts_count": 0,
68
+ "stylesheets_count": 0,
69
+ "dom_depth_estimate": 0,
70
+ "security_headers_hints": {},
71
+ }
72
+
73
+ # ── Title ──
74
+ title_tag = soup.find("title")
75
+ if title_tag:
76
+ result["title"] = title_tag.get_text(strip=True)
77
+ result["title_length"] = len(result["title"])
78
+
79
+ # ── Meta Tags ──
80
+ for meta in soup.find_all("meta"):
81
+ name = (meta.get("name") or "").lower()
82
+ property_attr = (meta.get("property") or "").lower()
83
+ content = meta.get("content", "")
84
+
85
+ if name == "description":
86
+ result["meta_description"] = content
87
+ result["meta_description_length"] = len(content)
88
+ elif name == "robots":
89
+ result["meta_robots"] = content
90
+ elif name == "viewport":
91
+ result["meta_viewport"] = content
92
+
93
+ # Open Graph
94
+ if property_attr.startswith("og:"):
95
+ result["open_graph"][property_attr] = content
96
+
97
+ # Twitter Card
98
+ if name.startswith("twitter:"):
99
+ result["twitter_card"][name] = content
100
+
101
+ # ── Language ──
102
+ html_tag = soup.find("html")
103
+ if html_tag and html_tag.get("lang"):
104
+ result["has_lang_attr"] = True
105
+ result["lang"] = html_tag.get("lang")
106
+
107
+ # ── Canonical ──
108
+ canonical = soup.find("link", rel="canonical")
109
+ if canonical:
110
+ result["canonical"] = canonical.get("href")
111
+
112
+ # ── Hreflang ──
113
+ for link in soup.find_all("link", rel="alternate"):
114
+ hreflang = link.get("hreflang")
115
+ if hreflang:
116
+ result["hreflang"].append({
117
+ "lang": hreflang,
118
+ "href": link.get("href"),
119
+ })
120
+
121
+ # ── Headings ──
122
+ for level in ["h1", "h2", "h3", "h4"]:
123
+ for tag in soup.find_all(level):
124
+ text = tag.get_text(strip=True)
125
+ if text:
126
+ result["headings"][level].append(text)
127
+
128
+ # ── Images ──
129
+ for img in soup.find_all("img"):
130
+ src = img.get("src", "")
131
+ if base_url and src:
132
+ src = urljoin(base_url, src)
133
+
134
+ has_alt = img.get("alt") is not None
135
+ alt_text = img.get("alt", "")
136
+ has_dimensions = bool(img.get("width") and img.get("height"))
137
+
138
+ result["images"].append({
139
+ "src": src,
140
+ "alt": alt_text,
141
+ "has_alt": has_alt,
142
+ "alt_empty": has_alt and alt_text.strip() == "",
143
+ "width": img.get("width"),
144
+ "height": img.get("height"),
145
+ "has_dimensions": has_dimensions,
146
+ "loading": img.get("loading"),
147
+ "srcset": img.get("srcset") is not None,
148
+ })
149
+
150
+ # ── Links ──
151
+ if base_url:
152
+ base_domain = urlparse(base_url).netloc
153
+
154
+ for a in soup.find_all("a", href=True):
155
+ href = a.get("href", "")
156
+ if not href or href.startswith("#") or href.startswith("javascript:"):
157
+ continue
158
+
159
+ full_url = urljoin(base_url, href)
160
+ parsed = urlparse(full_url)
161
+
162
+ link_data = {
163
+ "href": full_url,
164
+ "text": a.get_text(strip=True)[:100],
165
+ "rel": a.get("rel", []),
166
+ "is_nofollow": "nofollow" in (a.get("rel") or []),
167
+ "target": a.get("target"),
168
+ }
169
+
170
+ if parsed.netloc == base_domain:
171
+ result["links"]["internal"].append(link_data)
172
+ else:
173
+ result["links"]["external"].append(link_data)
174
+
175
+ # ── Schema (JSON-LD) ──
176
+ for script in soup.find_all("script", type="application/ld+json"):
177
+ try:
178
+ schema_data = json.loads(script.string)
179
+ if isinstance(schema_data, dict):
180
+ result["schema_blocks"].append({
181
+ "type": schema_data.get("@type", "unknown"),
182
+ "data": schema_data,
183
+ })
184
+ elif isinstance(schema_data, list):
185
+ for item in schema_data:
186
+ if isinstance(item, dict):
187
+ result["schema_blocks"].append({
188
+ "type": item.get("@type", "unknown"),
189
+ "data": item,
190
+ })
191
+ except (json.JSONDecodeError, TypeError):
192
+ result["schema_blocks"].append({"type": "PARSE_ERROR", "data": None})
193
+
194
+ # ── Resource Counts ──
195
+ result["scripts_count"] = len(soup.find_all("script"))
196
+ result["stylesheets_count"] = len(soup.find_all("link", rel="stylesheet"))
197
+
198
+ # ── Word Count & Text Ratio ──
199
+ text_soup = BeautifulSoup(html, HTML_PARSER)
200
+ for element in text_soup(["script", "style", "nav", "footer", "header", "noscript"]):
201
+ element.decompose()
202
+
203
+ visible_text = text_soup.get_text(separator=" ", strip=True)
204
+ words = re.findall(r"\b\w+\b", visible_text)
205
+ result["word_count"] = len(words)
206
+
207
+ text_bytes = len(visible_text.encode("utf-8"))
208
+ if result["html_size_bytes"] > 0:
209
+ result["text_ratio"] = round(text_bytes / result["html_size_bytes"], 3)
210
+
211
+ return result
212
+
213
+
214
+ # ── CLI ────────────────────────────────────────────────────────────
215
+
216
+ def main():
217
+ parser = argparse.ArgumentParser(
218
+ description="SEO Parse — HTML parser for SEO analysis (BMAD+ SEO Engine)"
219
+ )
220
+ parser.add_argument("file", nargs="?", help="HTML file to parse")
221
+ parser.add_argument("--url", "-u", help="Base URL for resolving relative links")
222
+ parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
223
+
224
+ args = parser.parse_args()
225
+
226
+ if args.file:
227
+ real_path = os.path.realpath(args.file)
228
+ if not os.path.isfile(real_path):
229
+ print(f"Error: File not found: {args.file}", file=sys.stderr)
230
+ sys.exit(1)
231
+ with open(real_path, "r", encoding="utf-8") as f:
232
+ html = f.read()
233
+ else:
234
+ html = sys.stdin.read()
235
+
236
+ result = parse_html(html, args.url)
237
+
238
+ if args.json:
239
+ print(json.dumps(result, indent=2, ensure_ascii=False))
240
+ else:
241
+ print(f"Title: {result['title']} ({result['title_length']} chars)")
242
+ print(f"Meta Description: {result['meta_description'][:80] + '...' if result['meta_description'] and len(result['meta_description']) > 80 else result['meta_description']}")
243
+ print(f"Canonical: {result['canonical']}")
244
+ print(f"Language: {result['lang']}")
245
+ print(f"H1: {len(result['headings']['h1'])} | H2: {len(result['headings']['h2'])} | H3: {len(result['headings']['h3'])}")
246
+ print(f"Images: {len(result['images'])} (missing alt: {sum(1 for i in result['images'] if not i['has_alt'])})")
247
+ print(f"Internal Links: {len(result['links']['internal'])} | External: {len(result['links']['external'])}")
248
+ print(f"Schema Blocks: {len(result['schema_blocks'])} ({', '.join(s['type'] for s in result['schema_blocks'])})")
249
+ print(f"Word Count: {result['word_count']:,}")
250
+ print(f"Text/HTML Ratio: {result['text_ratio']:.1%}")
251
+ print(f"Scripts: {result['scripts_count']} | Stylesheets: {result['stylesheets_count']}")
252
+
253
+
254
+ if __name__ == "__main__":
255
+ main()
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SEO Report — Professional HTML audit report generator.
4
+
5
+ Features:
6
+ - Single-file HTML with inline CSS (no external deps)
7
+ - SVG radar chart for score visualization
8
+ - Color-coded issue cards (Critical/High/Medium/Low)
9
+ - Quick Wins section
10
+ - Print-friendly (@media print)
11
+ - Responsive (mobile-readable)
12
+
13
+ Author: Laurent Rochetta
14
+ License: MIT
15
+ """
16
+
17
+ import argparse
18
+ import html
19
+ import json
20
+ import math
21
+ import os
22
+ import sys
23
+ from datetime import datetime
24
+
25
+
26
+ def generate_radar_svg(scores: dict, size: int = 300) -> str:
27
+ """Generate an SVG radar chart for the 7 score categories."""
28
+ categories = list(scores.keys())
29
+ values = list(scores.values())
30
+ n = len(categories)
31
+
32
+ if n == 0:
33
+ return ""
34
+
35
+ cx, cy = size // 2, size // 2
36
+ radius = size // 2 - 40
37
+
38
+ # Short labels for display
39
+ short_labels = {
40
+ "technical": "Tech",
41
+ "content_eeat": "E-E-A-T",
42
+ "on_page": "On-Page",
43
+ "schema": "Schema",
44
+ "performance": "Perf",
45
+ "ai_readiness": "AI/GEO",
46
+ "images": "Images",
47
+ }
48
+
49
+ def point(angle_deg, r):
50
+ angle_rad = math.radians(angle_deg - 90)
51
+ x = cx + r * math.cos(angle_rad)
52
+ y = cy + r * math.sin(angle_rad)
53
+ return x, y
54
+
55
+ svg_parts = [f'<svg viewBox="0 0 {size} {size}" xmlns="http://www.w3.org/2000/svg" style="max-width:{size}px;margin:auto;display:block;">']
56
+
57
+ # Background circles
58
+ for pct in [25, 50, 75, 100]:
59
+ r = radius * pct / 100
60
+ svg_parts.append(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="#e2e8f0" stroke-width="1" opacity="0.5"/>')
61
+
62
+ # Axis lines + labels
63
+ for i in range(n):
64
+ angle = (360 / n) * i
65
+ x1, y1 = point(angle, 0)
66
+ x2, y2 = point(angle, radius)
67
+ svg_parts.append(f'<line x1="{cx}" y1="{cy}" x2="{x2}" y2="{y2}" stroke="#e2e8f0" stroke-width="1"/>')
68
+
69
+ lx, ly = point(angle, radius + 20)
70
+ label = short_labels.get(categories[i], categories[i][:6])
71
+ svg_parts.append(f'<text x="{lx}" y="{ly}" text-anchor="middle" font-size="11" fill="#64748b" font-family="Inter,sans-serif">{label}</text>')
72
+
73
+ # Data polygon
74
+ data_points = []
75
+ for i in range(n):
76
+ angle = (360 / n) * i
77
+ r = radius * min(values[i], 100) / 100
78
+ x, y = point(angle, r)
79
+ data_points.append(f"{x},{y}")
80
+
81
+ poly = " ".join(data_points)
82
+ svg_parts.append(f'<polygon points="{poly}" fill="rgba(59,130,246,0.2)" stroke="#3b82f6" stroke-width="2"/>')
83
+
84
+ # Data points
85
+ for i in range(n):
86
+ angle = (360 / n) * i
87
+ r = radius * min(values[i], 100) / 100
88
+ x, y = point(angle, r)
89
+ color = "#22c55e" if values[i] >= 80 else "#f59e0b" if values[i] >= 50 else "#ef4444"
90
+ svg_parts.append(f'<circle cx="{x}" cy="{y}" r="4" fill="{color}" stroke="white" stroke-width="2"/>')
91
+
92
+ svg_parts.append('</svg>')
93
+ return "\n".join(svg_parts)
94
+
95
+
96
+ def severity_color(severity: str) -> str:
97
+ """Get color for severity level."""
98
+ return {
99
+ "critical": "#ef4444",
100
+ "high": "#f97316",
101
+ "medium": "#f59e0b",
102
+ "low": "#22c55e",
103
+ }.get(severity, "#64748b")
104
+
105
+
106
+ def severity_icon(severity: str) -> str:
107
+ return {
108
+ "critical": "🔴",
109
+ "high": "🟠",
110
+ "medium": "🟡",
111
+ "low": "🟢",
112
+ }.get(severity, "⚪")
113
+
114
+
115
+ def score_color(score: int) -> str:
116
+ if score >= 90:
117
+ return "#22c55e"
118
+ elif score >= 70:
119
+ return "#84cc16"
120
+ elif score >= 50:
121
+ return "#f59e0b"
122
+ else:
123
+ return "#ef4444"
124
+
125
+
126
+ def generate_html_report(audit_data: dict) -> str:
127
+ """Generate a complete HTML report from audit JSON data."""
128
+
129
+ domain = audit_data.get("domain", "Unknown")
130
+ timestamp = audit_data.get("timestamp", datetime.now().isoformat())
131
+ total_score = audit_data.get("score", {}).get("total", 0)
132
+
133
+ # Escaped copies of externally-sourced content for safe HTML interpolation
134
+ domain_esc = html.escape(str(domain))
135
+ timestamp_esc = html.escape(str(timestamp)[:10])
136
+ total_score_esc = html.escape(str(total_score))
137
+ categories = audit_data.get("score", {}).get("categories", {})
138
+ issues = audit_data.get("issues", [])
139
+
140
+ # Generate radar chart
141
+ radar_svg = generate_radar_svg(categories) if categories else ""
142
+
143
+ # Sort issues by severity
144
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
145
+ sorted_issues = sorted(issues, key=lambda x: severity_order.get(x.get("severity", "low"), 4))
146
+
147
+ # Count by severity
148
+ counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
149
+ for issue in issues:
150
+ sev = issue.get("severity", "low")
151
+ counts[sev] = counts.get(sev, 0) + 1
152
+
153
+ # Quick wins
154
+ quick_wins = [i for i in issues if i.get("quick_win", False)][:5]
155
+
156
+ # Build issue cards HTML
157
+ issue_cards = ""
158
+ for issue in sorted_issues:
159
+ sev = issue.get("severity", "low")
160
+ fix_html = ""
161
+ if issue.get("fix"):
162
+ fix_html = f'<div class="fix-block"><strong>Fix:</strong><pre><code>{html.escape(str(issue["fix"]))}</code></pre></div>'
163
+
164
+ issue_cards += f'''
165
+ <div class="issue-card" style="border-left: 4px solid {severity_color(sev)}">
166
+ <div class="issue-header">
167
+ <span class="severity-badge" style="background:{severity_color(sev)}">{html.escape(str(sev)).upper()}</span>
168
+ <span class="issue-category">{html.escape(str(issue.get("category", "")))}</span>
169
+ </div>
170
+ <h4>{html.escape(str(issue.get("title", "")))}</h4>
171
+ <p>{html.escape(str(issue.get("description", "")))}</p>
172
+ {fix_html}
173
+ </div>'''
174
+
175
+ # Quick wins HTML
176
+ qw_html = ""
177
+ if quick_wins:
178
+ qw_items = ""
179
+ for qw in quick_wins:
180
+ qw_items += f'<li>{severity_icon(qw.get("severity", ""))} {html.escape(str(qw.get("title", "")))}</li>'
181
+ qw_html = f'<div class="quick-wins"><h3>⚡ Quick Wins</h3><ul>{qw_items}</ul></div>'
182
+
183
+ # Category scores table
184
+ cat_rows = ""
185
+ for cat, score in categories.items():
186
+ cat_name = html.escape(str(cat).replace("_", " ").title())
187
+ score_esc = html.escape(str(score))
188
+ cat_rows += f'''
189
+ <tr>
190
+ <td>{cat_name}</td>
191
+ <td>
192
+ <div class="score-bar-bg">
193
+ <div class="score-bar" style="width:{score_esc}%;background:{score_color(score)}"></div>
194
+ </div>
195
+ </td>
196
+ <td style="color:{score_color(score)};font-weight:700">{score_esc}</td>
197
+ </tr>'''
198
+
199
+ html_out = f'''<!DOCTYPE html>
200
+ <html lang="en">
201
+ <head>
202
+ <meta charset="UTF-8">
203
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
204
+ <title>SEO Audit Report — {domain_esc}</title>
205
+ <style>
206
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
207
+
208
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
209
+ body {{
210
+ font-family: 'Inter', -apple-system, sans-serif;
211
+ background: #f8fafc;
212
+ color: #1e293b;
213
+ line-height: 1.6;
214
+ }}
215
+ .container {{ max-width: 900px; margin: 0 auto; padding: 2rem; }}
216
+
217
+ /* Header */
218
+ .header {{
219
+ background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%);
220
+ color: white;
221
+ padding: 3rem 2rem;
222
+ border-radius: 16px;
223
+ margin-bottom: 2rem;
224
+ text-align: center;
225
+ }}
226
+ .header h1 {{ font-size: 2rem; margin-bottom: 0.5rem; }}
227
+ .header .domain {{ font-size: 1.2rem; opacity: 0.8; }}
228
+ .header .date {{ font-size: 0.85rem; opacity: 0.6; margin-top: 0.5rem; }}
229
+
230
+ /* Score circle */
231
+ .score-hero {{
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ gap: 3rem;
236
+ margin: 2rem 0;
237
+ flex-wrap: wrap;
238
+ }}
239
+ .score-circle {{
240
+ width: 150px;
241
+ height: 150px;
242
+ border-radius: 50%;
243
+ display: flex;
244
+ flex-direction: column;
245
+ align-items: center;
246
+ justify-content: center;
247
+ border: 6px solid {score_color(total_score)};
248
+ background: white;
249
+ box-shadow: 0 4px 24px rgba(0,0,0,0.08);
250
+ }}
251
+ .score-number {{ font-size: 3rem; font-weight: 700; color: {score_color(total_score)}; }}
252
+ .score-label {{ font-size: 0.75rem; text-transform: uppercase; color: #64748b; letter-spacing: 1px; }}
253
+
254
+ /* Summary cards */
255
+ .summary-grid {{
256
+ display: grid;
257
+ grid-template-columns: repeat(4, 1fr);
258
+ gap: 1rem;
259
+ margin-bottom: 2rem;
260
+ }}
261
+ .summary-card {{
262
+ background: white;
263
+ border-radius: 12px;
264
+ padding: 1.2rem;
265
+ text-align: center;
266
+ box-shadow: 0 2px 8px rgba(0,0,0,0.04);
267
+ }}
268
+ .summary-card .count {{ font-size: 2rem; font-weight: 700; }}
269
+ .summary-card .label {{ font-size: 0.8rem; color: #64748b; }}
270
+
271
+ /* Sections */
272
+ .section {{ background: white; border-radius: 12px; padding: 2rem; margin-bottom: 1.5rem; box-shadow: 0 2px 8px rgba(0,0,0,0.04); }}
273
+ .section h2 {{ margin-bottom: 1rem; font-size: 1.3rem; }}
274
+ .section h3 {{ margin-bottom: 0.8rem; font-size: 1.1rem; }}
275
+
276
+ /* Score bars */
277
+ table {{ width: 100%; border-collapse: collapse; }}
278
+ td {{ padding: 0.6rem 0; }}
279
+ .score-bar-bg {{ width: 100%; height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; margin: 0 1rem; }}
280
+ .score-bar {{ height: 100%; border-radius: 4px; transition: width 0.5s ease; }}
281
+
282
+ /* Issue cards */
283
+ .issue-card {{
284
+ border: 1px solid #e2e8f0;
285
+ border-radius: 8px;
286
+ padding: 1rem;
287
+ margin-bottom: 0.8rem;
288
+ }}
289
+ .issue-header {{ display: flex; gap: 0.5rem; margin-bottom: 0.3rem; align-items: center; }}
290
+ .severity-badge {{ color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 600; }}
291
+ .issue-category {{ font-size: 0.8rem; color: #64748b; }}
292
+ .issue-card h4 {{ margin-bottom: 0.3rem; }}
293
+ .issue-card p {{ color: #475569; font-size: 0.9rem; }}
294
+ .fix-block {{ background: #f1f5f9; border-radius: 6px; padding: 0.8rem; margin-top: 0.5rem; }}
295
+ .fix-block pre {{ overflow-x: auto; font-size: 0.8rem; }}
296
+
297
+ /* Quick wins */
298
+ .quick-wins {{ background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }}
299
+ .quick-wins ul {{ list-style: none; padding: 0; }}
300
+ .quick-wins li {{ padding: 0.3rem 0; }}
301
+
302
+ /* Footer */
303
+ .footer {{ text-align: center; color: #94a3b8; font-size: 0.8rem; padding: 2rem 0; }}
304
+
305
+ /* Print */
306
+ @media print {{
307
+ body {{ background: white; }}
308
+ .container {{ max-width: 100%; padding: 0; }}
309
+ .header {{ break-after: avoid; }}
310
+ .section {{ break-inside: avoid; box-shadow: none; border: 1px solid #e2e8f0; }}
311
+ }}
312
+
313
+ /* Mobile */
314
+ @media (max-width: 640px) {{
315
+ .summary-grid {{ grid-template-columns: repeat(2, 1fr); }}
316
+ .score-hero {{ flex-direction: column; gap: 1.5rem; }}
317
+ }}
318
+ </style>
319
+ </head>
320
+ <body>
321
+ <div class="container">
322
+ <div class="header">
323
+ <h1>SEO Audit Report</h1>
324
+ <div class="domain">{domain_esc}</div>
325
+ <div class="date">{timestamp_esc}</div>
326
+ </div>
327
+
328
+ <div class="score-hero">
329
+ <div class="score-circle">
330
+ <div class="score-number">{total_score_esc}</div>
331
+ <div class="score-label">SEO Score</div>
332
+ </div>
333
+ <div>
334
+ {radar_svg}
335
+ </div>
336
+ </div>
337
+
338
+ <div class="summary-grid">
339
+ <div class="summary-card">
340
+ <div class="count" style="color:#ef4444">{counts["critical"]}</div>
341
+ <div class="label">Critical</div>
342
+ </div>
343
+ <div class="summary-card">
344
+ <div class="count" style="color:#f97316">{counts["high"]}</div>
345
+ <div class="label">High</div>
346
+ </div>
347
+ <div class="summary-card">
348
+ <div class="count" style="color:#f59e0b">{counts["medium"]}</div>
349
+ <div class="label">Medium</div>
350
+ </div>
351
+ <div class="summary-card">
352
+ <div class="count" style="color:#22c55e">{counts["low"]}</div>
353
+ <div class="label">Low</div>
354
+ </div>
355
+ </div>
356
+
357
+ {qw_html}
358
+
359
+ <div class="section">
360
+ <h2>📊 Category Scores</h2>
361
+ <table>{cat_rows}</table>
362
+ </div>
363
+
364
+ <div class="section">
365
+ <h2>🔍 Issues ({len(issues)})</h2>
366
+ {issue_cards}
367
+ </div>
368
+
369
+ <div class="footer">
370
+ Generated by BMAD+ SEO Engine v2.1 — By Laurent Rochetta
371
+ </div>
372
+ </div>
373
+ </body>
374
+ </html>'''
375
+
376
+ return html_out
377
+
378
+
379
+ # ── CLI ────────────────────────────────────────────────────────────
380
+
381
+ def main():
382
+ parser = argparse.ArgumentParser(
383
+ description="SEO Report — HTML audit report generator (BMAD+ SEO Engine)"
384
+ )
385
+ parser.add_argument("input", help="Audit JSON file")
386
+ parser.add_argument("--output", "-o", default="seo-report.html", help="Output HTML file")
387
+
388
+ args = parser.parse_args()
389
+
390
+ if not os.path.isfile(args.input):
391
+ print(f"Error: File not found: {args.input}", file=sys.stderr)
392
+ sys.exit(1)
393
+
394
+ with open(args.input, "r", encoding="utf-8") as f:
395
+ audit_data = json.load(f)
396
+
397
+ html = generate_html_report(audit_data)
398
+
399
+ with open(args.output, "w", encoding="utf-8") as f:
400
+ f.write(html)
401
+
402
+ print(f"✅ Report generated: {args.output}", file=sys.stderr)
403
+ print(f" Domain: {audit_data.get('domain', 'Unknown')}")
404
+ print(f" Score: {audit_data.get('score', {}).get('total', 0)}/100")
405
+ print(f" Issues: {len(audit_data.get('issues', []))}")
406
+
407
+
408
+ if __name__ == "__main__":
409
+ main()