graphifyy 0.9.2__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 (310) hide show
  1. {graphifyy-0.9.2/graphifyy.egg-info → graphifyy-0.9.4}/PKG-INFO +4 -2
  2. {graphifyy-0.9.2 → graphifyy-0.9.4}/README.md +3 -1
  3. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/__main__.py +78 -9
  4. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/affected.py +8 -1
  5. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/build.py +170 -21
  6. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/cluster.py +27 -0
  7. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/dedup.py +20 -2
  8. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/diagnostics.py +7 -1
  9. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/export.py +50 -3
  10. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extract.py +1992 -246
  11. graphifyy-0.9.4/graphify/extractors/csharp.py +393 -0
  12. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/hooks.py +17 -0
  13. graphifyy-0.9.4/graphify/paths.py +234 -0
  14. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/reflect.py +307 -2
  15. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/report.py +64 -0
  16. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/semantic_cleanup.py +10 -0
  17. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/serve.py +24 -3
  18. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/symbol_resolution.py +22 -2
  19. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/watch.py +8 -4
  20. {graphifyy-0.9.2 → graphifyy-0.9.4/graphifyy.egg-info}/PKG-INFO +4 -2
  21. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/SOURCES.txt +9 -0
  22. {graphifyy-0.9.2 → graphifyy-0.9.4}/pyproject.toml +1 -1
  23. graphifyy-0.9.4/tests/test_build_merge_hyperedges_and_prune.py +151 -0
  24. graphifyy-0.9.4/tests/test_community_hub_labels.py +66 -0
  25. graphifyy-0.9.4/tests/test_corrupt_graph_json.py +53 -0
  26. graphifyy-0.9.4/tests/test_cpp_objc_cross_file_calls.py +263 -0
  27. graphifyy-0.9.4/tests/test_csharp_type_resolution.py +569 -0
  28. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_dedup.py +40 -0
  29. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_explain_cli.py +43 -0
  30. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_export.py +69 -0
  31. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_extract.py +85 -0
  32. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_hooks.py +12 -2
  33. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_hypergraph.py +93 -0
  34. graphifyy-0.9.4/tests/test_indirect_dispatch.py +479 -0
  35. graphifyy-0.9.4/tests/test_indirect_dispatch_assign_return.py +133 -0
  36. graphifyy-0.9.4/tests/test_indirect_dispatch_getattr.py +153 -0
  37. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_js_import_resolution.py +251 -0
  38. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_languages.py +488 -0
  39. graphifyy-0.9.4/tests/test_paths.py +99 -0
  40. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_reflect.py +246 -0
  41. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_report.py +44 -0
  42. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_semantic_cleanup.py +33 -0
  43. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_serve.py +48 -0
  44. graphifyy-0.9.4/tests/test_skill_version_warning.py +60 -0
  45. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_symbol_resolution.py +90 -4
  46. graphifyy-0.9.2/graphify/extractors/csharp.py +0 -158
  47. graphifyy-0.9.2/graphify/paths.py +0 -46
  48. graphifyy-0.9.2/tests/test_csharp_type_resolution.py +0 -172
  49. {graphifyy-0.9.2 → graphifyy-0.9.4}/LICENSE +0 -0
  50. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/__init__.py +0 -0
  51. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/_minhash.py +0 -0
  52. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/agents-md.md +0 -0
  53. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/antigravity-rules.md +0 -0
  54. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/claude-md.md +0 -0
  55. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/gemini-md.md +0 -0
  56. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/kiro-steering.md +0 -0
  57. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/always_on/vscode-instructions.md +0 -0
  58. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/analyze.py +0 -0
  59. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/benchmark.py +0 -0
  60. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/cache.py +0 -0
  61. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/callflow_html.py +0 -0
  62. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/cargo_introspect.py +0 -0
  63. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/command-kilo.md +0 -0
  64. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/detect.py +0 -0
  65. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/__init__.py +0 -0
  66. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/base.py +0 -0
  67. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/blade.py +0 -0
  68. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/elixir.py +0 -0
  69. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/razor.py +0 -0
  70. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/extractors/zig.py +0 -0
  71. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/file_slice.py +0 -0
  72. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/global_graph.py +0 -0
  73. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/google_workspace.py +0 -0
  74. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/ids.py +0 -0
  75. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/ingest.py +0 -0
  76. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/llm.py +0 -0
  77. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/manifest.py +0 -0
  78. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/manifest_ingest.py +0 -0
  79. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/mcp_ingest.py +0 -0
  80. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/multigraph_compat.py +0 -0
  81. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/pg_introspect.py +0 -0
  82. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/prs.py +0 -0
  83. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/querylog.py +0 -0
  84. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/resolver_registry.py +0 -0
  85. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/ruby_resolution.py +0 -0
  86. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/scip_ingest.py +0 -0
  87. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/security.py +0 -0
  88. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-agents.md +0 -0
  89. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-aider.md +0 -0
  90. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-amp.md +0 -0
  91. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-claw.md +0 -0
  92. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-codex.md +0 -0
  93. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-copilot.md +0 -0
  94. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-devin.md +0 -0
  95. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-droid.md +0 -0
  96. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-kilo.md +0 -0
  97. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-kiro.md +0 -0
  98. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-opencode.md +0 -0
  99. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-pi.md +0 -0
  100. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-trae.md +0 -0
  101. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-vscode.md +0 -0
  102. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill-windows.md +0 -0
  103. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skill.md +0 -0
  104. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/add-watch.md +0 -0
  105. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/exports.md +0 -0
  106. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/extraction-spec.md +0 -0
  107. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/github-and-merge.md +0 -0
  108. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/hooks.md +0 -0
  109. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/query.md +0 -0
  110. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/transcribe.md +0 -0
  111. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/agents/references/update.md +0 -0
  112. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/add-watch.md +0 -0
  113. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/exports.md +0 -0
  114. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/extraction-spec.md +0 -0
  115. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/github-and-merge.md +0 -0
  116. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/hooks.md +0 -0
  117. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/query.md +0 -0
  118. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/transcribe.md +0 -0
  119. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/amp/references/update.md +0 -0
  120. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/add-watch.md +0 -0
  121. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/exports.md +0 -0
  122. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/extraction-spec.md +0 -0
  123. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/github-and-merge.md +0 -0
  124. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/hooks.md +0 -0
  125. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/query.md +0 -0
  126. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/transcribe.md +0 -0
  127. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claude/references/update.md +0 -0
  128. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/add-watch.md +0 -0
  129. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/exports.md +0 -0
  130. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/extraction-spec.md +0 -0
  131. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/github-and-merge.md +0 -0
  132. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/hooks.md +0 -0
  133. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/query.md +0 -0
  134. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/transcribe.md +0 -0
  135. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/claw/references/update.md +0 -0
  136. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/add-watch.md +0 -0
  137. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/exports.md +0 -0
  138. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/extraction-spec.md +0 -0
  139. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/github-and-merge.md +0 -0
  140. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/hooks.md +0 -0
  141. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/query.md +0 -0
  142. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/transcribe.md +0 -0
  143. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/codex/references/update.md +0 -0
  144. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/add-watch.md +0 -0
  145. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/exports.md +0 -0
  146. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/extraction-spec.md +0 -0
  147. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/github-and-merge.md +0 -0
  148. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/hooks.md +0 -0
  149. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/query.md +0 -0
  150. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/transcribe.md +0 -0
  151. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/copilot/references/update.md +0 -0
  152. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/add-watch.md +0 -0
  153. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/exports.md +0 -0
  154. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/extraction-spec.md +0 -0
  155. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/github-and-merge.md +0 -0
  156. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/hooks.md +0 -0
  157. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/query.md +0 -0
  158. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/transcribe.md +0 -0
  159. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/droid/references/update.md +0 -0
  160. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/add-watch.md +0 -0
  161. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/exports.md +0 -0
  162. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/extraction-spec.md +0 -0
  163. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/github-and-merge.md +0 -0
  164. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/hooks.md +0 -0
  165. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/query.md +0 -0
  166. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/transcribe.md +0 -0
  167. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kilo/references/update.md +0 -0
  168. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/add-watch.md +0 -0
  169. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/exports.md +0 -0
  170. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/extraction-spec.md +0 -0
  171. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/github-and-merge.md +0 -0
  172. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/hooks.md +0 -0
  173. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/query.md +0 -0
  174. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/transcribe.md +0 -0
  175. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/kiro/references/update.md +0 -0
  176. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/add-watch.md +0 -0
  177. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/exports.md +0 -0
  178. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/extraction-spec.md +0 -0
  179. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/github-and-merge.md +0 -0
  180. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/hooks.md +0 -0
  181. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/query.md +0 -0
  182. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/transcribe.md +0 -0
  183. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/opencode/references/update.md +0 -0
  184. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/add-watch.md +0 -0
  185. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/exports.md +0 -0
  186. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/extraction-spec.md +0 -0
  187. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/github-and-merge.md +0 -0
  188. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/hooks.md +0 -0
  189. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/query.md +0 -0
  190. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/transcribe.md +0 -0
  191. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/pi/references/update.md +0 -0
  192. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/add-watch.md +0 -0
  193. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/exports.md +0 -0
  194. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/extraction-spec.md +0 -0
  195. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/github-and-merge.md +0 -0
  196. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/hooks.md +0 -0
  197. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/query.md +0 -0
  198. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/transcribe.md +0 -0
  199. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/trae/references/update.md +0 -0
  200. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/add-watch.md +0 -0
  201. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/exports.md +0 -0
  202. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/extraction-spec.md +0 -0
  203. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/github-and-merge.md +0 -0
  204. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/hooks.md +0 -0
  205. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/query.md +0 -0
  206. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/transcribe.md +0 -0
  207. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/vscode/references/update.md +0 -0
  208. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/add-watch.md +0 -0
  209. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/exports.md +0 -0
  210. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/extraction-spec.md +0 -0
  211. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/github-and-merge.md +0 -0
  212. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/hooks.md +0 -0
  213. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/query.md +0 -0
  214. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/transcribe.md +0 -0
  215. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/skills/windows/references/update.md +0 -0
  216. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/transcribe.py +0 -0
  217. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/tree_html.py +0 -0
  218. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/validate.py +0 -0
  219. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphify/wiki.py +0 -0
  220. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/dependency_links.txt +0 -0
  221. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/entry_points.txt +0 -0
  222. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/requires.txt +0 -0
  223. {graphifyy-0.9.2 → graphifyy-0.9.4}/graphifyy.egg-info/top_level.txt +0 -0
  224. {graphifyy-0.9.2 → graphifyy-0.9.4}/setup.cfg +0 -0
  225. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_affected_cli.py +0 -0
  226. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_agents_platform.py +0 -0
  227. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_analyze.py +0 -0
  228. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_anthropic_custom_endpoint.py +0 -0
  229. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_antigravity_install.py +0 -0
  230. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_astro_extraction.py +0 -0
  231. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_backend_extras.py +0 -0
  232. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_benchmark.py +0 -0
  233. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_build.py +0 -0
  234. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cache.py +0 -0
  235. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_callflow_html.py +0 -0
  236. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cargo_introspect.py +0 -0
  237. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_charmap_encoding.py +0 -0
  238. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_chunking.py +0 -0
  239. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_claude_cli_backend.py +0 -0
  240. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_claude_md.py +0 -0
  241. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cli_export.py +0 -0
  242. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cluster.py +0 -0
  243. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_codebuddy.py +0 -0
  244. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_confidence.py +0 -0
  245. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_cpp_preprocess.py +0 -0
  246. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_dart.py +0 -0
  247. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_detect.py +0 -0
  248. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_devin.py +0 -0
  249. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_dotnet.py +0 -0
  250. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_extract_cli.py +0 -0
  251. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_extraction_spec_ids.py +0 -0
  252. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_falkordb_integration.py +0 -0
  253. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_file_node_id_spec.py +0 -0
  254. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_file_slice.py +0 -0
  255. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_global_graph.py +0 -0
  256. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_google_workspace.py +0 -0
  257. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_id_normalization_contract.py +0 -0
  258. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_image_vision.py +0 -0
  259. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_import_extension_resolution.py +0 -0
  260. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_incremental.py +0 -0
  261. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ingest.py +0 -0
  262. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install.py +0 -0
  263. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_references.py +0 -0
  264. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_roundtrip.py +0 -0
  265. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_strings.py +0 -0
  266. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_install_upgrade.py +0 -0
  267. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_java_type_resolution.py +0 -0
  268. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_label_retry.py +0 -0
  269. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_labeling.py +0 -0
  270. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_language_resolvers.py +0 -0
  271. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_llm_backends.py +0 -0
  272. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_llm_parser.py +0 -0
  273. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_manifest_ingest.py +0 -0
  274. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_mcp_ingest.py +0 -0
  275. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_minhash.py +0 -0
  276. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_multigraph_compat.py +0 -0
  277. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_multigraph_diagnostics.py +0 -0
  278. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_multilang.py +0 -0
  279. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_obsidian_dangling_member.py +0 -0
  280. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_obsidian_filename_cap.py +0 -0
  281. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_office_limits.py +0 -0
  282. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ollama.py +0 -0
  283. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_openai_custom_endpoint.py +0 -0
  284. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_pascal.py +0 -0
  285. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_path_cli.py +0 -0
  286. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_pg_introspect.py +0 -0
  287. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_pipeline.py +0 -0
  288. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_provider_registry.py +0 -0
  289. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_prs.py +0 -0
  290. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_python_import_resolution.py +0 -0
  291. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_query_cli.py +0 -0
  292. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_querylog.py +0 -0
  293. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_rationale.py +0 -0
  294. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_read_hook.py +0 -0
  295. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ruby_resolution.py +0 -0
  296. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_scip_ingest.py +0 -0
  297. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_security.py +0 -0
  298. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_semantic_similarity.py +0 -0
  299. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_serve_http.py +0 -0
  300. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_skillgen.py +0 -0
  301. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_swift_cross_file_calls.py +0 -0
  302. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_swift_import_resolution.py +0 -0
  303. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_terraform.py +0 -0
  304. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_transcribe.py +0 -0
  305. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_ts_inheritance.py +0 -0
  306. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_validate.py +0 -0
  307. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_vue_extraction.py +0 -0
  308. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_watch.py +0 -0
  309. {graphifyy-0.9.2 → graphifyy-0.9.4}/tests/test_wheel_packaging.py +0 -0
  310. {graphifyy-0.9.2 → 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.2
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
 
@@ -687,7 +687,9 @@ graphify save-result --question "Q" --answer "A" --nodes Foo Bar --outcome usefu
687
687
  graphify reflect # aggregate graphify-out/memory/ outcomes into reflections/LESSONS.md
688
688
  graphify reflect --if-stale # no-op when LESSONS.md is already newer than every input (cheap to run each session)
689
689
  graphify reflect --out docs/LESSONS.md # write the lessons doc somewhere else
690
- graphify reflect --graph graphify-out/graph.json # also group lessons by community
690
+ graphify reflect --graph graphify-out/graph.json # group lessons by community + write the work-memory overlay (.graphify_learning.json)
691
+ # the overlay tags nodes preferred/tentative/contested (recency-weighted, with provenance);
692
+ # graphify explain / query then show a "Lesson:" hint, flagged "code changed — re-verify" when the source moved on
691
693
 
692
694
  graphify uninstall # remove from all platforms in one shot
693
695
  graphify uninstall --purge # also delete graphify-out/
@@ -550,7 +550,9 @@ graphify save-result --question "Q" --answer "A" --nodes Foo Bar --outcome usefu
550
550
  graphify reflect # aggregate graphify-out/memory/ outcomes into reflections/LESSONS.md
551
551
  graphify reflect --if-stale # no-op when LESSONS.md is already newer than every input (cheap to run each session)
552
552
  graphify reflect --out docs/LESSONS.md # write the lessons doc somewhere else
553
- graphify reflect --graph graphify-out/graph.json # also group lessons by community
553
+ graphify reflect --graph graphify-out/graph.json # group lessons by community + write the work-memory overlay (.graphify_learning.json)
554
+ # the overlay tags nodes preferred/tentative/contested (recency-weighted, with provenance);
555
+ # graphify explain / query then show a "Lesson:" hint, flagged "code changed — re-verify" when the source moved on
554
556
 
555
557
  graphify uninstall # remove from all platforms in one shot
556
558
  graphify uninstall --purge # also delete graphify-out/
@@ -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:
@@ -3186,6 +3220,30 @@ def main() -> None:
3186
3220
  )
3187
3221
  print(f" Type: {d.get('file_type', '')}")
3188
3222
  print(f" Community: {d.get('community_name') or d.get('community', '')}")
3223
+ # Work-memory overlay: a derived experiential hint from `graphify reflect`,
3224
+ # merged in display-only from the .graphify_learning.json sidecar next to
3225
+ # graph.json. No line when the node has no overlay entry.
3226
+ try:
3227
+ from graphify.reflect import load_learning_overlay as _llo
3228
+ from graphify.security import sanitize_label as _sl
3229
+ _overlay = _llo(gp)
3230
+ _entry = _overlay.get(str(nid))
3231
+ if _entry:
3232
+ _status = _sl(str(_entry.get("status", "")))
3233
+ if _status == "contested":
3234
+ _line = (f" Lesson: contested (useful {_entry.get('uses', 0)} / "
3235
+ f"dead-end {_entry.get('neg', 0)})")
3236
+ elif _status == "preferred":
3237
+ _line = (f" Lesson: preferred source (start here) — "
3238
+ f"{_entry.get('uses', 0)} useful, score={_entry.get('score', 0)}")
3239
+ else:
3240
+ _line = (f" Lesson: {_status or 'tentative'} — "
3241
+ f"{_entry.get('uses', 0)} useful, score={_entry.get('score', 0)}")
3242
+ if _entry.get("stale"):
3243
+ _line += " [code changed since — re-verify]"
3244
+ print(_line)
3245
+ except Exception:
3246
+ pass
3189
3247
  print(f" Degree: {G.degree(nid)}")
3190
3248
  from graphify.build import edge_data
3191
3249
  connections: list[tuple[str, str, dict]] = [] # (direction, neighbor_id, edge_data)
@@ -3501,17 +3559,20 @@ def main() -> None:
3501
3559
  else:
3502
3560
  # No labels file yet (or `graphify label` forced a refresh). When run
3503
3561
  # standalone there is no orchestrating agent to do skill.md Step 5, so
3504
- # auto-name communities with the configured backend rather than leave
3505
- # "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
3506
3564
  from graphify.llm import generate_community_labels
3507
3565
  print("Labeling communities...")
3508
- # The final labels (LLM or placeholder fallback) are persisted to
3509
- # .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)
3510
3571
  label_communities_input = communities
3511
- labels = {}
3572
+ labels = dict(hub_labels)
3512
3573
  if missing_only:
3513
3574
  labels = {
3514
- cid: existing_labels.get(cid, f"Community {cid}")
3575
+ cid: existing_labels.get(cid, hub_labels[cid])
3515
3576
  for cid in communities
3516
3577
  }
3517
3578
  label_communities_input = {
@@ -3523,16 +3584,24 @@ def main() -> None:
3523
3584
  G, label_communities_input, backend=label_backend, model=label_model, gods=gods,
3524
3585
  max_concurrency=label_max_concurrency, batch_size=label_batch_size,
3525
3586
  )
3526
- 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
+ })
3527
3594
  stages.mark("label")
3528
3595
  questions = suggest_questions(G, communities, labels)
3529
3596
  tokens = {"input": 0, "output": 0}
3530
3597
  from graphify.export import _git_head as _gh
3531
3598
  _commit = _gh()
3599
+ from graphify.report import load_learning_for_report as _llfr
3532
3600
  report = generate(G, communities, cohesion, labels, gods, surprises,
3533
3601
  {"warning": "cluster-only mode — file stats not available"},
3534
3602
  tokens, str(watch_path), suggested_questions=questions,
3535
- min_community_size=min_community_size, built_at_commit=_commit)
3603
+ min_community_size=min_community_size, built_at_commit=_commit,
3604
+ learning=_llfr(out / "graph.json"))
3536
3605
  (out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8")
3537
3606
  stages.mark("report")
3538
3607
  from graphify.export import backup_if_protected as _backup
@@ -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}
@@ -53,6 +53,56 @@ _FILE_TYPE_SYNONYMS = {
53
53
  }
54
54
 
55
55
 
56
+ # Hyperedge member lists are canonically keyed `nodes` (see graphify/llm.py
57
+ # extraction spec), but LLM/subagent drift and externally-supplied graph.json
58
+ # sometimes emit `members` or `node_ids`. _normalize_hyperedge_members folds
59
+ # those aliases into `nodes` at ingest so every downstream consumer reads one
60
+ # canonical key — mirroring the `from`/`to` edge-endpoint tolerance below.
61
+ _HE_MEMBER_ALIASES = ("members", "node_ids")
62
+
63
+
64
+ def _normalize_hyperedge_members(he: object) -> None:
65
+ """Canonicalize a hyperedge's member list onto the `nodes` key, in place.
66
+
67
+ If `nodes` is already a list it wins (canonical), and only stray alias keys
68
+ are dropped. Otherwise the first alias (`members`, then `node_ids`) that is a
69
+ list is moved to `nodes`, deduped preserving order, with a single stderr
70
+ WARNING naming the hyperedge id and alias used. Leftover alias keys are
71
+ always removed so downstream code never re-reads them.
72
+ """
73
+ if not isinstance(he, dict):
74
+ return
75
+ if not isinstance(he.get("nodes"), list):
76
+ for alias in _HE_MEMBER_ALIASES:
77
+ val = he.get(alias)
78
+ if isinstance(val, list):
79
+ seen: set = set()
80
+ deduped: list = []
81
+ for ref in val:
82
+ try:
83
+ is_dupe = ref in seen
84
+ except TypeError:
85
+ is_dupe = False # unhashable ref: keep it, validator flags it
86
+ if is_dupe:
87
+ continue
88
+ try:
89
+ seen.add(ref)
90
+ except TypeError:
91
+ pass
92
+ deduped.append(ref)
93
+ he["nodes"] = deduped
94
+ print(
95
+ f"[graphify] WARNING: hyperedge "
96
+ f"'{he.get('id', '?')}' uses field '{alias}' instead of "
97
+ f"'nodes'; normalizing.",
98
+ file=sys.stderr,
99
+ )
100
+ break
101
+ # Drop any leftover alias keys regardless of which branch ran above.
102
+ for alias in _HE_MEMBER_ALIASES:
103
+ he.pop(alias, None)
104
+
105
+
56
106
  def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
57
107
  """Normalize path separators and relativize absolute paths.
58
108
 
@@ -67,10 +117,43 @@ def _norm_source_file(p: str | None, root: str | None = None) -> str | None:
67
117
  try:
68
118
  p = Path(p).relative_to(root).as_posix()
69
119
  except ValueError:
70
- 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
71
130
  return p
72
131
 
73
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
+
74
157
  def edge_data(G: nx.Graph, u: str, v: str) -> dict:
75
158
  """Return one edge attribute dict for (u, v), tolerating MultiGraph.
76
159
 
@@ -279,6 +362,14 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
279
362
  if ft and ft not in {"code", "document", "paper", "image", "rationale", "concept"}:
280
363
  node["file_type"] = _FILE_TYPE_SYNONYMS.get(ft, "concept")
281
364
 
365
+ # Canonicalize hyperedge member lists (#1561): producers sometimes key the
366
+ # member list `members`/`node_ids` instead of `nodes`. Fold aliases onto
367
+ # `nodes` here — BEFORE validation and the semantic-rekey loop below — so
368
+ # every downstream consumer (rekey, source_file relativize, to_json) reads
369
+ # one canonical key, the same way edge endpoints alias from/to at build.
370
+ for he in extraction.get("hyperedges", []) or []:
371
+ _normalize_hyperedge_members(he)
372
+
282
373
  errors = validate_extraction(extraction)
283
374
  # Dangling edges (stdlib/external imports) are expected - only warn about real schema errors.
284
375
  real_errors = [e for e in errors if "does not match any node id" not in e]
@@ -489,8 +580,15 @@ def build_from_json(extraction: dict, *, directed: bool = False, root: str | Pat
489
580
  ".ts": "js", ".tsx": "js",
490
581
  ".go": "go", ".rs": "rs",
491
582
  ".java": "jvm", ".kt": "jvm", ".scala": "jvm", ".groovy": "jvm",
492
- ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".hpp": "cpp",
493
- ".cu": "cpp", ".cuh": "cpp", ".metal": "cpp",
583
+ # C, C++, and ObjC interoperate within one compilation unit: a method
584
+ # declared in a shared `.h` is defined/called from a `.c`/`.cpp`/`.m`
585
+ # sibling, so a cross-file INFERRED call from impl to its header decl
586
+ # is legitimate, not a phantom name-collision across languages. Treat
587
+ # the whole C family as one so the receiver-typed C++/ObjC member-call
588
+ # resolvers' header-targeting edges survive build (#1547/#1556).
589
+ ".c": "c", ".h": "c", ".cc": "c", ".cpp": "c", ".hpp": "c",
590
+ ".cxx": "c", ".hh": "c", ".hxx": "c",
591
+ ".cu": "c", ".cuh": "c", ".metal": "c", ".m": "c", ".mm": "c",
494
592
  ".rb": "rb", ".php": "php", ".cs": "cs", ".swift": "swift", ".lua": "lua",
495
593
  }
496
594
  src_ext = Path(G.nodes[src].get("source_file") or "").suffix.lower()
@@ -577,7 +675,15 @@ def deduplicate_by_label(nodes: list[dict], edges: list[dict]) -> tuple[list[dic
577
675
  """Merge nodes that share a normalised label, rewriting edge references.
578
676
 
579
677
  Prefers IDs without chunk suffixes (_c\\d+) and shorter IDs when tied.
580
- 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).
581
687
  """
582
688
  _CHUNK_SUFFIX = re.compile(r"_c\d+$")
583
689
  canonical: dict[str, dict] = {} # norm_label -> surviving node
@@ -649,16 +755,36 @@ def build_merge(
649
755
  # NetworkX round-trip loses direction permanently (#760).
650
756
  from graphify.security import check_graph_file_size_cap
651
757
  check_graph_file_size_cap(graph_path)
652
- 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
653
765
  links_key = "links" if "links" in data else "edges"
654
766
  existing_nodes = list(data.get("nodes", []))
655
767
  existing_edges = list(data.get(links_key, []))
768
+ existing_hyperedges = list(data.get("hyperedges", []))
656
769
  had_graph = True
657
770
  else:
658
771
  existing_nodes = []
659
772
  existing_edges = []
773
+ existing_hyperedges = []
660
774
  had_graph = False
661
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
+
662
788
  # Re-extracted files REPLACE their prior contribution. Every source_file
663
789
  # present in new_chunks is dropped from the loaded base before merging, so a
664
790
  # CHANGED file's stale nodes/edges don't accumulate across incremental
@@ -668,7 +794,7 @@ def build_merge(
668
794
  # for them; genuinely deleted files are still handled via prune_sources.
669
795
  # Matched in both raw and _norm_source_file form because new_chunks may carry
670
796
  # absolute win32 paths while the stored graph keeps relative posix (#1007).
671
- _replace_root = str(Path(root).resolve()) if root is not None else None
797
+ _replace_root = _eff_root
672
798
  new_sources: set[str] = set()
673
799
  for ch in new_chunks:
674
800
  for n in ch.get("nodes", []):
@@ -691,23 +817,46 @@ def build_merge(
691
817
  all_chunks = base + list(new_chunks)
692
818
  G = build(all_chunks, directed=directed, dedup=dedup, dedup_llm_backend=dedup_llm_backend, root=root)
693
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
+
694
858
  # Prune nodes and edges from deleted source files
695
859
  if prune_sources:
696
- # Build a set containing both the raw form (matches nodes that kept
697
- # absolute source_file) and the normalised relative form (matches nodes
698
- # that were relativised by _norm_source_file at build time).
699
- # .resolve() handles symlinked roots and redundant ".." / "./" segments
700
- # so Path.relative_to() succeeds even when the scan root is a symlink.
701
- # (#1007: manifest absolute paths vs graph relative source_file mismatch)
702
- _root_str = str(Path(root).resolve()) if root is not None else None
703
- prune_set: set[str] = set()
704
- for p in prune_sources:
705
- if not p:
706
- continue
707
- prune_set.add(p)
708
- norm = _norm_source_file(p, _root_str)
709
- if norm:
710
- prune_set.add(norm)
711
860
  to_remove = [
712
861
  n for n, d in G.nodes(data=True)
713
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
@@ -636,6 +636,7 @@ def to_html(
636
636
  community_labels: dict[int, str] | None = None,
637
637
  member_counts: dict[int, int] | None = None,
638
638
  node_limit: int | None = None,
639
+ learning_overlay: dict | None = None,
639
640
  ) -> None:
640
641
  """Generate an interactive vis.js HTML visualization of the graph.
641
642
 
@@ -678,7 +679,7 @@ def to_html(
678
679
  if raw_hyperedges:
679
680
  remapped = []
680
681
  for he in raw_hyperedges:
681
- he_members = he.get("nodes") or he.get("members") or []
682
+ he_members = he.get("nodes", [])
682
683
  comm_ids, seen = [], set()
683
684
  for nid in he_members:
684
685
  c = node_to_community.get(nid)
@@ -713,6 +714,21 @@ def to_html(
713
714
  max_deg = max(degree.values(), default=1) or 1
714
715
  max_mc = (max(member_counts.values(), default=1) or 1) if member_counts else 1
715
716
 
717
+ # Work-memory overlay (derived sidecar). When not passed explicitly, load it
718
+ # best-effort from the sibling .graphify_learning.json next to the output
719
+ # graph.html (which lives beside graph.json). Empty/missing => no learning
720
+ # fields, so the un-annotated render is byte-identical to pre-feature.
721
+ if learning_overlay is None:
722
+ learning_overlay = {}
723
+ try:
724
+ from graphify.reflect import load_learning_overlay as _llo
725
+ learning_overlay = _llo(Path(output_path))
726
+ except Exception:
727
+ learning_overlay = {}
728
+ # Status -> ring color. preferred=green, contested=amber. Tentative gets no
729
+ # ring (it's not yet trustworthy enough to highlight in the map).
730
+ _RING = {"preferred": "#22c55e", "contested": "#f59e0b"}
731
+
716
732
  # Build nodes list for vis.js
717
733
  vis_nodes = []
718
734
  for node_id, data in G.nodes(data=True):
@@ -728,7 +744,7 @@ def to_html(
728
744
  size = 10 + 30 * (deg / max_deg)
729
745
  # Only show label for high-degree nodes by default; others show on hover
730
746
  font_size = 12 if deg >= max_deg * 0.15 else 0
731
- vis_nodes.append({
747
+ node = {
732
748
  "id": node_id,
733
749
  "label": label,
734
750
  "color": {"background": color, "border": color, "highlight": {"background": "#ffffff", "border": color}},
@@ -740,7 +756,38 @@ def to_html(
740
756
  "source_file": sanitize_label(str(data.get("source_file") or "")),
741
757
  "file_type": data.get("file_type", ""),
742
758
  "degree": deg,
743
- })
759
+ }
760
+ # Conditional learning fields — only present for annotated nodes, so
761
+ # un-annotated output keeps the exact pre-feature node dict shape.
762
+ entry = learning_overlay.get(str(node_id)) if learning_overlay else None
763
+ if entry:
764
+ status = sanitize_label(str(entry.get("status", "")))
765
+ stale = bool(entry.get("stale"))
766
+ node["learning_status"] = status
767
+ node["learning_stale"] = stale
768
+ ring = _RING.get(status)
769
+ if ring:
770
+ # Status-colored ring via the border; stale => desaturated +
771
+ # dashed (vis.js supports per-node `shapeProperties.borderDashes`).
772
+ if stale:
773
+ ring = "#9ca3af"
774
+ node["shapeProperties"] = {"borderDashes": [4, 4]}
775
+ node["borderWidth"] = 3
776
+ node["color"] = {
777
+ "background": color, "border": ring,
778
+ "highlight": {"background": "#ffffff", "border": ring},
779
+ }
780
+ # Lesson line appended to the hover title.
781
+ if status == "contested":
782
+ lesson = f"Lesson: contested (useful {entry.get('uses', 0)} / dead-end {entry.get('neg', 0)})"
783
+ elif status == "preferred":
784
+ lesson = f"Lesson: preferred source ({entry.get('uses', 0)} useful, score={entry.get('score', 0)})"
785
+ else:
786
+ lesson = f"Lesson: {status} ({entry.get('uses', 0)} useful)"
787
+ if stale:
788
+ lesson += " [code changed — re-verify]"
789
+ node["title"] = _html.escape(label) + "\n" + _html.escape(sanitize_label(lesson))
790
+ vis_nodes.append(node)
744
791
 
745
792
  # Build edges list. Restore original edge direction from _src/_tgt
746
793
  # (stashed by build.py for exactly this reason): undirected NetworkX