graphitect 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. graphify/__init__.py +30 -0
  2. graphify/__main__.py +757 -0
  3. graphify/_minhash.py +107 -0
  4. graphify/affected.py +318 -0
  5. graphify/always_on/agents-md.md +12 -0
  6. graphify/always_on/antigravity-rules.md +14 -0
  7. graphify/always_on/claude-md.md +9 -0
  8. graphify/always_on/gemini-md.md +9 -0
  9. graphify/always_on/kiro-steering.md +5 -0
  10. graphify/always_on/vscode-instructions.md +17 -0
  11. graphify/analyze.py +769 -0
  12. graphify/benchmark.py +152 -0
  13. graphify/build.py +2300 -0
  14. graphify/cache.py +1746 -0
  15. graphify/callflow_html.py +2051 -0
  16. graphify/cargo_introspect.py +109 -0
  17. graphify/cli.py +4745 -0
  18. graphify/cluster.py +409 -0
  19. graphify/command-kilo.md +15 -0
  20. graphify/cross_repo_calls.py +216 -0
  21. graphify/cross_repo_types.py +75 -0
  22. graphify/csharp_dispatch.py +154 -0
  23. graphify/dedup.py +1213 -0
  24. graphify/detect.py +2566 -0
  25. graphify/diagnostics.py +406 -0
  26. graphify/export.py +1349 -0
  27. graphify/exporters/__init__.py +1 -0
  28. graphify/exporters/base.py +14 -0
  29. graphify/exporters/graphdb.py +173 -0
  30. graphify/exporters/html.py +637 -0
  31. graphify/extract.py +7856 -0
  32. graphify/extractors/MIGRATION.md +107 -0
  33. graphify/extractors/__init__.py +66 -0
  34. graphify/extractors/apex.py +215 -0
  35. graphify/extractors/base.py +85 -0
  36. graphify/extractors/bash.py +579 -0
  37. graphify/extractors/blade.py +53 -0
  38. graphify/extractors/commonlisp.py +540 -0
  39. graphify/extractors/csharp.py +448 -0
  40. graphify/extractors/dart.py +564 -0
  41. graphify/extractors/dm.py +494 -0
  42. graphify/extractors/elixir.py +241 -0
  43. graphify/extractors/engine.py +6509 -0
  44. graphify/extractors/fortran.py +311 -0
  45. graphify/extractors/go.py +527 -0
  46. graphify/extractors/json_config.py +240 -0
  47. graphify/extractors/julia.py +289 -0
  48. graphify/extractors/markdown.py +408 -0
  49. graphify/extractors/models.py +131 -0
  50. graphify/extractors/objc.py +566 -0
  51. graphify/extractors/ocaml.py +289 -0
  52. graphify/extractors/pascal.py +688 -0
  53. graphify/extractors/pascal_forms.py +196 -0
  54. graphify/extractors/powershell.py +522 -0
  55. graphify/extractors/razor.py +192 -0
  56. graphify/extractors/resolution.py +3584 -0
  57. graphify/extractors/robot.py +296 -0
  58. graphify/extractors/rust.py +470 -0
  59. graphify/extractors/sln.py +92 -0
  60. graphify/extractors/sql.py +720 -0
  61. graphify/extractors/terraform.py +181 -0
  62. graphify/extractors/verilog.py +329 -0
  63. graphify/extractors/zig.py +181 -0
  64. graphify/file_slice.py +246 -0
  65. graphify/global_graph.py +194 -0
  66. graphify/google_workspace.py +237 -0
  67. graphify/hooks.py +933 -0
  68. graphify/ids.py +93 -0
  69. graphify/ingest.py +358 -0
  70. graphify/install.py +2366 -0
  71. graphify/llm.py +3544 -0
  72. graphify/manifest.py +4 -0
  73. graphify/manifest_ingest.py +311 -0
  74. graphify/mcp_ingest.py +386 -0
  75. graphify/multigraph_compat.py +212 -0
  76. graphify/pascal_resolution.py +129 -0
  77. graphify/paths.py +436 -0
  78. graphify/pg_introspect.py +165 -0
  79. graphify/prs.py +770 -0
  80. graphify/querylog.py +80 -0
  81. graphify/reflect.py +882 -0
  82. graphify/report.py +346 -0
  83. graphify/resolver_registry.py +85 -0
  84. graphify/ruby_resolution.py +242 -0
  85. graphify/scip_ingest.py +363 -0
  86. graphify/security.py +460 -0
  87. graphify/semantic_cleanup.py +336 -0
  88. graphify/serve.py +2608 -0
  89. graphify/skill-agents.md +710 -0
  90. graphify/skill-aider.md +1283 -0
  91. graphify/skill-amp.md +710 -0
  92. graphify/skill-claw.md +713 -0
  93. graphify/skill-codex.md +710 -0
  94. graphify/skill-copilot.md +713 -0
  95. graphify/skill-devin.md +1410 -0
  96. graphify/skill-droid.md +710 -0
  97. graphify/skill-kilo.md +722 -0
  98. graphify/skill-kiro.md +713 -0
  99. graphify/skill-opencode.md +705 -0
  100. graphify/skill-pi.md +713 -0
  101. graphify/skill-trae.md +711 -0
  102. graphify/skill-vscode.md +709 -0
  103. graphify/skill-windows.md +755 -0
  104. graphify/skill.md +713 -0
  105. graphify/skills/agents/references/add-watch.md +56 -0
  106. graphify/skills/agents/references/exports.md +87 -0
  107. graphify/skills/agents/references/extraction-spec.md +70 -0
  108. graphify/skills/agents/references/github-and-merge.md +46 -0
  109. graphify/skills/agents/references/hooks.md +33 -0
  110. graphify/skills/agents/references/query.md +311 -0
  111. graphify/skills/agents/references/transcribe.md +52 -0
  112. graphify/skills/agents/references/update.md +210 -0
  113. graphify/skills/amp/references/add-watch.md +56 -0
  114. graphify/skills/amp/references/exports.md +87 -0
  115. graphify/skills/amp/references/extraction-spec.md +70 -0
  116. graphify/skills/amp/references/github-and-merge.md +46 -0
  117. graphify/skills/amp/references/hooks.md +33 -0
  118. graphify/skills/amp/references/query.md +311 -0
  119. graphify/skills/amp/references/transcribe.md +52 -0
  120. graphify/skills/amp/references/update.md +210 -0
  121. graphify/skills/claude/references/add-watch.md +56 -0
  122. graphify/skills/claude/references/exports.md +87 -0
  123. graphify/skills/claude/references/extraction-spec.md +70 -0
  124. graphify/skills/claude/references/github-and-merge.md +46 -0
  125. graphify/skills/claude/references/hooks.md +33 -0
  126. graphify/skills/claude/references/query.md +311 -0
  127. graphify/skills/claude/references/transcribe.md +52 -0
  128. graphify/skills/claude/references/update.md +210 -0
  129. graphify/skills/claw/references/add-watch.md +56 -0
  130. graphify/skills/claw/references/exports.md +87 -0
  131. graphify/skills/claw/references/extraction-spec.md +31 -0
  132. graphify/skills/claw/references/github-and-merge.md +46 -0
  133. graphify/skills/claw/references/hooks.md +33 -0
  134. graphify/skills/claw/references/query.md +311 -0
  135. graphify/skills/claw/references/transcribe.md +52 -0
  136. graphify/skills/claw/references/update.md +210 -0
  137. graphify/skills/codex/references/add-watch.md +56 -0
  138. graphify/skills/codex/references/exports.md +87 -0
  139. graphify/skills/codex/references/extraction-spec.md +31 -0
  140. graphify/skills/codex/references/github-and-merge.md +46 -0
  141. graphify/skills/codex/references/hooks.md +33 -0
  142. graphify/skills/codex/references/query.md +311 -0
  143. graphify/skills/codex/references/transcribe.md +52 -0
  144. graphify/skills/codex/references/update.md +210 -0
  145. graphify/skills/copilot/references/add-watch.md +56 -0
  146. graphify/skills/copilot/references/exports.md +87 -0
  147. graphify/skills/copilot/references/extraction-spec.md +70 -0
  148. graphify/skills/copilot/references/github-and-merge.md +46 -0
  149. graphify/skills/copilot/references/hooks.md +33 -0
  150. graphify/skills/copilot/references/query.md +311 -0
  151. graphify/skills/copilot/references/transcribe.md +52 -0
  152. graphify/skills/copilot/references/update.md +210 -0
  153. graphify/skills/droid/references/add-watch.md +56 -0
  154. graphify/skills/droid/references/exports.md +87 -0
  155. graphify/skills/droid/references/extraction-spec.md +70 -0
  156. graphify/skills/droid/references/github-and-merge.md +46 -0
  157. graphify/skills/droid/references/hooks.md +33 -0
  158. graphify/skills/droid/references/query.md +311 -0
  159. graphify/skills/droid/references/transcribe.md +52 -0
  160. graphify/skills/droid/references/update.md +210 -0
  161. graphify/skills/kilo/references/add-watch.md +56 -0
  162. graphify/skills/kilo/references/exports.md +87 -0
  163. graphify/skills/kilo/references/extraction-spec.md +70 -0
  164. graphify/skills/kilo/references/github-and-merge.md +46 -0
  165. graphify/skills/kilo/references/hooks.md +33 -0
  166. graphify/skills/kilo/references/query.md +311 -0
  167. graphify/skills/kilo/references/transcribe.md +52 -0
  168. graphify/skills/kilo/references/update.md +210 -0
  169. graphify/skills/kiro/references/add-watch.md +56 -0
  170. graphify/skills/kiro/references/exports.md +87 -0
  171. graphify/skills/kiro/references/extraction-spec.md +31 -0
  172. graphify/skills/kiro/references/github-and-merge.md +46 -0
  173. graphify/skills/kiro/references/hooks.md +33 -0
  174. graphify/skills/kiro/references/query.md +311 -0
  175. graphify/skills/kiro/references/transcribe.md +52 -0
  176. graphify/skills/kiro/references/update.md +210 -0
  177. graphify/skills/opencode/references/add-watch.md +56 -0
  178. graphify/skills/opencode/references/exports.md +87 -0
  179. graphify/skills/opencode/references/extraction-spec.md +70 -0
  180. graphify/skills/opencode/references/github-and-merge.md +46 -0
  181. graphify/skills/opencode/references/hooks.md +33 -0
  182. graphify/skills/opencode/references/query.md +311 -0
  183. graphify/skills/opencode/references/transcribe.md +52 -0
  184. graphify/skills/opencode/references/update.md +210 -0
  185. graphify/skills/pi/references/add-watch.md +56 -0
  186. graphify/skills/pi/references/exports.md +87 -0
  187. graphify/skills/pi/references/extraction-spec.md +31 -0
  188. graphify/skills/pi/references/github-and-merge.md +46 -0
  189. graphify/skills/pi/references/hooks.md +33 -0
  190. graphify/skills/pi/references/query.md +311 -0
  191. graphify/skills/pi/references/transcribe.md +52 -0
  192. graphify/skills/pi/references/update.md +210 -0
  193. graphify/skills/trae/references/add-watch.md +56 -0
  194. graphify/skills/trae/references/exports.md +87 -0
  195. graphify/skills/trae/references/extraction-spec.md +70 -0
  196. graphify/skills/trae/references/github-and-merge.md +46 -0
  197. graphify/skills/trae/references/hooks.md +35 -0
  198. graphify/skills/trae/references/query.md +311 -0
  199. graphify/skills/trae/references/transcribe.md +52 -0
  200. graphify/skills/trae/references/update.md +210 -0
  201. graphify/skills/vscode/references/add-watch.md +56 -0
  202. graphify/skills/vscode/references/exports.md +87 -0
  203. graphify/skills/vscode/references/extraction-spec.md +70 -0
  204. graphify/skills/vscode/references/github-and-merge.md +46 -0
  205. graphify/skills/vscode/references/hooks.md +33 -0
  206. graphify/skills/vscode/references/query.md +311 -0
  207. graphify/skills/vscode/references/transcribe.md +52 -0
  208. graphify/skills/vscode/references/update.md +210 -0
  209. graphify/skills/windows/references/add-watch.md +56 -0
  210. graphify/skills/windows/references/exports.md +87 -0
  211. graphify/skills/windows/references/extraction-spec.md +70 -0
  212. graphify/skills/windows/references/github-and-merge.md +46 -0
  213. graphify/skills/windows/references/hooks.md +33 -0
  214. graphify/skills/windows/references/query.md +311 -0
  215. graphify/skills/windows/references/transcribe.md +52 -0
  216. graphify/skills/windows/references/update.md +210 -0
  217. graphify/symbol_resolution.py +556 -0
  218. graphify/transcribe.py +186 -0
  219. graphify/tree_html.py +603 -0
  220. graphify/validate.py +95 -0
  221. graphify/watch.py +2280 -0
  222. graphify/wiki.py +405 -0
  223. graphitect/__init__.py +28 -0
  224. graphitect/__main__.py +4 -0
  225. graphitect/_vendor/__init__.py +2 -0
  226. graphitect/_vendor/archify/LICENSE +22 -0
  227. graphitect/_vendor/archify/SKILL.md +137 -0
  228. graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
  229. graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
  230. graphitect/_vendor/archify/assets/template.html +14935 -0
  231. graphitect/_vendor/archify/bin/archify.mjs +2091 -0
  232. graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
  233. graphitect/_vendor/archify/bin/preview.mjs +653 -0
  234. graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
  235. graphitect/_vendor/archify/brand-marks/README.md +31 -0
  236. graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
  237. graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
  238. graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
  239. graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
  240. graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
  241. graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
  242. graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
  243. graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
  244. graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
  245. graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
  246. graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
  247. graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
  248. graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
  249. graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
  250. graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
  251. graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
  252. graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
  253. graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
  254. graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
  255. graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
  256. graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
  257. graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
  258. graphitect/_vendor/archify/package-lock.json +149 -0
  259. graphitect/_vendor/archify/package.json +39 -0
  260. graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
  261. graphitect/_vendor/archify/references/authoring-contract.md +243 -0
  262. graphitect/_vendor/archify/references/brand-marks.md +65 -0
  263. graphitect/_vendor/archify/references/delivery-contract.md +120 -0
  264. graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
  265. graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
  266. graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
  267. graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
  268. graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  269. graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
  270. graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  271. graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
  272. graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
  273. graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
  274. graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
  275. graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
  276. graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
  277. graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
  278. graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  279. graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
  280. graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
  281. graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
  282. graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
  283. graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
  284. graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
  285. graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
  286. graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
  287. graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
  288. graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
  289. graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
  290. graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
  291. graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
  292. graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  293. graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  294. graphitect/_vendor/archify/schemas/README.md +211 -0
  295. graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
  296. graphitect/_vendor/archify/schemas/common.schema.json +115 -0
  297. graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
  298. graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
  299. graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
  300. graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
  301. graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
  302. graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
  303. graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
  304. graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
  305. graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
  306. graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
  307. graphitect/_vendor/archify/skill-release.json +10 -0
  308. graphitect/cli.py +981 -0
  309. graphitect/deliver/__init__.py +5 -0
  310. graphitect/deliver/archify_adapter.py +1877 -0
  311. graphitect/deliver/archify_ir.py +160 -0
  312. graphitect/deliver/archify_repair.py +135 -0
  313. graphitect/deliver/doc_compiler.py +916 -0
  314. graphitect/ground/__init__.py +5 -0
  315. graphitect/ground/describe_source.py +27 -0
  316. graphitect/ground/fullread_source.py +56 -0
  317. graphitect/ground/graphify_source.py +107 -0
  318. graphitect/models.py +118 -0
  319. graphitect/skill/SKILL.md +80 -0
  320. graphitect/skill/agents/openai.yaml +4 -0
  321. graphitect/synthesize/__init__.py +5 -0
  322. graphitect/synthesize/engine.py +281 -0
  323. graphitect/synthesize/llm_backend.py +331 -0
  324. graphitect/synthesize/questions.py +139 -0
  325. graphitect/synthesize/rubric.py +104 -0
  326. graphitect-0.2.0.dist-info/METADATA +284 -0
  327. graphitect-0.2.0.dist-info/RECORD +336 -0
  328. graphitect-0.2.0.dist-info/WHEEL +5 -0
  329. graphitect-0.2.0.dist-info/entry_points.txt +2 -0
  330. graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
  331. graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
  332. graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
  333. graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
  334. graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
  335. graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
  336. graphitect-0.2.0.dist-info/top_level.txt +2 -0
graphify/tree_html.py ADDED
@@ -0,0 +1,603 @@
1
+ """tree_html — emit a D3 v7 collapsible-tree HTML view of a graph.
2
+
3
+ A self-contained printable / browseable tree-of-modules view
4
+ intended to complement the existing force-directed ``graph.html``.
5
+ Key visual elements:
6
+
7
+ * Expand-all / collapse-all / reset-view buttons.
8
+ * Multi-line label wrapping (``wrapText``) with separately-coloured
9
+ name and descendant-count.
10
+ * Depth-based colour palette (top-level directories get distinct
11
+ accent colours; deeper levels follow a level-specific palette).
12
+ * Click-to-toggle subtree.
13
+
14
+ Tree-data shape:
15
+
16
+ {
17
+ "name": "<root label>",
18
+ "total_count": <int>,
19
+ "children": [ { "name", "total_count", "children": [...] }, ... ]
20
+ }
21
+
22
+ CLI: ``graphify tree [--graph PATH] [--output HTML] [--root PATH]
23
+ [--max-children N] [--label NAME]``.
24
+
25
+ Implementation notes:
26
+ - ``total_count`` is the descendant leaf count, so collapsed nodes
27
+ can show ``(Total Count: 95)`` without needing the children loaded.
28
+ - ``--max-children`` (default 200) caps how many children render
29
+ under any one node; a synthetic ``(+N more)`` leaf appears when the
30
+ cap fires so very wide directories stay usable.
31
+ - The first-level palette is auto-populated from the live top-level
32
+ directories so each gets a stable accent colour.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import html as _html
38
+ import json
39
+ from collections import defaultdict
40
+ from pathlib import Path
41
+ from typing import Any, Dict, List, Optional
42
+
43
+ DEFAULT_MAX_CHILDREN = 200
44
+
45
+
46
+ # ── Tree builder (filesystem hierarchy → JSON) ──────────────────
47
+
48
+
49
+ def _common_root(paths: List[str]) -> str:
50
+ if not paths:
51
+ return ""
52
+ parts = [Path(p).parts for p in paths if p]
53
+ if not parts:
54
+ return ""
55
+ common = parts[0]
56
+ for p in parts[1:]:
57
+ i = 0
58
+ while i < len(common) and i < len(p) and common[i] == p[i]:
59
+ i += 1
60
+ common = common[:i]
61
+ return str(Path(*common)) if common else ""
62
+
63
+
64
+ def _make_truncation_leaf(extra: int) -> Dict[str, Any]:
65
+ return {"name": f"(+{extra} more)", "total_count": extra, "children": []}
66
+
67
+
68
+ def build_tree(
69
+ graph: Dict[str, Any],
70
+ *,
71
+ root: Optional[str] = None,
72
+ max_children: int = DEFAULT_MAX_CHILDREN,
73
+ project_label: Optional[str] = None,
74
+ ) -> Dict[str, Any]:
75
+ """Build a ``{name, total_count, children}`` hierarchy.
76
+
77
+ Each leaf is either a code symbol (class / top-level function) or
78
+ a synthetic "(+N more)" placeholder for truncated wide directories.
79
+ Each interior node carries ``total_count = sum of leaf counts``.
80
+
81
+ Raises ``ValueError`` when an explicit ``root`` matches none of the
82
+ graph's ``source_file`` paths — every file would silently attach flat
83
+ to the root and the hierarchy would be lost (#2534).
84
+ """
85
+ nodes: List[Dict[str, Any]] = list(graph.get("nodes", []))
86
+ file_nodes = [n for n in nodes if n.get("source_file")]
87
+ if not file_nodes:
88
+ return {"name": "(empty graph)", "total_count": 0, "children": []}
89
+
90
+ explicit_root = root is not None
91
+ if root is None:
92
+ root = _common_root([n["source_file"] for n in file_nodes])
93
+ root_path = Path(root)
94
+
95
+ by_file: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
96
+ for n in file_nodes:
97
+ by_file[n["source_file"]].append(n)
98
+
99
+ # Build dir tree.
100
+ dir_index: Dict[str, Dict[str, Any]] = {}
101
+ label_root = project_label or root_path.name or root or "/"
102
+ root_node: Dict[str, Any] = {
103
+ "name": label_root, "total_count": 0, "children": [],
104
+ }
105
+ dir_index[str(root_path)] = root_node
106
+
107
+ def _ensure_dir(abs_path: Path) -> Dict[str, Any]:
108
+ key = str(abs_path)
109
+ if key in dir_index:
110
+ return dir_index[key]
111
+ if abs_path == abs_path.parent:
112
+ return root_node
113
+ parent = (_ensure_dir(abs_path.parent)
114
+ if abs_path.parent != abs_path else root_node)
115
+ node = {"name": abs_path.name, "total_count": 0, "children": []}
116
+ dir_index[key] = node
117
+ parent["children"].append(node)
118
+ return node
119
+
120
+ matched_files = 0
121
+ for src_file, syms in sorted(by_file.items()):
122
+ src_path = Path(src_file)
123
+ try:
124
+ rel = src_path.relative_to(root_path)
125
+ parent_path = (root_path / rel).parent
126
+ matched_files += 1
127
+ except ValueError:
128
+ parent_path = root_path
129
+ parent_dir = _ensure_dir(parent_path)
130
+
131
+ # File node — children are the symbols.
132
+ sym_children: List[Dict[str, Any]] = []
133
+ for n in syms:
134
+ label = n.get("label", n.get("id", "?"))
135
+ # Skip the redundant file-name node graphify emits (bare basename or
136
+ # the directory-qualified form from the #2032 disambiguation pass).
137
+ if n.get("file_type") == "code":
138
+ from graphify.build import _is_file_node_label
139
+ if _is_file_node_label(label, src_file):
140
+ continue
141
+ sym_children.append({
142
+ "name": label,
143
+ "total_count": 1,
144
+ "children": [],
145
+ })
146
+ # Sort: code symbols first by name, then anything else.
147
+ sym_children.sort(key=lambda c: (
148
+ c["name"].startswith("_"),
149
+ c["name"].lower(),
150
+ ))
151
+ if len(sym_children) > max_children:
152
+ extra = len(sym_children) - max_children
153
+ sym_children = sym_children[:max_children] + [
154
+ _make_truncation_leaf(extra),
155
+ ]
156
+ file_node = {
157
+ "name": src_path.name,
158
+ "total_count": len(sym_children) or 1,
159
+ "children": sym_children,
160
+ }
161
+ parent_dir["children"].append(file_node)
162
+
163
+ # An explicit --root that matches NOTHING would silently flatten the whole
164
+ # hierarchy (every file attaches to the root); the common case is passing an
165
+ # absolute checkout path while source_file is stored repo-relative (#2534).
166
+ # A PARTIAL match stays fine — files outside the root legitimately attach flat.
167
+ if explicit_root and matched_files == 0:
168
+ sample = min(by_file)
169
+ raise ValueError(
170
+ f"--root {root} matched 0 of {len(by_file)} source files "
171
+ f"(source_file paths are stored repo-relative, e.g. {sample!r})"
172
+ )
173
+
174
+ # Sort each dir's children + propagate total_count up.
175
+ def _finalise(d: Dict[str, Any]) -> int:
176
+ kids = d.get("children") or []
177
+ kids.sort(key=lambda c: (
178
+ 0 if (c.get("children") and len(c["children"]) > 0) else 1,
179
+ c["name"].lower(),
180
+ ))
181
+ if not kids:
182
+ return d.get("total_count") or 1
183
+ n = 0
184
+ for c in kids:
185
+ n += _finalise(c)
186
+ d["total_count"] = n or 1
187
+ return d["total_count"]
188
+
189
+ _finalise(root_node)
190
+ return root_node
191
+
192
+
193
+ # ── HTML emitter (single-data-blob substitution) ──────────────────
194
+
195
+
196
+ # We emit a Python f-string with literal CSS/JS braces escaped as {{ }}.
197
+ _HTML_TEMPLATE = r"""<!DOCTYPE html>
198
+ <html lang="en">
199
+ <head>
200
+ <meta charset="UTF-8">
201
+ <title>{title}</title>
202
+ <style>
203
+ body {{
204
+ font-family: 'Segoe UI', sans-serif;
205
+ margin: 0;
206
+ padding: 0;
207
+ background: #f9f9f9;
208
+ color: #333;
209
+ }}
210
+ h1 {{
211
+ margin: 20px 0 0 24px;
212
+ font-size: 2.2rem;
213
+ font-weight: bold;
214
+ color: #1e3a56;
215
+ }}
216
+ .controls {{
217
+ margin: 20px 0 15px 24px;
218
+ }}
219
+ button {{
220
+ margin-right: 10px;
221
+ padding: 8px 18px;
222
+ background: #007bff;
223
+ color: #fff;
224
+ border: none;
225
+ border-radius: 5px;
226
+ font-size: 0.95rem;
227
+ cursor: pointer;
228
+ transition: background 0.2s ease-in-out;
229
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
230
+ }}
231
+ button:hover {{
232
+ background: #0056b3;
233
+ }}
234
+ button:active {{
235
+ background: #004085;
236
+ }}
237
+ #tree-container {{
238
+ width: calc(100vw - 48px); /* Adjust for body margin/padding */
239
+ height: 85vh;
240
+ overflow: auto;
241
+ border-radius: 8px;
242
+ background: #fff;
243
+ margin-left: 24px;
244
+ margin-right: 24px;
245
+ box-shadow: 0 4px 12px rgba(0,0,0,0.08);
246
+ border: 1px solid #ddd;
247
+ }}
248
+ svg {{
249
+ background: #fff;
250
+ border-radius: 8px;
251
+ display: block; /* Important for D3 */
252
+ }}
253
+ .node circle {{
254
+ stroke-width: 2.5px;
255
+ }}
256
+ .node text {{ /* Base style for the <text> container */
257
+ font: 13px 'Segoe UI', sans-serif;
258
+ paint-order: stroke fill; /* Ensures text is readable over lines */
259
+ stroke: #fff; /* White halo */
260
+ stroke-width: 3px; /* Halo thickness */
261
+ stroke-linejoin: round;
262
+ stroke-opacity: 0.85; /* Halo opacity */
263
+ }}
264
+ .link {{
265
+ fill: none;
266
+ stroke-opacity: 0.7;
267
+ stroke-width: 2px;
268
+ }}
269
+ </style>
270
+ </head>
271
+ <body>
272
+ <h1>{header}</h1>
273
+ <div class="controls">
274
+ <button onclick="expandAll()">Expand All</button>
275
+ <button onclick="collapseAll()">Collapse All</button>
276
+ <button onclick="resetView()">Reset View</button>
277
+ </div>
278
+ <div id="tree-container">
279
+ <svg id="tree-svg" width="{svg_width}" height="{svg_height}"></svg>
280
+ </div>
281
+
282
+ <script src="https://d3js.org/d3.v7.min.js"></script>
283
+ <script>
284
+ const initialJsonData = {data_json};
285
+
286
+ function transformData(jsonData) {{
287
+ // Helper function to recursively build the children structure
288
+ function processNode(node, parentL1StageName) {{
289
+ let displayName = node.name;
290
+ // Append total_count if it exists and is not already in the name
291
+ if (node.total_count !== undefined) {{
292
+ if (!/\(Total Count: \d+\)$/.test(displayName)) {{
293
+ displayName += ` (Total Count: ${{node.total_count}})`;
294
+ }}
295
+ }}
296
+
297
+ const newNode = {{ name: displayName }};
298
+
299
+ if (parentL1StageName === "Root") {{
300
+ newNode.originalStageName = node.name;
301
+ }} else {{
302
+ newNode.originalStageName = parentL1StageName;
303
+ }}
304
+
305
+ if (node.children && node.children.length > 0) {{
306
+ const stageNameToPass = (parentL1StageName === "Root") ? node.name : parentL1StageName;
307
+ newNode.children = node.children.map(child => processNode(child, stageNameToPass));
308
+ }}
309
+
310
+ return newNode;
311
+ }}
312
+
313
+ let rootDisplayName = jsonData.name;
314
+ if (jsonData.total_count !== undefined && !/\(Total Count: \d+\)$/.test(rootDisplayName)) {{
315
+ rootDisplayName += ` (Total Count: ${{jsonData.total_count}})`;
316
+ }}
317
+
318
+ return {{
319
+ name: rootDisplayName,
320
+ originalStageName: "Root",
321
+ children: (jsonData.children || []).map(child => processNode(child, "Root"))
322
+ }};
323
+ }}
324
+
325
+ const treeData = transformData(initialJsonData);
326
+
327
+ // Auto-populated phaseColors: every depth-1 child of the root gets
328
+ // a stable colour from a bigger palette so all top-level dirs are
329
+ // distinguishable.
330
+ const PALETTE = [
331
+ ["#3498DB","#2980B9","#AED6F1"], ["#2ECC71","#27AE60","#A9DFBF"],
332
+ ["#E74C3C","#C0392B","#F5B7B1"], ["#9B59B6","#8E44AD","#D7BDE2"],
333
+ ["#F39C12","#D68910","#FAD7A0"], ["#1ABC9C","#117864","#A2D9CE"],
334
+ ["#34495E","#1B2631","#ABB2B9"], ["#E67E22","#BA4A00","#F5CBA7"],
335
+ ["#16A085","#0E6655","#A2D9CE"], ["#D35400","#A04000","#EDBB99"],
336
+ ["#7F8C8D","#566573","#D5DBDB"], ["#C0392B","#7B241C","#F5B7B1"],
337
+ ["#2E86C1","#1B4F72","#A9CCE3"], ["#28B463","#196F3D","#A9DFBF"],
338
+ ["#AF7AC5","#6C3483","#D2B4DE"],
339
+ ];
340
+ const phaseColors = {{ "Root": {{ fill: "#4A4A4A", stroke: "#333333", collapsedFill: "#6C757D" }},
341
+ "Default": {{ fill: "#BDC3C7", stroke: "#95A5A6", collapsedFill: "#ECF0F1" }} }};
342
+ (initialJsonData.children || []).forEach((c, i) => {{
343
+ const pal = PALETTE[i % PALETTE.length];
344
+ phaseColors[c.name] = {{ fill: pal[0], stroke: pal[1], collapsedFill: pal[2] }};
345
+ }});
346
+
347
+ const levelSpecificPalettes = {{
348
+ 0: {{ fill: "#4A4A4A", stroke: "#333333", collapsedFill: "#6C757D" }},
349
+ 2: {{ fill: "#6ab04c", stroke: "#508a38", collapsedFill: "#a3d391" }},
350
+ 3: {{ fill: "#f0932b", stroke: "#d0730f", collapsedFill: "#f6c07e" }},
351
+ 4: {{ fill: "#be2edd", stroke: "#a01cb3", collapsedFill: "#e08bf2" }},
352
+ 5: {{ fill: "#00a8ff", stroke: "#007ac1", collapsedFill: "#74d2ff" }},
353
+ 6: {{ fill: "#e55039", stroke: "#c23620", collapsedFill: "#f09a8d" }},
354
+ default: {{ fill: "#747d8c", stroke: "#57606f", collapsedFill: "#a4b0be" }}
355
+ }};
356
+
357
+ const svgElement = d3.select("#tree-svg");
358
+ const initialSvgWidth = +svgElement.attr("width");
359
+ const initialSvgHeight = +svgElement.attr("height");
360
+ const margin = {{ top: 40, right: 120, bottom: 80, left: 450 }};
361
+ let width = initialSvgWidth - margin.left - margin.right;
362
+ let height = initialSvgHeight - margin.top - margin.bottom;
363
+ const duration = 500;
364
+ let nodeCounter = 0;
365
+ const g = svgElement.append("g").attr("transform", `translate(${{margin.left}},${{margin.top}})`);
366
+ const treemap = d3.tree().nodeSize([40, 0]);
367
+ let rootNode = d3.hierarchy(treeData, d => d.children);
368
+ rootNode.x0 = 0;
369
+ rootNode.y0 = 0;
370
+
371
+ if (rootNode.children) {{
372
+ rootNode.children.forEach(d_child => {{
373
+ if (d_child.children) {{ collapseBranch(d_child); }}
374
+ }});
375
+ }}
376
+ updateTree(rootNode);
377
+
378
+ function collapseBranch(d) {{ if (d.children) {{ d._children = d.children; d._children.forEach(collapseBranch); d.children = null; }} }}
379
+ function expandBranch(d) {{ if (d._children) {{ d.children = d._children; d._children = null; }} if (d.children) {{ d.children.forEach(expandBranch); }} }}
380
+ window.expandAll = () => {{ expandBranch(rootNode); updateTree(rootNode); }};
381
+ window.collapseAll = () => {{ if (rootNode.children) {{ rootNode.children.forEach(collapseBranch); }} updateTree(rootNode); }};
382
+ window.resetView = () => {{ if (rootNode.children) {{ rootNode.children.forEach(d_child => {{ if (d_child.children || d_child._children) {{ collapseBranch(d_child); }} }}); }} if (rootNode._children && !rootNode.children) {{ rootNode.children = rootNode._children; rootNode._children = null; }} updateTree(rootNode); }};
383
+
384
+ function updateTree(source) {{
385
+ const treeLayoutData = treemap(rootNode);
386
+ let nodes = treeLayoutData.descendants();
387
+ let links = treeLayoutData.descendants().slice(1);
388
+
389
+ let minX = 0;
390
+ let maxX = 0;
391
+ if (nodes.length > 0) {{
392
+ minX = d3.min(nodes, d => d.x);
393
+ maxX = d3.max(nodes, d => d.x);
394
+ }}
395
+
396
+ let neededHeight = Math.max(initialSvgHeight, maxX - minX + margin.top + margin.bottom + 100);
397
+ svgElement.transition().duration(duration / 2).attr("height", neededHeight);
398
+ g.transition().duration(duration / 2).attr("transform", `translate(${{margin.left}},${{margin.top - minX + 40}})`);
399
+
400
+ nodes.forEach(d => {{ d.y = d.depth * 400; }}); // Adjust horizontal separation if needed
401
+
402
+ const node = g.selectAll('g.node').data(nodes, d => d.id || (d.id = ++nodeCounter));
403
+ const nodeEnter = node.enter().append('g')
404
+ .attr('class', d => "node" + (d.children || d._children ? " node--internal" : " node--leaf") + (d._children ? " _children" : ""))
405
+ .attr('transform', d => `translate(${{source.y0}},${{source.x0}})`)
406
+ .on('click', (event, d) => {{ if (d.children) {{ d._children = d.children; d.children = null; }} else if (d._children) {{ d.children = d._children; d._children = null; }} updateTree(d); }})
407
+ .style('cursor', d => (d.children || d._children) ? 'pointer' : 'default');
408
+
409
+ nodeEnter.append('circle').attr('r', 1e-6);
410
+
411
+ nodeEnter.append('text')
412
+ .attr('dy', '.35em')
413
+ .attr('x', d => d.children || d._children ? -14 : 14)
414
+ .attr('text-anchor', d => d.children || d._children ? 'end' : 'start')
415
+ .style("fill-opacity", 1e-6)
416
+ .call(wrapText, 380);
417
+
418
+ const nodeUpdate = nodeEnter.merge(node);
419
+ nodeUpdate.transition().duration(duration)
420
+ .attr('transform', d => `translate(${{d.y}},${{d.x}})`)
421
+ .attr('class', d => "node" + (d.children ? " node--internal" : " node--leaf") + (d._children ? " node--internal _children" : ""));
422
+
423
+ nodeUpdate.select('circle').attr('r', 8.5)
424
+ .style('fill', d => {{
425
+ let palette;
426
+ if (d.depth === 0) {{
427
+ palette = levelSpecificPalettes[0];
428
+ }} else if (d.depth === 1) {{
429
+ palette = phaseColors[d.data.originalStageName] || phaseColors.Default;
430
+ }} else {{
431
+ palette = levelSpecificPalettes[d.depth] || levelSpecificPalettes.default;
432
+ }}
433
+ if (d._children) return palette.collapsedFill;
434
+ if (d.children) return palette.fill;
435
+ return "#fff";
436
+ }})
437
+ .style('stroke', d => {{
438
+ let palette;
439
+ if (d.depth === 0) {{
440
+ palette = levelSpecificPalettes[0];
441
+ }} else if (d.depth === 1) {{
442
+ palette = phaseColors[d.data.originalStageName] || phaseColors.Default;
443
+ }} else {{
444
+ palette = levelSpecificPalettes[d.depth] || levelSpecificPalettes.default;
445
+ }}
446
+ return palette.stroke;
447
+ }});
448
+ nodeUpdate.select('text').style("fill-opacity", 1).call(wrapText, 380);
449
+
450
+ const nodeExit = node.exit().transition().duration(duration).attr('transform', d => `translate(${{source.y}},${{source.x}})`).remove();
451
+ nodeExit.select('circle').attr('r', 1e-6);
452
+ nodeExit.select('text').style('fill-opacity', 1e-6);
453
+
454
+ const link = g.selectAll('path.link').data(links, d => d.id);
455
+ const linkEnter = link.enter().insert('path', "g").attr('class', 'link').attr('d', d => {{ const o = {{ x: source.x0, y: source.y0 }}; return diagonal(o, o); }});
456
+
457
+ linkEnter.merge(link).transition().duration(duration).attr('d', d => diagonal(d, d.parent))
458
+ .style('stroke', d => {{
459
+ const sourceNode = d.parent;
460
+ if (!sourceNode) return phaseColors.Default.stroke;
461
+ const l1AncestorName = sourceNode.data.originalStageName;
462
+ const colorPalette = phaseColors[l1AncestorName] || phaseColors.Default;
463
+ return colorPalette.stroke;
464
+ }});
465
+ link.exit().transition().duration(duration).attr('d', d => {{ const o = {{ x: source.x, y: source.y }}; return diagonal(o, o); }}).remove();
466
+ nodes.forEach(d => {{ d.x0 = d.x; d.y0 = d.y; }});
467
+ }}
468
+
469
+ function diagonal(s, d) {{ return `M ${{s.y}} ${{s.x}} C ${{(s.y + d.y) / 2}} ${{s.x}}, ${{(s.y + d.y) / 2}} ${{d.x}}, ${{d.y}} ${{d.x}}`; }}
470
+
471
+ function wrapText(textElements, maxWidth) {{
472
+ const textPartColors = {{
473
+ name: '#343a40',
474
+ count: '#0056b3'
475
+ }};
476
+ const countRegex = /(\s\(Total Count: \d+\))$/;
477
+
478
+ textElements.each(function () {{
479
+ const textD3 = d3.select(this);
480
+ const originalNodeText = textD3.datum().data.name;
481
+ const x = parseFloat(textD3.attr("x") || 0);
482
+ const initialDy = textD3.attr("dy");
483
+ const textAnchor = textD3.attr("text-anchor");
484
+ const lineHeight = 1.1;
485
+
486
+ textD3.text(null);
487
+
488
+ let namePart = originalNodeText;
489
+ let countPartText = "";
490
+
491
+ const countMatch = originalNodeText.match(countRegex);
492
+ if (countMatch && originalNodeText.endsWith(countMatch[0])) {{
493
+ namePart = originalNodeText.substring(0, originalNodeText.length - countMatch[0].length).trim();
494
+ countPartText = countMatch[0].trim();
495
+ }}
496
+
497
+ const tokens = [];
498
+ namePart.split(/\s+/).filter(Boolean).forEach(word => {{
499
+ tokens.push({{ text: word, type: 'name' }});
500
+ }});
501
+ if (countPartText) {{
502
+ tokens.push({{ text: countPartText, type: 'count' }});
503
+ }}
504
+
505
+ if (tokens.length === 0 && originalNodeText) {{
506
+ tokens.push({{ text: originalNodeText, type: 'name' }});
507
+ }}
508
+
509
+ let currentTspan = textD3.append("tspan").attr("x", x).attr("dy", initialDy);
510
+ if (textAnchor === "end") currentTspan.attr("text-anchor", "end");
511
+
512
+ let lineTokens = [];
513
+
514
+ for (let i = 0; i < tokens.length; i++) {{
515
+ const tokenObj = tokens[i];
516
+
517
+ lineTokens.push(tokenObj);
518
+ currentTspan.text(lineTokens.map(t => t.text).join(" "));
519
+
520
+ if (currentTspan.node().getComputedTextLength() > maxWidth && lineTokens.length > 1) {{
521
+ lineTokens.pop();
522
+
523
+ currentTspan.text(null);
524
+ lineTokens.forEach((prevToken, idx) => {{
525
+ currentTspan.append("tspan")
526
+ .text((idx > 0 ? " " : "") + prevToken.text)
527
+ .style("fill", textPartColors[prevToken.type] || textPartColors.name)
528
+ .style("font-weight", prevToken.type === 'count' ? "bold" : "normal");
529
+ }});
530
+
531
+ lineTokens = [tokenObj];
532
+ currentTspan = textD3.append("tspan").attr("x", x).attr("dy", lineHeight + "em");
533
+ if (textAnchor === "end") currentTspan.attr("text-anchor", "end");
534
+ }}
535
+ }}
536
+
537
+ currentTspan.text(null);
538
+ lineTokens.forEach((token, idx) => {{
539
+ currentTspan.append("tspan")
540
+ .text((idx > 0 ? " " : "") + token.text)
541
+ .style("fill", textPartColors[token.type] || textPartColors.name)
542
+ .style("font-weight", token.type === 'count' ? "bold" : "normal");
543
+ }});
544
+
545
+ if (textD3.selectAll("tspan > tspan").empty() && textD3.select("tspan").text().length === 0 && originalNodeText) {{
546
+ let t = textD3.select("tspan");
547
+ let displayText = originalNodeText;
548
+ t.text(displayText).style("fill", textPartColors.name);
549
+ if (t.node() && t.node().getComputedTextLength() > maxWidth && displayText.length > 20) {{
550
+ let estimatedChars = Math.floor(maxWidth / (t.node().getComputedTextLength()/displayText.length) );
551
+ displayText = displayText.substring(0, Math.max(0, estimatedChars - 3)) + "...";
552
+ t.text(displayText);
553
+ }}
554
+ }}
555
+ }});
556
+ }}
557
+ </script>
558
+ </body>
559
+ </html>
560
+ """
561
+
562
+
563
+ def emit_html(
564
+ tree: Dict[str, Any],
565
+ *,
566
+ title: str,
567
+ header: str,
568
+ svg_width: int = 6000,
569
+ svg_height: int = 8000,
570
+ ) -> str:
571
+ # Escape </script> sequences so embedded JSON cannot break out of the
572
+ # <script> tag, and HTML-escape values that land in <title>/<h1>.
573
+ data_json = json.dumps(tree, ensure_ascii=True, separators=(",", ":")).replace("</", "<\\/")
574
+ return _HTML_TEMPLATE.format(
575
+ title=_html.escape(title),
576
+ header=_html.escape(header),
577
+ svg_width=svg_width,
578
+ svg_height=svg_height,
579
+ data_json=data_json,
580
+ )
581
+
582
+
583
+ def write_tree_html(
584
+ graph_path: Path,
585
+ output_path: Path,
586
+ *,
587
+ root: Optional[str] = None,
588
+ max_children: int = DEFAULT_MAX_CHILDREN,
589
+ project_label: Optional[str] = None,
590
+ # kept for CLI compatibility with the older signature; ignored now
591
+ top_k_edges: int = 0,
592
+ ) -> Path:
593
+ from graphify.security import check_graph_file_size_cap
594
+ check_graph_file_size_cap(graph_path)
595
+ graph = json.loads(graph_path.read_text(encoding="utf-8"))
596
+ tree = build_tree(graph, root=root, max_children=max_children,
597
+ project_label=project_label)
598
+ title = f"{tree['name']} — graphify tree viewer"
599
+ header = f"{tree['name']} — Knowledge Graph"
600
+ html = emit_html(tree, title=title, header=header)
601
+ output_path.parent.mkdir(parents=True, exist_ok=True)
602
+ output_path.write_text(html, encoding="utf-8")
603
+ return output_path