raise-cli 2.2.1__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.
Files changed (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,134 @@
1
+ """Tier detection and capability registry.
2
+
3
+ Detects the active deployment tier (COMMUNITY/PRO/Enterprise) from the project
4
+ manifest and exposes capability checks for adapters and CLI commands.
5
+
6
+ Architecture: ADR-037 (TierContext)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from enum import StrEnum
13
+ from pathlib import Path
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ __all__ = ["Capability", "TierCapabilityError", "TierContext", "TierLevel"]
20
+
21
+
22
+ class Capability(StrEnum):
23
+ """Capabilities available across tiers (ADR-037)."""
24
+
25
+ SHARED_MEMORY = "shared_memory"
26
+ SEMANTIC_SEARCH = "semantic_search"
27
+ TEAM_AWARENESS = "team_awareness"
28
+ JIRA_INTEGRATION = "jira_integration"
29
+ DOCS_PUBLISH = "docs_publish"
30
+ ORG_GOVERNANCE = "org_governance"
31
+ AUDIT_LOGGING = "audit_logging"
32
+
33
+
34
+ class TierLevel(StrEnum):
35
+ """Deployment tier levels."""
36
+
37
+ COMMUNITY = "community"
38
+ PRO = "pro"
39
+ ENTERPRISE = "enterprise"
40
+
41
+
42
+ class TierCapabilityError(Exception):
43
+ """Raised when a required capability is not available in the current tier."""
44
+
45
+ def __init__(
46
+ self,
47
+ capability: Capability,
48
+ current_tier: TierLevel,
49
+ suggested_tier: TierLevel,
50
+ ) -> None:
51
+ self.capability = capability
52
+ self.current_tier = current_tier
53
+ self.suggested_tier = suggested_tier
54
+ super().__init__(
55
+ f"Capability '{capability}' requires {suggested_tier} tier "
56
+ f"(current: {current_tier}). "
57
+ f"Upgrade to {suggested_tier} to enable this feature."
58
+ )
59
+
60
+
61
+ # Minimum tier required for each capability.
62
+ _CAPABILITY_TIER: dict[Capability, TierLevel] = {
63
+ Capability.SHARED_MEMORY: TierLevel.PRO,
64
+ Capability.SEMANTIC_SEARCH: TierLevel.PRO,
65
+ Capability.TEAM_AWARENESS: TierLevel.PRO,
66
+ Capability.JIRA_INTEGRATION: TierLevel.PRO,
67
+ Capability.DOCS_PUBLISH: TierLevel.PRO,
68
+ Capability.ORG_GOVERNANCE: TierLevel.ENTERPRISE,
69
+ Capability.AUDIT_LOGGING: TierLevel.ENTERPRISE,
70
+ }
71
+
72
+
73
+ class TierContext(BaseModel):
74
+ """Tier detection and capability registry."""
75
+
76
+ tier: TierLevel = TierLevel.COMMUNITY
77
+ backend_url: str | None = None
78
+ capabilities: set[Capability] = Field(default_factory=lambda: set[Capability]())
79
+
80
+ def has(self, capability: Capability) -> bool:
81
+ """Check if a capability is available."""
82
+ return capability in self.capabilities
83
+
84
+ def require_or_suggest(self, capability: Capability) -> None:
85
+ """Raise TierCapabilityError if capability is missing."""
86
+ if not self.has(capability):
87
+ suggested = _CAPABILITY_TIER.get(capability, TierLevel.PRO)
88
+ raise TierCapabilityError(
89
+ capability=capability,
90
+ current_tier=self.tier,
91
+ suggested_tier=suggested,
92
+ )
93
+
94
+ @classmethod
95
+ def from_manifest(cls, project_root: Path) -> TierContext:
96
+ """Detect tier from .raise/manifest.yaml via load_manifest().
97
+
98
+ Falls back to COMMUNITY if no manifest or no tier section.
99
+ """
100
+ from raise_cli.onboarding.manifest import load_manifest
101
+
102
+ manifest = load_manifest(project_root)
103
+ if manifest is None or manifest.tier is None:
104
+ return cls.community()
105
+
106
+ tier_cfg = manifest.tier
107
+
108
+ # Parse tier level, fall back to COMMUNITY for unknown values.
109
+ try:
110
+ tier_level = TierLevel(tier_cfg.level)
111
+ except ValueError:
112
+ logger.warning(
113
+ "Unknown tier level '%s', defaulting to community", tier_cfg.level
114
+ )
115
+ tier_level = TierLevel.COMMUNITY
116
+
117
+ # Parse capabilities, skip unknown ones.
118
+ capabilities: set[Capability] = set()
119
+ for cap_str in tier_cfg.capabilities:
120
+ try:
121
+ capabilities.add(Capability(cap_str))
122
+ except ValueError:
123
+ logger.warning("Unknown capability '%s', skipping", cap_str)
124
+
125
+ return cls(
126
+ tier=tier_level,
127
+ backend_url=tier_cfg.backend_url,
128
+ capabilities=capabilities,
129
+ )
130
+
131
+ @classmethod
132
+ def community(cls) -> TierContext:
133
+ """Factory for default COMMUNITY tier."""
134
+ return cls()
@@ -0,0 +1,7 @@
1
+ """Memory graph visualization — interactive HTML viewer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from raise_cli.viz.generator import generate_viz_html
6
+
7
+ __all__ = ["generate_viz_html"]
@@ -0,0 +1,406 @@
1
+ """Generate self-contained interactive HTML visualization of the memory graph.
2
+
3
+ Reads the NetworkX-compatible index.json and produces a single HTML file
4
+ with an embedded D3.js force-directed graph. No external dependencies at runtime.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+
12
+ # D3 v7 minified is ~270KB — we load from CDN for now to keep the file small.
13
+ # The HTML is still self-contained in that it has no server dependency.
14
+
15
+ _HTML_TEMPLATE = r"""<!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="utf-8">
19
+ <title>RaiSE Memory Graph</title>
20
+ <style>
21
+ :root { --s: min(1vw, 1vh); }
22
+ * { margin: 0; padding: 0; box-sizing: border-box; }
23
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0d1117; color: #c9d1d9; overflow: hidden; }
24
+ #controls { position: fixed; top: 1.5vh; left: 1.5vw; z-index: 10; display: flex; flex-wrap: wrap; gap: calc(var(--s) * 0.8); max-width: 80vw; }
25
+ .chip { padding: 0.6vh 1.2vw; border-radius: 1.2vh; font-size: clamp(14px, 1.4vw, 28px); cursor: pointer; border: 1px solid #30363d; background: #161b22; transition: all 0.15s; user-select: none; }
26
+ .chip:hover { border-color: #58a6ff; }
27
+ .chip.active { border-color: #58a6ff; background: #1f2937; color: #58a6ff; }
28
+ .chip .count { opacity: 0.5; margin-left: 0.4vw; }
29
+ #info { position: fixed; bottom: 1.5vh; left: 1.5vw; z-index: 10; font-size: clamp(12px, 1.2vw, 24px); color: #484f58; }
30
+ #tooltip { position: fixed; pointer-events: none; z-index: 20; background: #1c2128; border: 1px solid #30363d; border-radius: 1vh; padding: 1.2vh 1.5vw; font-size: clamp(14px, 1.3vw, 26px); max-width: 35vw; display: none; box-shadow: 0 0.4vh 1.2vh rgba(0,0,0,0.4); }
31
+ #tooltip .tt-id { font-weight: 600; color: #58a6ff; margin-bottom: 0.5vh; }
32
+ #tooltip .tt-type { font-size: clamp(12px, 1.1vw, 22px); color: #8b949e; margin-bottom: 0.6vh; }
33
+ #tooltip .tt-tags { font-size: clamp(11px, 1vw, 20px); color: #7c3aed; margin-bottom: 0.6vh; }
34
+ #tooltip .tt-from { font-size: clamp(11px, 1vw, 20px); color: #d29922; margin-bottom: 0.6vh; }
35
+ #tooltip .tt-content { color: #c9d1d9; line-height: 1.4; max-height: 25vh; overflow: hidden; white-space: pre-wrap; }
36
+ #search { position: fixed; top: 1.5vh; right: 1.5vw; z-index: 10; padding: 0.6vh 1.2vw; border-radius: 0.8vh; border: 1px solid #30363d; background: #161b22; color: #c9d1d9; font-size: clamp(14px, 1.4vw, 28px); width: clamp(200px, 18vw, 400px); outline: none; }
37
+ #search:focus { border-color: #58a6ff; }
38
+ #search::placeholder { color: #484f58; }
39
+ svg { width: 100vw; height: 100vh; }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div id="controls"></div>
44
+ <input id="search" type="text" placeholder="Search nodes..." />
45
+ <div id="tooltip"><div class="tt-id"></div><div class="tt-type"></div><div class="tt-tags"></div><div class="tt-from"></div><div class="tt-content"></div></div>
46
+ <div id="info"></div>
47
+ <svg></svg>
48
+ <script src="https://d3js.org/d3.v7.min.js"></script>
49
+ <script>
50
+ // --- DATA (injected by generator) ---
51
+ const graphData = %%GRAPH_DATA%%;
52
+
53
+ // --- COLOR PALETTE by node type ---
54
+ const typeColors = {
55
+ component: '#3fb950', pattern: '#a371f7', session: '#d29922', calibration: '#f0883e',
56
+ story: '#58a6ff', epic: '#1f6feb', term: '#8b949e', principle: '#f778ba',
57
+ decision: '#db6d28', guardrail: '#f85149', skill: '#79c0ff', module: '#56d364',
58
+ bounded_context: '#7ee787', requirement: '#d2a8ff', layer: '#388bfd', architecture: '#3fb950',
59
+ outcome: '#a5d6ff', project: '#ffd33d'
60
+ };
61
+ const defaultColor = '#484f58';
62
+ function color(type) { return typeColors[type] || defaultColor; }
63
+
64
+ // --- DOMAIN CLUSTERS ---
65
+ // Map each node type to a high-level domain
66
+ const typeToDomain = {
67
+ principle: 'Governance', guardrail: 'Governance', requirement: 'Governance', outcome: 'Governance', term: 'Governance',
68
+ component: 'Architecture', module: 'Architecture', bounded_context: 'Architecture', layer: 'Architecture', architecture: 'Architecture',
69
+ pattern: 'Memory', calibration: 'Memory', session: 'Memory',
70
+ epic: 'Work', story: 'Work', decision: 'Work', project: 'Work',
71
+ skill: 'Skills'
72
+ };
73
+ const domainList = ['Governance', 'Architecture', 'Memory', 'Work', 'Skills'];
74
+ const domainColors = {
75
+ Governance: '#f778ba', Architecture: '#3fb950', Memory: '#a371f7', Work: '#58a6ff', Skills: '#79c0ff'
76
+ };
77
+ function getDomain(type) { return typeToDomain[type] || 'Other'; }
78
+
79
+ // --- PATTERN SUB-CATEGORIES ---
80
+ // Top pattern categories with distinct colors for visual separation
81
+ const patternCatColors = {
82
+ architecture: '#c9b1ff', process: '#d2a8ff', testing: '#b088f9',
83
+ design: '#a371f7', graph: '#8957e5', skills: '#6e40c9',
84
+ cli: '#9d86e9', discovery: '#bf8cff', ontology: '#7c3aed',
85
+ workflow: '#e0c3fc', governance: '#dbb7ff', validation: '#cab0f5',
86
+ research: '#a78bfa', memory: '#8b5cf6', general: '#7e6cb5'
87
+ };
88
+ function patternColor(category) { return patternCatColors[category] || '#a371f7'; }
89
+
90
+ // --- PREP DATA ---
91
+ const nodeMap = new Map(graphData.nodes.map(n => [n.id, n]));
92
+ const typeCounts = {};
93
+ graphData.nodes.forEach(n => { typeCounts[n.type] = (typeCounts[n.type] || 0) + 1; });
94
+
95
+ // For large graphs, sample edges to keep it interactive
96
+ let links = graphData.links || graphData.edges || [];
97
+ const MAX_EDGES = 3000;
98
+ if (links.length > MAX_EDGES) {
99
+ // Prioritize non-related_to edges, then sample related_to
100
+ const important = links.filter(l => l.type !== 'related_to');
101
+ const relatedTo = links.filter(l => l.type === 'related_to');
102
+ const remaining = MAX_EDGES - important.length;
103
+ const sampled = relatedTo.sort(() => Math.random() - 0.5).slice(0, Math.max(0, remaining));
104
+ links = [...important, ...sampled];
105
+ }
106
+
107
+ // Resolve links to node references
108
+ const nodes = graphData.nodes.map(n => ({...n}));
109
+ const nodeIdx = new Map(nodes.map((n, i) => [n.id, i]));
110
+ const resolvedLinks = [];
111
+ links.forEach(l => {
112
+ const s = l.source.id !== undefined ? l.source.id : l.source;
113
+ const t = l.target.id !== undefined ? l.target.id : l.target;
114
+ if (nodeIdx.has(s) && nodeIdx.has(t)) {
115
+ resolvedLinks.push({source: s, target: t, type: l.type || 'related_to'});
116
+ }
117
+ });
118
+
119
+ // --- FILTER STATE ---
120
+ let activeTypes = new Set(Object.keys(typeCounts));
121
+ let searchTerm = '';
122
+
123
+ // --- CONTROLS ---
124
+ const controls = d3.select('#controls');
125
+ const sortedTypes = Object.entries(typeCounts).sort((a, b) => b[1] - a[1]);
126
+ sortedTypes.forEach(([type, count]) => {
127
+ controls.append('span')
128
+ .attr('class', 'chip active')
129
+ .attr('data-type', type)
130
+ .html(`<span style="color:${color(type)}">\u25CF</span> ${type}<span class="count">${count}</span>`)
131
+ .on('click', function() {
132
+ const chip = d3.select(this);
133
+ const t = chip.attr('data-type');
134
+ if (activeTypes.has(t)) { activeTypes.delete(t); chip.classed('active', false); }
135
+ else { activeTypes.add(t); chip.classed('active', true); }
136
+ applyFilter();
137
+ });
138
+ });
139
+
140
+ // --- SEARCH ---
141
+ d3.select('#search').on('input', function() {
142
+ searchTerm = this.value.toLowerCase();
143
+ applyFilter();
144
+ });
145
+
146
+ // --- SVG SETUP ---
147
+ const svg = d3.select('svg');
148
+ const width = window.innerWidth;
149
+ const height = window.innerHeight;
150
+ const g = svg.append('g');
151
+
152
+ // Zoom
153
+ const zoom = d3.zoom().scaleExtent([0.1, 8]).on('zoom', e => g.attr('transform', e.transform));
154
+ svg.call(zoom);
155
+
156
+ // --- SCALE FACTOR (viewport-aware) ---
157
+ const vMin = Math.min(width, height);
158
+ const S = vMin / 100; // 1% of smallest viewport dimension
159
+
160
+ // --- NODE RADIUS ---
161
+ function nodeRadius(d) {
162
+ if (d.type === 'module' || d.type === 'bounded_context' || d.type === 'layer') return S * 1.8;
163
+ if (d.type === 'epic' || d.type === 'architecture') return S * 1.5;
164
+ if (d.type === 'story' || d.type === 'skill' || d.type === 'principle') return S * 1.2;
165
+ return S * 0.9;
166
+ }
167
+
168
+ // --- CLUSTER LAYOUT ---
169
+ // Position domains in a circle around center
170
+ const clusterRadius = Math.min(width, height) * 0.45;
171
+ const domainCenters = {};
172
+ domainList.forEach((d, i) => {
173
+ const angle = (i / domainList.length) * 2 * Math.PI - Math.PI / 2;
174
+ domainCenters[d] = { x: width / 2 + clusterRadius * Math.cos(angle), y: height / 2 + clusterRadius * Math.sin(angle) };
175
+ });
176
+ domainCenters['Other'] = { x: width / 2, y: height / 2 };
177
+
178
+ // --- PATTERN SUB-CLUSTER POSITIONS ---
179
+ // Collect unique pattern categories and arrange them in a mini-circle within Memory domain
180
+ const patternCategories = [...new Set(nodes.filter(n => n.type === 'pattern').map(n => n.category || 'general'))];
181
+ const memCenter = domainCenters['Memory'];
182
+ const subRadius = clusterRadius * 0.4;
183
+ const patternCenters = {};
184
+ patternCategories.forEach((cat, i) => {
185
+ const angle = (i / patternCategories.length) * 2 * Math.PI - Math.PI / 2;
186
+ patternCenters[cat] = { x: memCenter.x + subRadius * Math.cos(angle), y: memCenter.y + subRadius * Math.sin(angle) };
187
+ });
188
+
189
+ // Target position for each node — patterns get sub-cluster positions
190
+ function targetX(d) {
191
+ if (d.type === 'pattern') return patternCenters[d.category || 'general']?.x || memCenter.x;
192
+ return domainCenters[getDomain(d.type)]?.x || width / 2;
193
+ }
194
+ function targetY(d) {
195
+ if (d.type === 'pattern') return patternCenters[d.category || 'general']?.y || memCenter.y;
196
+ return domainCenters[getDomain(d.type)]?.y || height / 2;
197
+ }
198
+
199
+ // --- SIMULATION ---
200
+ const simulation = d3.forceSimulation(nodes)
201
+ .force('link', d3.forceLink(resolvedLinks).id(d => d.id).distance(S * 6).strength(0.05))
202
+ .force('charge', d3.forceManyBody().strength(-S * 4).distanceMax(S * 40))
203
+ .force('collision', d3.forceCollide().radius(d => nodeRadius(d) + S * 0.3))
204
+ .force('x', d3.forceX(d => targetX(d)).strength(0.25))
205
+ .force('y', d3.forceY(d => targetY(d)).strength(0.25))
206
+ .alphaDecay(0.02);
207
+
208
+ // --- DRAW ---
209
+ // Domain background labels
210
+ const domainLabels = g.append('g').attr('class', 'domain-labels');
211
+ domainList.forEach(d => {
212
+ const c = domainCenters[d];
213
+ domainLabels.append('text')
214
+ .attr('x', c.x).attr('y', c.y)
215
+ .attr('text-anchor', 'middle').attr('dominant-baseline', 'central')
216
+ .attr('font-size', (S * 4) + 'px').attr('font-weight', '800')
217
+ .attr('fill', domainColors[d] || '#484f58').attr('opacity', 0.15)
218
+ .text(d.toUpperCase());
219
+ });
220
+
221
+ // Pattern sub-cluster labels
222
+ patternCategories.forEach(cat => {
223
+ const c = patternCenters[cat];
224
+ if (c) {
225
+ domainLabels.append('text')
226
+ .attr('x', c.x).attr('y', c.y - subRadius * 0.25)
227
+ .attr('text-anchor', 'middle').attr('font-size', (S * 1.2) + 'px').attr('font-weight', '500')
228
+ .attr('fill', patternColor(cat)).attr('opacity', 0.35)
229
+ .text(cat);
230
+ }
231
+ });
232
+
233
+ // Edge lines
234
+ const link = g.append('g').attr('class', 'links')
235
+ .selectAll('line').data(resolvedLinks).enter().append('line')
236
+ .attr('stroke', '#21262d').attr('stroke-width', S * 0.06).attr('stroke-opacity', 0.35);
237
+
238
+ // Edge labels (shown on hover via CSS)
239
+ const linkLabel = g.append('g').attr('class', 'link-labels')
240
+ .selectAll('text').data(resolvedLinks).enter().append('text')
241
+ .text(d => d.type.replace(/_/g, ' '))
242
+ .attr('font-size', (S * 0.8) + 'px').attr('fill', '#484f58').attr('text-anchor', 'middle')
243
+ .attr('dy', -S * 0.3).style('pointer-events', 'none').attr('opacity', 0);
244
+
245
+ // Node groups (circle + label)
246
+ const nodeG = g.append('g').attr('class', 'nodes')
247
+ .selectAll('g').data(nodes).enter().append('g')
248
+ .call(drag(simulation));
249
+
250
+ nodeG.append('circle')
251
+ .attr('r', d => nodeRadius(d))
252
+ .attr('fill', d => d.type === 'pattern' ? patternColor(d.category || 'general') : color(d.type))
253
+ .attr('stroke', '#0d1117').attr('stroke-width', 1);
254
+
255
+ // Node labels
256
+ nodeG.append('text')
257
+ .text(d => d.id.length > 24 ? d.id.substring(0, 22) + '..' : d.id)
258
+ .attr('font-size', d => (nodeRadius(d) > S ? S * 1.1 : S * 0.9) + 'px')
259
+ .attr('fill', '#c9d1d9')
260
+ .attr('text-anchor', 'middle')
261
+ .attr('dy', d => nodeRadius(d) + S * 1.2)
262
+ .attr('font-weight', '500')
263
+ .style('pointer-events', 'none');
264
+
265
+ // Tooltip
266
+ const tooltip = d3.select('#tooltip');
267
+ nodeG.on('mouseover', (e, d) => {
268
+ tooltip.select('.tt-id').text(d.id);
269
+ if (d.type === 'pattern') {
270
+ tooltip.select('.tt-type').text('pattern \u2022 ' + (d.category || 'general'));
271
+ tooltip.select('.tt-tags').text(d.tags && d.tags.length ? 'Tags: ' + d.tags.join(', ') : '');
272
+ tooltip.select('.tt-from').text(d.learned_from ? 'Learned from: ' + d.learned_from : '');
273
+ tooltip.select('.tt-content').text(d.content || '');
274
+ } else {
275
+ tooltip.select('.tt-type').text(d.type + (d.source_file ? ' \u2022 ' + d.source_file : ''));
276
+ tooltip.select('.tt-tags').text('');
277
+ tooltip.select('.tt-from').text('');
278
+ tooltip.select('.tt-content').text((d.content || '').substring(0, 200));
279
+ }
280
+ tooltip.style('display', 'block');
281
+ // Show edge labels for connected edges
282
+ linkLabel.attr('opacity', l => {
283
+ const sId = l.source.id !== undefined ? l.source.id : l.source;
284
+ const tId = l.target.id !== undefined ? l.target.id : l.target;
285
+ return (sId === d.id || tId === d.id) ? 0.8 : 0;
286
+ });
287
+ link.attr('stroke', l => {
288
+ const sId = l.source.id !== undefined ? l.source.id : l.source;
289
+ const tId = l.target.id !== undefined ? l.target.id : l.target;
290
+ return (sId === d.id || tId === d.id) ? '#58a6ff' : '#21262d';
291
+ }).attr('stroke-width', l => {
292
+ const sId = l.source.id !== undefined ? l.source.id : l.source;
293
+ const tId = l.target.id !== undefined ? l.target.id : l.target;
294
+ return (sId === d.id || tId === d.id) ? S * 0.15 : S * 0.06;
295
+ });
296
+ }).on('mousemove', e => {
297
+ tooltip.style('left', (e.clientX + 14) + 'px').style('top', (e.clientY - 10) + 'px');
298
+ }).on('mouseout', () => {
299
+ tooltip.style('display', 'none');
300
+ linkLabel.attr('opacity', 0);
301
+ link.attr('stroke', '#21262d').attr('stroke-width', S * 0.06);
302
+ });
303
+
304
+ // Info
305
+ d3.select('#info').text(`${nodes.length} nodes \u2022 ${resolvedLinks.length} edges (${links.length < (graphData.links || graphData.edges || []).length ? 'sampled' : 'all'})`);
306
+
307
+ // Tick
308
+ simulation.on('tick', () => {
309
+ link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
310
+ .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
311
+ linkLabel.attr('x', d => (d.source.x + d.target.x) / 2).attr('y', d => (d.source.y + d.target.y) / 2);
312
+ nodeG.attr('transform', d => `translate(${d.x},${d.y})`);
313
+ });
314
+
315
+ // --- FILTER ---
316
+ function applyFilter() {
317
+ nodeG.attr('display', d => {
318
+ const typeMatch = activeTypes.has(d.type);
319
+ const searchMatch = !searchTerm || d.id.toLowerCase().includes(searchTerm) || (d.content || '').toLowerCase().includes(searchTerm);
320
+ return (typeMatch && searchMatch) ? null : 'none';
321
+ });
322
+ const visibleIds = new Set();
323
+ nodeG.each(function(d) { if (d3.select(this).attr('display') !== 'none') visibleIds.add(d.id); });
324
+ link.attr('display', d => {
325
+ const sId = d.source.id !== undefined ? d.source.id : d.source;
326
+ const tId = d.target.id !== undefined ? d.target.id : d.target;
327
+ return (visibleIds.has(sId) && visibleIds.has(tId)) ? null : 'none';
328
+ });
329
+ linkLabel.attr('display', d => {
330
+ const sId = d.source.id !== undefined ? d.source.id : d.source;
331
+ const tId = d.target.id !== undefined ? d.target.id : d.target;
332
+ return (visibleIds.has(sId) && visibleIds.has(tId)) ? null : 'none';
333
+ });
334
+ }
335
+
336
+ // --- DRAG ---
337
+ function drag(sim) {
338
+ return d3.drag()
339
+ .on('start', (e, d) => { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
340
+ .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
341
+ .on('end', (e, d) => { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; });
342
+ }
343
+ </script>
344
+ </body>
345
+ </html>"""
346
+
347
+
348
+ def generate_viz_html(
349
+ index_path: Path,
350
+ output_path: Path,
351
+ ) -> Path:
352
+ """Generate an interactive HTML visualization from the memory graph.
353
+
354
+ Args:
355
+ index_path: Path to the memory index.json file.
356
+ output_path: Path to write the HTML file.
357
+
358
+ Returns:
359
+ The output path written to.
360
+ """
361
+ graph_data = json.loads(index_path.read_text(encoding="utf-8"))
362
+
363
+ # Strip heavy content from nodes to keep the HTML small
364
+ # For patterns: keep full content + context tags for sub-clustering
365
+ # For others: keep first 200 chars
366
+ slim_nodes = []
367
+ for node in graph_data.get("nodes", []):
368
+ meta: dict[str, object] = node.get("metadata") or {}
369
+ is_pattern = node.get("type") == "pattern"
370
+ content_raw: str = node.get("content", "") or ""
371
+ slim_node: dict[str, str | list[str]] = {
372
+ "id": node["id"],
373
+ "type": node.get("type", "unknown"),
374
+ "source_file": node.get("source_file", ""),
375
+ "content": content_raw if is_pattern else content_raw[:200],
376
+ }
377
+ if is_pattern:
378
+ ctx: list[str] = meta.get("context") or [] # type: ignore[assignment]
379
+ slim_node["category"] = ctx[0] if ctx else "general"
380
+ slim_node["tags"] = ctx
381
+ slim_node["learned_from"] = str(meta.get("learned_from", ""))
382
+ slim_nodes.append(slim_node)
383
+
384
+ # Build slim edge list
385
+ links = graph_data.get("links", graph_data.get("edges", []))
386
+ slim_links = []
387
+ for link in links:
388
+ slim_links.append(
389
+ {
390
+ "source": link.get("source", ""),
391
+ "target": link.get("target", ""),
392
+ "type": link.get("type", link.get("relation", "related_to")),
393
+ }
394
+ )
395
+
396
+ slim_graph: dict[str, list[dict[str, str]]] = {
397
+ "nodes": slim_nodes,
398
+ "links": slim_links,
399
+ }
400
+ graph_json = json.dumps(slim_graph, separators=(",", ":"))
401
+
402
+ html = _HTML_TEMPLATE.replace("%%GRAPH_DATA%%", graph_json)
403
+
404
+ output_path.parent.mkdir(parents=True, exist_ok=True)
405
+ output_path.write_text(html, encoding="utf-8")
406
+ return output_path