graphifyy 0.9.3__tar.gz → 0.9.4__tar.gz

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 (307) hide show
  1. {graphifyy-0.9.3/graphifyy.egg-info → graphifyy-0.9.4}/PKG-INFO +1 -1
  2. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/__main__.py +51 -8
  3. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/affected.py +8 -1
  4. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/build.py +103 -19
  5. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/cluster.py +27 -0
  6. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/dedup.py +20 -2
  7. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/diagnostics.py +7 -1
  8. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extract.py +602 -3
  9. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/hooks.py +17 -0
  10. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/watch.py +6 -3
  11. {graphifyy-0.9.3 → graphifyy-0.9.4/graphifyy.egg-info}/PKG-INFO +1 -1
  12. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphifyy.egg-info/SOURCES.txt +7 -0
  13. {graphifyy-0.9.3 → graphifyy-0.9.4}/pyproject.toml +1 -1
  14. graphifyy-0.9.4/tests/test_build_merge_hyperedges_and_prune.py +151 -0
  15. graphifyy-0.9.4/tests/test_community_hub_labels.py +66 -0
  16. graphifyy-0.9.4/tests/test_corrupt_graph_json.py +53 -0
  17. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_dedup.py +40 -0
  18. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_hooks.py +12 -2
  19. graphifyy-0.9.4/tests/test_indirect_dispatch.py +479 -0
  20. graphifyy-0.9.4/tests/test_indirect_dispatch_assign_return.py +133 -0
  21. graphifyy-0.9.4/tests/test_indirect_dispatch_getattr.py +153 -0
  22. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_languages.py +45 -0
  23. graphifyy-0.9.4/tests/test_skill_version_warning.py +60 -0
  24. {graphifyy-0.9.3 → graphifyy-0.9.4}/LICENSE +0 -0
  25. {graphifyy-0.9.3 → graphifyy-0.9.4}/README.md +0 -0
  26. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/__init__.py +0 -0
  27. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/_minhash.py +0 -0
  28. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/always_on/agents-md.md +0 -0
  29. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/always_on/antigravity-rules.md +0 -0
  30. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/always_on/claude-md.md +0 -0
  31. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/always_on/gemini-md.md +0 -0
  32. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/always_on/kiro-steering.md +0 -0
  33. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/always_on/vscode-instructions.md +0 -0
  34. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/analyze.py +0 -0
  35. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/benchmark.py +0 -0
  36. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/cache.py +0 -0
  37. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/callflow_html.py +0 -0
  38. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/cargo_introspect.py +0 -0
  39. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/command-kilo.md +0 -0
  40. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/detect.py +0 -0
  41. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/export.py +0 -0
  42. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/__init__.py +0 -0
  43. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/base.py +0 -0
  44. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/blade.py +0 -0
  45. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/csharp.py +0 -0
  46. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/elixir.py +0 -0
  47. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/razor.py +0 -0
  48. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/extractors/zig.py +0 -0
  49. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/file_slice.py +0 -0
  50. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/global_graph.py +0 -0
  51. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/google_workspace.py +0 -0
  52. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/ids.py +0 -0
  53. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/ingest.py +0 -0
  54. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/llm.py +0 -0
  55. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/manifest.py +0 -0
  56. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/manifest_ingest.py +0 -0
  57. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/mcp_ingest.py +0 -0
  58. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/multigraph_compat.py +0 -0
  59. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/paths.py +0 -0
  60. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/pg_introspect.py +0 -0
  61. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/prs.py +0 -0
  62. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/querylog.py +0 -0
  63. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/reflect.py +0 -0
  64. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/report.py +0 -0
  65. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/resolver_registry.py +0 -0
  66. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/ruby_resolution.py +0 -0
  67. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/scip_ingest.py +0 -0
  68. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/security.py +0 -0
  69. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/semantic_cleanup.py +0 -0
  70. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/serve.py +0 -0
  71. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-agents.md +0 -0
  72. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-aider.md +0 -0
  73. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-amp.md +0 -0
  74. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-claw.md +0 -0
  75. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-codex.md +0 -0
  76. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-copilot.md +0 -0
  77. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-devin.md +0 -0
  78. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-droid.md +0 -0
  79. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-kilo.md +0 -0
  80. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-kiro.md +0 -0
  81. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-opencode.md +0 -0
  82. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-pi.md +0 -0
  83. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-trae.md +0 -0
  84. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-vscode.md +0 -0
  85. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill-windows.md +0 -0
  86. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skill.md +0 -0
  87. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/add-watch.md +0 -0
  88. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/exports.md +0 -0
  89. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/extraction-spec.md +0 -0
  90. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/github-and-merge.md +0 -0
  91. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/hooks.md +0 -0
  92. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/query.md +0 -0
  93. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/transcribe.md +0 -0
  94. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/agents/references/update.md +0 -0
  95. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/add-watch.md +0 -0
  96. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/exports.md +0 -0
  97. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/extraction-spec.md +0 -0
  98. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/github-and-merge.md +0 -0
  99. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/hooks.md +0 -0
  100. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/query.md +0 -0
  101. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/transcribe.md +0 -0
  102. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/amp/references/update.md +0 -0
  103. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/add-watch.md +0 -0
  104. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/exports.md +0 -0
  105. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/extraction-spec.md +0 -0
  106. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/github-and-merge.md +0 -0
  107. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/hooks.md +0 -0
  108. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/query.md +0 -0
  109. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/transcribe.md +0 -0
  110. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claude/references/update.md +0 -0
  111. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/add-watch.md +0 -0
  112. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/exports.md +0 -0
  113. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/extraction-spec.md +0 -0
  114. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/github-and-merge.md +0 -0
  115. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/hooks.md +0 -0
  116. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/query.md +0 -0
  117. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/transcribe.md +0 -0
  118. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/claw/references/update.md +0 -0
  119. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/add-watch.md +0 -0
  120. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/exports.md +0 -0
  121. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/extraction-spec.md +0 -0
  122. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/github-and-merge.md +0 -0
  123. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/hooks.md +0 -0
  124. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/query.md +0 -0
  125. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/transcribe.md +0 -0
  126. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/codex/references/update.md +0 -0
  127. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/add-watch.md +0 -0
  128. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/exports.md +0 -0
  129. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/extraction-spec.md +0 -0
  130. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/github-and-merge.md +0 -0
  131. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/hooks.md +0 -0
  132. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/query.md +0 -0
  133. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/transcribe.md +0 -0
  134. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/copilot/references/update.md +0 -0
  135. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/add-watch.md +0 -0
  136. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/exports.md +0 -0
  137. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/extraction-spec.md +0 -0
  138. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/github-and-merge.md +0 -0
  139. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/hooks.md +0 -0
  140. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/query.md +0 -0
  141. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/transcribe.md +0 -0
  142. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/droid/references/update.md +0 -0
  143. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/add-watch.md +0 -0
  144. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/exports.md +0 -0
  145. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/extraction-spec.md +0 -0
  146. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/github-and-merge.md +0 -0
  147. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/hooks.md +0 -0
  148. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/query.md +0 -0
  149. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/transcribe.md +0 -0
  150. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kilo/references/update.md +0 -0
  151. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/add-watch.md +0 -0
  152. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/exports.md +0 -0
  153. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/extraction-spec.md +0 -0
  154. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/github-and-merge.md +0 -0
  155. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/hooks.md +0 -0
  156. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/query.md +0 -0
  157. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/transcribe.md +0 -0
  158. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/kiro/references/update.md +0 -0
  159. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/add-watch.md +0 -0
  160. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/exports.md +0 -0
  161. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/extraction-spec.md +0 -0
  162. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/github-and-merge.md +0 -0
  163. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/hooks.md +0 -0
  164. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/query.md +0 -0
  165. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/transcribe.md +0 -0
  166. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/opencode/references/update.md +0 -0
  167. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/add-watch.md +0 -0
  168. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/exports.md +0 -0
  169. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/extraction-spec.md +0 -0
  170. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/github-and-merge.md +0 -0
  171. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/hooks.md +0 -0
  172. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/query.md +0 -0
  173. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/transcribe.md +0 -0
  174. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/pi/references/update.md +0 -0
  175. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/add-watch.md +0 -0
  176. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/exports.md +0 -0
  177. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/extraction-spec.md +0 -0
  178. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/github-and-merge.md +0 -0
  179. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/hooks.md +0 -0
  180. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/query.md +0 -0
  181. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/transcribe.md +0 -0
  182. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/trae/references/update.md +0 -0
  183. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/add-watch.md +0 -0
  184. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/exports.md +0 -0
  185. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/extraction-spec.md +0 -0
  186. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/github-and-merge.md +0 -0
  187. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/hooks.md +0 -0
  188. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/query.md +0 -0
  189. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/transcribe.md +0 -0
  190. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/vscode/references/update.md +0 -0
  191. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/add-watch.md +0 -0
  192. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/exports.md +0 -0
  193. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/extraction-spec.md +0 -0
  194. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/github-and-merge.md +0 -0
  195. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/hooks.md +0 -0
  196. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/query.md +0 -0
  197. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/transcribe.md +0 -0
  198. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/skills/windows/references/update.md +0 -0
  199. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/symbol_resolution.py +0 -0
  200. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/transcribe.py +0 -0
  201. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/tree_html.py +0 -0
  202. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/validate.py +0 -0
  203. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphify/wiki.py +0 -0
  204. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphifyy.egg-info/dependency_links.txt +0 -0
  205. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphifyy.egg-info/entry_points.txt +0 -0
  206. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphifyy.egg-info/requires.txt +0 -0
  207. {graphifyy-0.9.3 → graphifyy-0.9.4}/graphifyy.egg-info/top_level.txt +0 -0
  208. {graphifyy-0.9.3 → graphifyy-0.9.4}/setup.cfg +0 -0
  209. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_affected_cli.py +0 -0
  210. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_agents_platform.py +0 -0
  211. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_analyze.py +0 -0
  212. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_anthropic_custom_endpoint.py +0 -0
  213. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_antigravity_install.py +0 -0
  214. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_astro_extraction.py +0 -0
  215. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_backend_extras.py +0 -0
  216. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_benchmark.py +0 -0
  217. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_build.py +0 -0
  218. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_cache.py +0 -0
  219. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_callflow_html.py +0 -0
  220. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_cargo_introspect.py +0 -0
  221. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_charmap_encoding.py +0 -0
  222. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_chunking.py +0 -0
  223. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_claude_cli_backend.py +0 -0
  224. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_claude_md.py +0 -0
  225. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_cli_export.py +0 -0
  226. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_cluster.py +0 -0
  227. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_codebuddy.py +0 -0
  228. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_confidence.py +0 -0
  229. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_cpp_objc_cross_file_calls.py +0 -0
  230. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_cpp_preprocess.py +0 -0
  231. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_csharp_type_resolution.py +0 -0
  232. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_dart.py +0 -0
  233. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_detect.py +0 -0
  234. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_devin.py +0 -0
  235. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_dotnet.py +0 -0
  236. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_explain_cli.py +0 -0
  237. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_export.py +0 -0
  238. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_extract.py +0 -0
  239. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_extract_cli.py +0 -0
  240. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_extraction_spec_ids.py +0 -0
  241. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_falkordb_integration.py +0 -0
  242. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_file_node_id_spec.py +0 -0
  243. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_file_slice.py +0 -0
  244. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_global_graph.py +0 -0
  245. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_google_workspace.py +0 -0
  246. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_hypergraph.py +0 -0
  247. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_id_normalization_contract.py +0 -0
  248. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_image_vision.py +0 -0
  249. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_import_extension_resolution.py +0 -0
  250. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_incremental.py +0 -0
  251. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_ingest.py +0 -0
  252. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_install.py +0 -0
  253. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_install_references.py +0 -0
  254. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_install_roundtrip.py +0 -0
  255. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_install_strings.py +0 -0
  256. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_install_upgrade.py +0 -0
  257. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_java_type_resolution.py +0 -0
  258. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_js_import_resolution.py +0 -0
  259. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_label_retry.py +0 -0
  260. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_labeling.py +0 -0
  261. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_language_resolvers.py +0 -0
  262. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_llm_backends.py +0 -0
  263. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_llm_parser.py +0 -0
  264. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_manifest_ingest.py +0 -0
  265. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_mcp_ingest.py +0 -0
  266. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_minhash.py +0 -0
  267. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_multigraph_compat.py +0 -0
  268. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_multigraph_diagnostics.py +0 -0
  269. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_multilang.py +0 -0
  270. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_obsidian_dangling_member.py +0 -0
  271. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_obsidian_filename_cap.py +0 -0
  272. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_office_limits.py +0 -0
  273. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_ollama.py +0 -0
  274. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_openai_custom_endpoint.py +0 -0
  275. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_pascal.py +0 -0
  276. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_path_cli.py +0 -0
  277. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_paths.py +0 -0
  278. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_pg_introspect.py +0 -0
  279. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_pipeline.py +0 -0
  280. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_provider_registry.py +0 -0
  281. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_prs.py +0 -0
  282. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_python_import_resolution.py +0 -0
  283. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_query_cli.py +0 -0
  284. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_querylog.py +0 -0
  285. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_rationale.py +0 -0
  286. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_read_hook.py +0 -0
  287. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_reflect.py +0 -0
  288. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_report.py +0 -0
  289. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_ruby_resolution.py +0 -0
  290. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_scip_ingest.py +0 -0
  291. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_security.py +0 -0
  292. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_semantic_cleanup.py +0 -0
  293. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_semantic_similarity.py +0 -0
  294. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_serve.py +0 -0
  295. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_serve_http.py +0 -0
  296. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_skillgen.py +0 -0
  297. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_swift_cross_file_calls.py +0 -0
  298. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_swift_import_resolution.py +0 -0
  299. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_symbol_resolution.py +0 -0
  300. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_terraform.py +0 -0
  301. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_transcribe.py +0 -0
  302. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_ts_inheritance.py +0 -0
  303. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_validate.py +0 -0
  304. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_vue_extraction.py +0 -0
  305. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_watch.py +0 -0
  306. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_wheel_packaging.py +0 -0
  307. {graphifyy-0.9.3 → graphifyy-0.9.4}/tests/test_wiki.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphifyy
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: AI coding assistant skill (Claude Code, CodeBuddy, Codex, OpenCode, Kilo Code, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph
5
5
  License: MIT License
6
6
 
@@ -148,7 +148,41 @@ def _check_skill_version(skill_dst: Path) -> None:
148
148
  except OSError:
149
149
  return
150
150
  if installed != __version__:
151
- print(f" warning: skill is from graphify {installed}, package is {__version__}. Run 'graphify install' to update.", file=sys.stderr)
151
+ if _version_tuple(installed) > _version_tuple(__version__):
152
+ # The skill on disk is NEWER than the running package. `graphify install`
153
+ # writes the package's OWN (older) bundled skill and re-stamps the version,
154
+ # so following the old "run install" advice would silently DOWNGRADE the
155
+ # skill. The real fix is to upgrade the package (#1568). Common for a stale
156
+ # `uv tool` CLI, or a contributor whose dev checkout stamped a newer skill.
157
+ print(
158
+ f" warning: skill is from graphify {installed}, but the package is "
159
+ f"{__version__} (older). Upgrade the package "
160
+ f"(e.g. 'uv tool upgrade graphifyy' or 'pip install -U graphifyy'); "
161
+ f"running 'graphify install' would downgrade the skill.",
162
+ file=sys.stderr,
163
+ )
164
+ else:
165
+ print(f" warning: skill is from graphify {installed}, package is {__version__}. Run 'graphify install' to update.", file=sys.stderr)
166
+
167
+
168
+ def _version_tuple(version: str) -> tuple[int, ...]:
169
+ """Parse a version string into a comparable integer tuple (``0.9.2`` -> ``(0, 9, 2)``).
170
+
171
+ Reads the leading digits of each dot-segment, so pre/post-release suffixes
172
+ (``1.0.0rc1``) compare by their numeric core. A non-numeric or empty segment
173
+ becomes 0, so a malformed stamp degrades to a conservative comparison rather
174
+ than raising.
175
+ """
176
+ parts: list[int] = []
177
+ for segment in str(version).split("."):
178
+ digits = ""
179
+ for ch in segment:
180
+ if ch.isdigit():
181
+ digits += ch
182
+ else:
183
+ break
184
+ parts.append(int(digits) if digits else 0)
185
+ return tuple(parts)
152
186
 
153
187
 
154
188
  def _refresh_all_version_stamps() -> None:
@@ -3525,17 +3559,20 @@ def main() -> None:
3525
3559
  else:
3526
3560
  # No labels file yet (or `graphify label` forced a refresh). When run
3527
3561
  # standalone there is no orchestrating agent to do skill.md Step 5, so
3528
- # auto-name communities with the configured backend rather than leave
3529
- # "Community N" (#1097). Degrades to placeholders if no backend/on error.
3562
+ # auto-name communities rather than leave "Community N" (#1097).
3563
+ from graphify.cluster import label_communities_by_hub
3530
3564
  from graphify.llm import generate_community_labels
3531
3565
  print("Labeling communities...")
3532
- # The final labels (LLM or placeholder fallback) are persisted to
3533
- # .graphify_labels.json by the unconditional write below.
3566
+ # Deterministic, LLM-free base labels: name each community after its
3567
+ # highest-degree hub, so the report is readable even with no backend
3568
+ # (previously bare "Community N"). A configured LLM backend overrides these
3569
+ # with richer names below; its no-backend placeholder fallback does NOT.
3570
+ hub_labels = label_communities_by_hub(G, communities)
3534
3571
  label_communities_input = communities
3535
- labels = {}
3572
+ labels = dict(hub_labels)
3536
3573
  if missing_only:
3537
3574
  labels = {
3538
- cid: existing_labels.get(cid, f"Community {cid}")
3575
+ cid: existing_labels.get(cid, hub_labels[cid])
3539
3576
  for cid in communities
3540
3577
  }
3541
3578
  label_communities_input = {
@@ -3547,7 +3584,13 @@ def main() -> None:
3547
3584
  G, label_communities_input, backend=label_backend, model=label_model, gods=gods,
3548
3585
  max_concurrency=label_max_concurrency, batch_size=label_batch_size,
3549
3586
  )
3550
- labels.update(generated_labels)
3587
+ # Only let the LLM OVERRIDE where it produced a real name — its no-backend
3588
+ # fallback returns "Community {cid}" placeholders, which must not clobber
3589
+ # the deterministic hub labels.
3590
+ labels.update({
3591
+ cid: v for cid, v in generated_labels.items()
3592
+ if v and v != f"Community {cid}"
3593
+ })
3551
3594
  stages.mark("label")
3552
3595
  questions = suggest_questions(G, communities, labels)
3553
3596
  tokens = {"input": 0, "output": 0}
@@ -11,6 +11,7 @@ import networkx as nx
11
11
 
12
12
  DEFAULT_AFFECTED_RELATIONS = (
13
13
  "calls",
14
+ "indirect_call",
14
15
  "references",
15
16
  "imports",
16
17
  "imports_from",
@@ -209,7 +210,13 @@ def load_graph(path: Path) -> nx.Graph:
209
210
  import json
210
211
  from networkx.readwrite import json_graph
211
212
 
212
- raw = json.loads(path.read_text(encoding="utf-8"))
213
+ try:
214
+ raw = json.loads(path.read_text(encoding="utf-8"))
215
+ except (json.JSONDecodeError, OSError) as exc:
216
+ raise RuntimeError(
217
+ f"Cannot read graph file {path}: {exc}. "
218
+ "Re-run 'graphify extract' to regenerate it."
219
+ ) from exc
213
220
  # Force directed so stored caller→callee direction survives the round-trip;
214
221
  # mirrors serve.py and __main__.py (#1174).
215
222
  raw = {**raw, "directed": True}
@@ -117,10 +117,43 @@ def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
117
117
  try:
118
118
  p = Path(p).relative_to(root).as_posix()
119
119
  except ValueError:
120
- pass
120
+ # Lexical relative_to failed. Retry with both sides fully resolved:
121
+ # a symlinked scan root (macOS /var -> /private/var, or a symlinked
122
+ # home/worktree) makes the raw prefixes differ even though they point
123
+ # at the same dir, which otherwise silently defeats prune/replace
124
+ # matching. Only the slow path resolves, so the common lexical match
125
+ # stays filesystem-free.
126
+ try:
127
+ p = Path(p).resolve().relative_to(Path(root).resolve()).as_posix()
128
+ except (ValueError, OSError):
129
+ pass
121
130
  return p
122
131
 
123
132
 
133
+ def _infer_merge_root(graph_path: Path) -> str | None:
134
+ """Best-effort scan root for relativizing paths in build_merge when the caller
135
+ passes no ``root`` (#1571).
136
+
137
+ Prefers the committed ``graphify-out/.graphify_root`` marker — the authoritative
138
+ scan root graphify records at build/watch time (#686/#1423) — then falls back to
139
+ the directory that contains the output dir (``graph.json``'s grandparent, i.e.
140
+ ``<root>/graphify-out/graph.json`` -> ``<root>``). Returns None if neither
141
+ resolves, in which case normalization is a no-op (prior behavior).
142
+ """
143
+ try:
144
+ marker = graph_path.parent / ".graphify_root"
145
+ if marker.exists():
146
+ recorded = marker.read_text(encoding="utf-8").strip()
147
+ if recorded:
148
+ return str(Path(recorded).resolve())
149
+ except OSError:
150
+ pass
151
+ try:
152
+ return str(graph_path.parent.parent.resolve())
153
+ except Exception:
154
+ return None
155
+
156
+
124
157
  def edge_data(G: nx.Graph, u: str, v: str) -> dict:
125
158
  """Return one edge attribute dict for (u, v), tolerating MultiGraph.
126
159
 
@@ -642,7 +675,15 @@ def deduplicate_by_label(nodes: list[dict], edges: list[dict]) -> tuple[list[dic
642
675
  """Merge nodes that share a normalised label, rewriting edge references.
643
676
 
644
677
  Prefers IDs without chunk suffixes (_c\\d+) and shorter IDs when tied.
645
- Drops self-loops created by the merge. Called in build() automatically.
678
+ Drops self-loops created by the merge.
679
+
680
+ Dormant: this is NOT wired into ``build()`` — the active dedup path is
681
+ ``deduplicate_entities`` (imported and called in ``build``), which supersedes
682
+ it. The previous "Called in build() automatically" note was never true. It
683
+ also merges by label alone with no ``file_type`` guard, so it must not be
684
+ enabled for code nodes: same-label symbols from different files/packages
685
+ (e.g. two ``Account`` types) would collapse into one — the cross-file
686
+ conflation ``deduplicate_entities`` deliberately avoids for code (#1205).
646
687
  """
647
688
  _CHUNK_SUFFIX = re.compile(r"_c\d+$")
648
689
  canonical: dict[str, dict] = {} # norm_label -> surviving node
@@ -714,16 +755,36 @@ def build_merge(
714
755
  # NetworkX round-trip loses direction permanently (#760).
715
756
  from graphify.security import check_graph_file_size_cap
716
757
  check_graph_file_size_cap(graph_path)
717
- data = json.loads(graph_path.read_text(encoding="utf-8"))
758
+ try:
759
+ data = json.loads(graph_path.read_text(encoding="utf-8"))
760
+ except (json.JSONDecodeError, OSError) as exc:
761
+ raise RuntimeError(
762
+ f"Cannot read {graph_path} for incremental merge: {exc}. "
763
+ "Delete the file and run a full rebuild."
764
+ ) from exc
718
765
  links_key = "links" if "links" in data else "edges"
719
766
  existing_nodes = list(data.get("nodes", []))
720
767
  existing_edges = list(data.get(links_key, []))
768
+ existing_hyperedges = list(data.get("hyperedges", []))
721
769
  had_graph = True
722
770
  else:
723
771
  existing_nodes = []
724
772
  existing_edges = []
773
+ existing_hyperedges = []
725
774
  had_graph = False
726
775
 
776
+ # Effective root for relativizing absolute source_file / prune paths back to the
777
+ # stored relative source_file keys. When the caller passes root we use it;
778
+ # otherwise fall back to the graph's recorded scan root, so absolute
779
+ # prune_sources and new-chunk paths still match even when a caller omits root
780
+ # (#1571 — the skill's --update runbook calls build_merge without root, so
781
+ # absolute deleted-file paths never matched the relative node keys and their
782
+ # nodes survived as ghosts).
783
+ _eff_root = (
784
+ str(Path(root).resolve()) if root is not None
785
+ else _infer_merge_root(graph_path)
786
+ )
787
+
727
788
  # Re-extracted files REPLACE their prior contribution. Every source_file
728
789
  # present in new_chunks is dropped from the loaded base before merging, so a
729
790
  # CHANGED file's stale nodes/edges don't accumulate across incremental
@@ -733,7 +794,7 @@ def build_merge(
733
794
  # for them; genuinely deleted files are still handled via prune_sources.
734
795
  # Matched in both raw and _norm_source_file form because new_chunks may carry
735
796
  # absolute win32 paths while the stored graph keeps relative posix (#1007).
736
- _replace_root = str(Path(root).resolve()) if root is not None else None
797
+ _replace_root = _eff_root
737
798
  new_sources: set[str] = set()
738
799
  for ch in new_chunks:
739
800
  for n in ch.get("nodes", []):
@@ -756,23 +817,46 @@ def build_merge(
756
817
  all_chunks = base + list(new_chunks)
757
818
  G = build(all_chunks, directed=directed, dedup=dedup, dedup_llm_backend=dedup_llm_backend, root=root)
758
819
 
820
+ # Prune set for deleted source files — both the raw form (matches nodes that
821
+ # kept absolute source_file) and the normalised relative form (matches nodes
822
+ # relativised by _norm_source_file at build time). .resolve() (via _eff_root)
823
+ # handles symlinked roots and ".." / "./" segments so Path.relative_to()
824
+ # succeeds even when the scan root is a symlink. (#1007, #1571)
825
+ prune_set: set[str] = set()
826
+ for p in (prune_sources or []):
827
+ if not p:
828
+ continue
829
+ prune_set.add(p)
830
+ norm = _norm_source_file(p, _eff_root)
831
+ if norm:
832
+ prune_set.add(norm)
833
+
834
+ # Carry forward hyperedges from files that were neither re-extracted nor
835
+ # deleted (#1574). build() only sees the new chunks' hyperedges, so without
836
+ # this every --update collapses the graph's hyperedge set down to just the
837
+ # changed files'. Re-extracted files' prior hyperedges are dropped (their new
838
+ # version is already in G — replace-per-source, like nodes/edges); deleted
839
+ # files' are dropped via prune_set. id-dedup (attach_hyperedges) so a carried
840
+ # hyperedge never duplicates one the new chunks re-emitted. Mirrors watch.py,
841
+ # which already preserves existing hyperedges across a rebuild.
842
+ if existing_hyperedges:
843
+ carried = []
844
+ for he in existing_hyperedges:
845
+ if not isinstance(he, dict):
846
+ continue
847
+ sf = he.get("source_file")
848
+ norm = _norm_source_file(sf, _eff_root)
849
+ if sf in new_sources or norm in new_sources:
850
+ continue # re-extracted — replaced by the new chunk's version
851
+ if sf in prune_set or norm in prune_set:
852
+ continue # deleted — pruned
853
+ carried.append(he)
854
+ if carried:
855
+ from graphify.export import attach_hyperedges
856
+ attach_hyperedges(G, carried)
857
+
759
858
  # Prune nodes and edges from deleted source files
760
859
  if prune_sources:
761
- # Build a set containing both the raw form (matches nodes that kept
762
- # absolute source_file) and the normalised relative form (matches nodes
763
- # that were relativised by _norm_source_file at build time).
764
- # .resolve() handles symlinked roots and redundant ".." / "./" segments
765
- # so Path.relative_to() succeeds even when the scan root is a symlink.
766
- # (#1007: manifest absolute paths vs graph relative source_file mismatch)
767
- _root_str = str(Path(root).resolve()) if root is not None else None
768
- prune_set: set[str] = set()
769
- for p in prune_sources:
770
- if not p:
771
- continue
772
- prune_set.add(p)
773
- norm = _norm_source_file(p, _root_str)
774
- if norm:
775
- prune_set.add(norm)
776
860
  to_remove = [
777
861
  n for n, d in G.nodes(data=True)
778
862
  if d.get("source_file") in prune_set
@@ -83,6 +83,33 @@ _COHESION_SPLIT_THRESHOLD = 0.05 # re-split communities with cohesion below this
83
83
  _COHESION_SPLIT_MIN_SIZE = 50 # only cohesion-split if community has at least this many nodes
84
84
 
85
85
 
86
+ def label_communities_by_hub(
87
+ G: nx.Graph, communities: dict[int, list[str]]
88
+ ) -> dict[int, str]:
89
+ """Deterministic, LLM-free community labels: name each community after its
90
+ highest-degree member — the structural hub — so a report reads ``auth`` /
91
+ ``log_action`` instead of ``Community 70``. Degree is measured on the full graph
92
+ ``G``; ties break by node id for run-to-run stability. A community whose members
93
+ are all absent from ``G`` falls back to ``Community {cid}``.
94
+
95
+ Used as the default (no-backend) labeler; an LLM naming pass, when configured,
96
+ overrides these with richer names.
97
+ """
98
+ labels: dict[int, str] = {}
99
+ for cid, members in communities.items():
100
+ present = [n for n in members if n in G]
101
+ if not present:
102
+ labels[cid] = f"Community {cid}"
103
+ continue
104
+ # highest degree wins; ties broken by node id (ascending) for determinism
105
+ hub = min(present, key=lambda n: (-G.degree(n), str(n)))
106
+ name = str(G.nodes[hub].get("label") or hub).strip()
107
+ if name.endswith("()"):
108
+ name = name[:-2]
109
+ labels[cid] = name or f"Community {cid}"
110
+ return labels
111
+
112
+
86
113
  def cluster(
87
114
  G: nx.Graph,
88
115
  resolution: float = 1.0,
@@ -6,6 +6,7 @@ Jaro-Winkler verification → same-community boost → union-find merge.
6
6
  from __future__ import annotations
7
7
  import math
8
8
  import re
9
+ import sys
9
10
  import unicodedata
10
11
  from collections import defaultdict
11
12
 
@@ -219,12 +220,29 @@ def deduplicate_entities(
219
220
  if len(nodes) <= 1:
220
221
  return nodes, edges
221
222
 
222
- # Pre-deduplicate: keep first occurrence of each id
223
+ # Pre-deduplicate: keep first occurrence of each id.
224
+ # Warn when two nodes share an ID but originate from different source files —
225
+ # this indicates a cross-chunk ID collision (#1504) where silent data loss occurs.
223
226
  seen_ids: dict[str, dict] = {}
224
227
  for node in nodes:
225
228
  nid = node.get("id", "")
226
- if nid and nid not in seen_ids:
229
+ if not nid:
230
+ continue
231
+ if nid not in seen_ids:
227
232
  seen_ids[nid] = node
233
+ else:
234
+ existing_sf = seen_ids[nid].get("source_file") or ""
235
+ new_sf = node.get("source_file") or ""
236
+ if existing_sf != new_sf:
237
+ print(
238
+ f"[graphify] WARNING: node '{nid}' from '{new_sf}' collides with "
239
+ f"node from '{existing_sf}' — the second node will be dropped. "
240
+ f"This is a cross-chunk ID collision caused by two files with the "
241
+ f"same name in different directories. To avoid data loss, run "
242
+ f"'graphify extract' per subfolder and merge with "
243
+ f"'graphify merge-graphs'.",
244
+ file=sys.stderr,
245
+ )
228
246
  unique_nodes = list(seen_ids.values())
229
247
 
230
248
  if len(unique_nodes) <= 1:
@@ -274,7 +274,13 @@ def _read_json_file(path: str | Path) -> dict[str, Any]:
274
274
 
275
275
  json_path = Path(path)
276
276
  check_graph_file_size_cap(json_path)
277
- data = json.loads(json_path.read_text(encoding="utf-8"))
277
+ try:
278
+ data = json.loads(json_path.read_text(encoding="utf-8"))
279
+ except (json.JSONDecodeError, OSError) as exc:
280
+ raise RuntimeError(
281
+ f"Cannot parse {json_path}: {exc}. "
282
+ "The file may be corrupted — re-run 'graphify extract'."
283
+ ) from exc
278
284
  if not isinstance(data, dict):
279
285
  raise ValueError("diagnostic input must be a JSON object")
280
286
  return data