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.
- raise_cli/__init__.py +38 -0
- raise_cli/__main__.py +30 -0
- raise_cli/adapters/__init__.py +91 -0
- raise_cli/adapters/declarative/__init__.py +26 -0
- raise_cli/adapters/declarative/adapter.py +267 -0
- raise_cli/adapters/declarative/discovery.py +94 -0
- raise_cli/adapters/declarative/expressions.py +150 -0
- raise_cli/adapters/declarative/reference/__init__.py +1 -0
- raise_cli/adapters/declarative/reference/github.yaml +143 -0
- raise_cli/adapters/declarative/schema.py +98 -0
- raise_cli/adapters/filesystem.py +299 -0
- raise_cli/adapters/mcp_bridge.py +10 -0
- raise_cli/adapters/mcp_confluence.py +246 -0
- raise_cli/adapters/mcp_jira.py +405 -0
- raise_cli/adapters/models.py +205 -0
- raise_cli/adapters/protocols.py +180 -0
- raise_cli/adapters/registry.py +90 -0
- raise_cli/adapters/sync.py +149 -0
- raise_cli/agents/__init__.py +14 -0
- raise_cli/agents/antigravity.yaml +8 -0
- raise_cli/agents/claude.yaml +8 -0
- raise_cli/agents/copilot.yaml +8 -0
- raise_cli/agents/copilot_plugin.py +124 -0
- raise_cli/agents/cursor.yaml +7 -0
- raise_cli/agents/roo.yaml +8 -0
- raise_cli/agents/windsurf.yaml +8 -0
- raise_cli/artifacts/__init__.py +30 -0
- raise_cli/artifacts/models.py +43 -0
- raise_cli/artifacts/reader.py +55 -0
- raise_cli/artifacts/renderer.py +104 -0
- raise_cli/artifacts/story_design.py +69 -0
- raise_cli/artifacts/writer.py +45 -0
- raise_cli/backlog/__init__.py +1 -0
- raise_cli/backlog/sync.py +115 -0
- raise_cli/cli/__init__.py +3 -0
- raise_cli/cli/commands/__init__.py +3 -0
- raise_cli/cli/commands/_resolve.py +153 -0
- raise_cli/cli/commands/adapters.py +362 -0
- raise_cli/cli/commands/artifact.py +137 -0
- raise_cli/cli/commands/backlog.py +333 -0
- raise_cli/cli/commands/base.py +31 -0
- raise_cli/cli/commands/discover.py +551 -0
- raise_cli/cli/commands/docs.py +130 -0
- raise_cli/cli/commands/doctor.py +177 -0
- raise_cli/cli/commands/gate.py +223 -0
- raise_cli/cli/commands/graph.py +1086 -0
- raise_cli/cli/commands/info.py +81 -0
- raise_cli/cli/commands/init.py +746 -0
- raise_cli/cli/commands/journal.py +167 -0
- raise_cli/cli/commands/mcp.py +524 -0
- raise_cli/cli/commands/memory.py +467 -0
- raise_cli/cli/commands/pattern.py +348 -0
- raise_cli/cli/commands/profile.py +59 -0
- raise_cli/cli/commands/publish.py +80 -0
- raise_cli/cli/commands/release.py +338 -0
- raise_cli/cli/commands/session.py +528 -0
- raise_cli/cli/commands/signal.py +410 -0
- raise_cli/cli/commands/skill.py +350 -0
- raise_cli/cli/commands/skill_set.py +145 -0
- raise_cli/cli/error_handler.py +158 -0
- raise_cli/cli/main.py +163 -0
- raise_cli/compat.py +66 -0
- raise_cli/config/__init__.py +41 -0
- raise_cli/config/agent_plugin.py +105 -0
- raise_cli/config/agent_registry.py +233 -0
- raise_cli/config/agents.py +120 -0
- raise_cli/config/ide.py +32 -0
- raise_cli/config/paths.py +379 -0
- raise_cli/config/settings.py +180 -0
- raise_cli/context/__init__.py +42 -0
- raise_cli/context/analyzers/__init__.py +16 -0
- raise_cli/context/analyzers/models.py +36 -0
- raise_cli/context/analyzers/protocol.py +43 -0
- raise_cli/context/analyzers/python.py +292 -0
- raise_cli/context/builder.py +1569 -0
- raise_cli/context/diff.py +213 -0
- raise_cli/context/extractors/__init__.py +13 -0
- raise_cli/context/extractors/skills.py +121 -0
- raise_cli/core/__init__.py +37 -0
- raise_cli/core/files.py +66 -0
- raise_cli/core/text.py +174 -0
- raise_cli/core/tools.py +441 -0
- raise_cli/discovery/__init__.py +50 -0
- raise_cli/discovery/analyzer.py +691 -0
- raise_cli/discovery/drift.py +355 -0
- raise_cli/discovery/scanner.py +1687 -0
- raise_cli/doctor/__init__.py +4 -0
- raise_cli/doctor/checks/__init__.py +1 -0
- raise_cli/doctor/checks/environment.py +110 -0
- raise_cli/doctor/checks/project.py +238 -0
- raise_cli/doctor/fix.py +80 -0
- raise_cli/doctor/models.py +56 -0
- raise_cli/doctor/protocol.py +43 -0
- raise_cli/doctor/registry.py +100 -0
- raise_cli/doctor/report.py +141 -0
- raise_cli/doctor/runner.py +95 -0
- raise_cli/engines/__init__.py +3 -0
- raise_cli/exceptions.py +215 -0
- raise_cli/gates/__init__.py +19 -0
- raise_cli/gates/builtin/__init__.py +1 -0
- raise_cli/gates/builtin/coverage.py +52 -0
- raise_cli/gates/builtin/lint.py +48 -0
- raise_cli/gates/builtin/tests.py +48 -0
- raise_cli/gates/builtin/types.py +48 -0
- raise_cli/gates/models.py +40 -0
- raise_cli/gates/protocol.py +41 -0
- raise_cli/gates/registry.py +141 -0
- raise_cli/governance/__init__.py +11 -0
- raise_cli/governance/extractor.py +412 -0
- raise_cli/governance/models.py +134 -0
- raise_cli/governance/parsers/__init__.py +35 -0
- raise_cli/governance/parsers/_convert.py +38 -0
- raise_cli/governance/parsers/adr.py +274 -0
- raise_cli/governance/parsers/backlog.py +356 -0
- raise_cli/governance/parsers/constitution.py +119 -0
- raise_cli/governance/parsers/epic.py +323 -0
- raise_cli/governance/parsers/glossary.py +316 -0
- raise_cli/governance/parsers/guardrails.py +345 -0
- raise_cli/governance/parsers/prd.py +112 -0
- raise_cli/governance/parsers/roadmap.py +118 -0
- raise_cli/governance/parsers/vision.py +116 -0
- raise_cli/graph/__init__.py +1 -0
- raise_cli/graph/backends/__init__.py +57 -0
- raise_cli/graph/backends/api.py +137 -0
- raise_cli/graph/backends/dual.py +139 -0
- raise_cli/graph/backends/pending.py +84 -0
- raise_cli/handlers/__init__.py +3 -0
- raise_cli/hooks/__init__.py +54 -0
- raise_cli/hooks/builtin/__init__.py +1 -0
- raise_cli/hooks/builtin/backlog.py +216 -0
- raise_cli/hooks/builtin/gate_bridge.py +83 -0
- raise_cli/hooks/builtin/jira_sync.py +127 -0
- raise_cli/hooks/builtin/memory.py +117 -0
- raise_cli/hooks/builtin/telemetry.py +72 -0
- raise_cli/hooks/emitter.py +184 -0
- raise_cli/hooks/events.py +262 -0
- raise_cli/hooks/protocol.py +38 -0
- raise_cli/hooks/registry.py +117 -0
- raise_cli/mcp/__init__.py +33 -0
- raise_cli/mcp/bridge.py +218 -0
- raise_cli/mcp/models.py +43 -0
- raise_cli/mcp/registry.py +77 -0
- raise_cli/mcp/schema.py +41 -0
- raise_cli/memory/__init__.py +58 -0
- raise_cli/memory/loader.py +247 -0
- raise_cli/memory/migration.py +241 -0
- raise_cli/memory/models.py +169 -0
- raise_cli/memory/writer.py +598 -0
- raise_cli/onboarding/__init__.py +103 -0
- raise_cli/onboarding/bootstrap.py +324 -0
- raise_cli/onboarding/claudemd.py +17 -0
- raise_cli/onboarding/conventions.py +742 -0
- raise_cli/onboarding/detection.py +374 -0
- raise_cli/onboarding/governance.py +443 -0
- raise_cli/onboarding/instructions.py +672 -0
- raise_cli/onboarding/manifest.py +201 -0
- raise_cli/onboarding/memory_md.py +399 -0
- raise_cli/onboarding/migration.py +207 -0
- raise_cli/onboarding/profile.py +624 -0
- raise_cli/onboarding/skill_conflict.py +100 -0
- raise_cli/onboarding/skill_manifest.py +176 -0
- raise_cli/onboarding/skills.py +437 -0
- raise_cli/onboarding/workflows.py +101 -0
- raise_cli/output/__init__.py +28 -0
- raise_cli/output/console.py +394 -0
- raise_cli/output/formatters/__init__.py +9 -0
- raise_cli/output/formatters/adapters.py +135 -0
- raise_cli/output/formatters/discover.py +439 -0
- raise_cli/output/formatters/skill.py +298 -0
- raise_cli/publish/__init__.py +3 -0
- raise_cli/publish/changelog.py +80 -0
- raise_cli/publish/check.py +179 -0
- raise_cli/publish/version.py +172 -0
- raise_cli/rai_base/__init__.py +22 -0
- raise_cli/rai_base/framework/__init__.py +7 -0
- raise_cli/rai_base/framework/methodology.yaml +233 -0
- raise_cli/rai_base/governance/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
- raise_cli/rai_base/governance/architecture/system-context.md +34 -0
- raise_cli/rai_base/governance/architecture/system-design.md +24 -0
- raise_cli/rai_base/governance/backlog.md +8 -0
- raise_cli/rai_base/governance/guardrails.md +17 -0
- raise_cli/rai_base/governance/prd.md +25 -0
- raise_cli/rai_base/governance/vision.md +16 -0
- raise_cli/rai_base/identity/__init__.py +8 -0
- raise_cli/rai_base/identity/core.md +119 -0
- raise_cli/rai_base/identity/perspective.md +119 -0
- raise_cli/rai_base/memory/__init__.py +7 -0
- raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
- raise_cli/schemas/__init__.py +3 -0
- raise_cli/schemas/journal.py +49 -0
- raise_cli/schemas/session_state.py +117 -0
- raise_cli/session/__init__.py +5 -0
- raise_cli/session/bundle.py +820 -0
- raise_cli/session/close.py +268 -0
- raise_cli/session/journal.py +119 -0
- raise_cli/session/resolver.py +126 -0
- raise_cli/session/state.py +187 -0
- raise_cli/skills/__init__.py +44 -0
- raise_cli/skills/locator.py +141 -0
- raise_cli/skills/name_checker.py +199 -0
- raise_cli/skills/parser.py +145 -0
- raise_cli/skills/scaffold.py +212 -0
- raise_cli/skills/schema.py +132 -0
- raise_cli/skills/skillsets.py +195 -0
- raise_cli/skills/validator.py +197 -0
- raise_cli/skills_base/__init__.py +80 -0
- raise_cli/skills_base/contract-template.md +60 -0
- raise_cli/skills_base/preamble.md +37 -0
- raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
- raise_cli/skills_base/rai-debug/SKILL.md +171 -0
- raise_cli/skills_base/rai-discover/SKILL.md +167 -0
- raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
- raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
- raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
- raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
- raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
- raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
- raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
- raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
- raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
- raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
- raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
- raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
- raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
- raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
- raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
- raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
- raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
- raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
- raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
- raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
- raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
- raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
- raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
- raise_cli/skills_base/rai-research/SKILL.md +143 -0
- raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
- raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
- raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
- raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
- raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
- raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
- raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
- raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
- raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
- raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
- raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
- raise_cli/telemetry/__init__.py +42 -0
- raise_cli/telemetry/schemas.py +285 -0
- raise_cli/telemetry/writer.py +217 -0
- raise_cli/tier/__init__.py +0 -0
- raise_cli/tier/context.py +134 -0
- raise_cli/viz/__init__.py +7 -0
- raise_cli/viz/generator.py +406 -0
- raise_cli-2.2.1.dist-info/METADATA +433 -0
- raise_cli-2.2.1.dist-info/RECORD +264 -0
- raise_cli-2.2.1.dist-info/WHEEL +4 -0
- raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
- raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
- 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,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
|