graphitect 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. graphify/__init__.py +30 -0
  2. graphify/__main__.py +757 -0
  3. graphify/_minhash.py +107 -0
  4. graphify/affected.py +318 -0
  5. graphify/always_on/agents-md.md +12 -0
  6. graphify/always_on/antigravity-rules.md +14 -0
  7. graphify/always_on/claude-md.md +9 -0
  8. graphify/always_on/gemini-md.md +9 -0
  9. graphify/always_on/kiro-steering.md +5 -0
  10. graphify/always_on/vscode-instructions.md +17 -0
  11. graphify/analyze.py +769 -0
  12. graphify/benchmark.py +152 -0
  13. graphify/build.py +2300 -0
  14. graphify/cache.py +1746 -0
  15. graphify/callflow_html.py +2051 -0
  16. graphify/cargo_introspect.py +109 -0
  17. graphify/cli.py +4745 -0
  18. graphify/cluster.py +409 -0
  19. graphify/command-kilo.md +15 -0
  20. graphify/cross_repo_calls.py +216 -0
  21. graphify/cross_repo_types.py +75 -0
  22. graphify/csharp_dispatch.py +154 -0
  23. graphify/dedup.py +1213 -0
  24. graphify/detect.py +2566 -0
  25. graphify/diagnostics.py +406 -0
  26. graphify/export.py +1349 -0
  27. graphify/exporters/__init__.py +1 -0
  28. graphify/exporters/base.py +14 -0
  29. graphify/exporters/graphdb.py +173 -0
  30. graphify/exporters/html.py +637 -0
  31. graphify/extract.py +7856 -0
  32. graphify/extractors/MIGRATION.md +107 -0
  33. graphify/extractors/__init__.py +66 -0
  34. graphify/extractors/apex.py +215 -0
  35. graphify/extractors/base.py +85 -0
  36. graphify/extractors/bash.py +579 -0
  37. graphify/extractors/blade.py +53 -0
  38. graphify/extractors/commonlisp.py +540 -0
  39. graphify/extractors/csharp.py +448 -0
  40. graphify/extractors/dart.py +564 -0
  41. graphify/extractors/dm.py +494 -0
  42. graphify/extractors/elixir.py +241 -0
  43. graphify/extractors/engine.py +6509 -0
  44. graphify/extractors/fortran.py +311 -0
  45. graphify/extractors/go.py +527 -0
  46. graphify/extractors/json_config.py +240 -0
  47. graphify/extractors/julia.py +289 -0
  48. graphify/extractors/markdown.py +408 -0
  49. graphify/extractors/models.py +131 -0
  50. graphify/extractors/objc.py +566 -0
  51. graphify/extractors/ocaml.py +289 -0
  52. graphify/extractors/pascal.py +688 -0
  53. graphify/extractors/pascal_forms.py +196 -0
  54. graphify/extractors/powershell.py +522 -0
  55. graphify/extractors/razor.py +192 -0
  56. graphify/extractors/resolution.py +3584 -0
  57. graphify/extractors/robot.py +296 -0
  58. graphify/extractors/rust.py +470 -0
  59. graphify/extractors/sln.py +92 -0
  60. graphify/extractors/sql.py +720 -0
  61. graphify/extractors/terraform.py +181 -0
  62. graphify/extractors/verilog.py +329 -0
  63. graphify/extractors/zig.py +181 -0
  64. graphify/file_slice.py +246 -0
  65. graphify/global_graph.py +194 -0
  66. graphify/google_workspace.py +237 -0
  67. graphify/hooks.py +933 -0
  68. graphify/ids.py +93 -0
  69. graphify/ingest.py +358 -0
  70. graphify/install.py +2366 -0
  71. graphify/llm.py +3544 -0
  72. graphify/manifest.py +4 -0
  73. graphify/manifest_ingest.py +311 -0
  74. graphify/mcp_ingest.py +386 -0
  75. graphify/multigraph_compat.py +212 -0
  76. graphify/pascal_resolution.py +129 -0
  77. graphify/paths.py +436 -0
  78. graphify/pg_introspect.py +165 -0
  79. graphify/prs.py +770 -0
  80. graphify/querylog.py +80 -0
  81. graphify/reflect.py +882 -0
  82. graphify/report.py +346 -0
  83. graphify/resolver_registry.py +85 -0
  84. graphify/ruby_resolution.py +242 -0
  85. graphify/scip_ingest.py +363 -0
  86. graphify/security.py +460 -0
  87. graphify/semantic_cleanup.py +336 -0
  88. graphify/serve.py +2608 -0
  89. graphify/skill-agents.md +710 -0
  90. graphify/skill-aider.md +1283 -0
  91. graphify/skill-amp.md +710 -0
  92. graphify/skill-claw.md +713 -0
  93. graphify/skill-codex.md +710 -0
  94. graphify/skill-copilot.md +713 -0
  95. graphify/skill-devin.md +1410 -0
  96. graphify/skill-droid.md +710 -0
  97. graphify/skill-kilo.md +722 -0
  98. graphify/skill-kiro.md +713 -0
  99. graphify/skill-opencode.md +705 -0
  100. graphify/skill-pi.md +713 -0
  101. graphify/skill-trae.md +711 -0
  102. graphify/skill-vscode.md +709 -0
  103. graphify/skill-windows.md +755 -0
  104. graphify/skill.md +713 -0
  105. graphify/skills/agents/references/add-watch.md +56 -0
  106. graphify/skills/agents/references/exports.md +87 -0
  107. graphify/skills/agents/references/extraction-spec.md +70 -0
  108. graphify/skills/agents/references/github-and-merge.md +46 -0
  109. graphify/skills/agents/references/hooks.md +33 -0
  110. graphify/skills/agents/references/query.md +311 -0
  111. graphify/skills/agents/references/transcribe.md +52 -0
  112. graphify/skills/agents/references/update.md +210 -0
  113. graphify/skills/amp/references/add-watch.md +56 -0
  114. graphify/skills/amp/references/exports.md +87 -0
  115. graphify/skills/amp/references/extraction-spec.md +70 -0
  116. graphify/skills/amp/references/github-and-merge.md +46 -0
  117. graphify/skills/amp/references/hooks.md +33 -0
  118. graphify/skills/amp/references/query.md +311 -0
  119. graphify/skills/amp/references/transcribe.md +52 -0
  120. graphify/skills/amp/references/update.md +210 -0
  121. graphify/skills/claude/references/add-watch.md +56 -0
  122. graphify/skills/claude/references/exports.md +87 -0
  123. graphify/skills/claude/references/extraction-spec.md +70 -0
  124. graphify/skills/claude/references/github-and-merge.md +46 -0
  125. graphify/skills/claude/references/hooks.md +33 -0
  126. graphify/skills/claude/references/query.md +311 -0
  127. graphify/skills/claude/references/transcribe.md +52 -0
  128. graphify/skills/claude/references/update.md +210 -0
  129. graphify/skills/claw/references/add-watch.md +56 -0
  130. graphify/skills/claw/references/exports.md +87 -0
  131. graphify/skills/claw/references/extraction-spec.md +31 -0
  132. graphify/skills/claw/references/github-and-merge.md +46 -0
  133. graphify/skills/claw/references/hooks.md +33 -0
  134. graphify/skills/claw/references/query.md +311 -0
  135. graphify/skills/claw/references/transcribe.md +52 -0
  136. graphify/skills/claw/references/update.md +210 -0
  137. graphify/skills/codex/references/add-watch.md +56 -0
  138. graphify/skills/codex/references/exports.md +87 -0
  139. graphify/skills/codex/references/extraction-spec.md +31 -0
  140. graphify/skills/codex/references/github-and-merge.md +46 -0
  141. graphify/skills/codex/references/hooks.md +33 -0
  142. graphify/skills/codex/references/query.md +311 -0
  143. graphify/skills/codex/references/transcribe.md +52 -0
  144. graphify/skills/codex/references/update.md +210 -0
  145. graphify/skills/copilot/references/add-watch.md +56 -0
  146. graphify/skills/copilot/references/exports.md +87 -0
  147. graphify/skills/copilot/references/extraction-spec.md +70 -0
  148. graphify/skills/copilot/references/github-and-merge.md +46 -0
  149. graphify/skills/copilot/references/hooks.md +33 -0
  150. graphify/skills/copilot/references/query.md +311 -0
  151. graphify/skills/copilot/references/transcribe.md +52 -0
  152. graphify/skills/copilot/references/update.md +210 -0
  153. graphify/skills/droid/references/add-watch.md +56 -0
  154. graphify/skills/droid/references/exports.md +87 -0
  155. graphify/skills/droid/references/extraction-spec.md +70 -0
  156. graphify/skills/droid/references/github-and-merge.md +46 -0
  157. graphify/skills/droid/references/hooks.md +33 -0
  158. graphify/skills/droid/references/query.md +311 -0
  159. graphify/skills/droid/references/transcribe.md +52 -0
  160. graphify/skills/droid/references/update.md +210 -0
  161. graphify/skills/kilo/references/add-watch.md +56 -0
  162. graphify/skills/kilo/references/exports.md +87 -0
  163. graphify/skills/kilo/references/extraction-spec.md +70 -0
  164. graphify/skills/kilo/references/github-and-merge.md +46 -0
  165. graphify/skills/kilo/references/hooks.md +33 -0
  166. graphify/skills/kilo/references/query.md +311 -0
  167. graphify/skills/kilo/references/transcribe.md +52 -0
  168. graphify/skills/kilo/references/update.md +210 -0
  169. graphify/skills/kiro/references/add-watch.md +56 -0
  170. graphify/skills/kiro/references/exports.md +87 -0
  171. graphify/skills/kiro/references/extraction-spec.md +31 -0
  172. graphify/skills/kiro/references/github-and-merge.md +46 -0
  173. graphify/skills/kiro/references/hooks.md +33 -0
  174. graphify/skills/kiro/references/query.md +311 -0
  175. graphify/skills/kiro/references/transcribe.md +52 -0
  176. graphify/skills/kiro/references/update.md +210 -0
  177. graphify/skills/opencode/references/add-watch.md +56 -0
  178. graphify/skills/opencode/references/exports.md +87 -0
  179. graphify/skills/opencode/references/extraction-spec.md +70 -0
  180. graphify/skills/opencode/references/github-and-merge.md +46 -0
  181. graphify/skills/opencode/references/hooks.md +33 -0
  182. graphify/skills/opencode/references/query.md +311 -0
  183. graphify/skills/opencode/references/transcribe.md +52 -0
  184. graphify/skills/opencode/references/update.md +210 -0
  185. graphify/skills/pi/references/add-watch.md +56 -0
  186. graphify/skills/pi/references/exports.md +87 -0
  187. graphify/skills/pi/references/extraction-spec.md +31 -0
  188. graphify/skills/pi/references/github-and-merge.md +46 -0
  189. graphify/skills/pi/references/hooks.md +33 -0
  190. graphify/skills/pi/references/query.md +311 -0
  191. graphify/skills/pi/references/transcribe.md +52 -0
  192. graphify/skills/pi/references/update.md +210 -0
  193. graphify/skills/trae/references/add-watch.md +56 -0
  194. graphify/skills/trae/references/exports.md +87 -0
  195. graphify/skills/trae/references/extraction-spec.md +70 -0
  196. graphify/skills/trae/references/github-and-merge.md +46 -0
  197. graphify/skills/trae/references/hooks.md +35 -0
  198. graphify/skills/trae/references/query.md +311 -0
  199. graphify/skills/trae/references/transcribe.md +52 -0
  200. graphify/skills/trae/references/update.md +210 -0
  201. graphify/skills/vscode/references/add-watch.md +56 -0
  202. graphify/skills/vscode/references/exports.md +87 -0
  203. graphify/skills/vscode/references/extraction-spec.md +70 -0
  204. graphify/skills/vscode/references/github-and-merge.md +46 -0
  205. graphify/skills/vscode/references/hooks.md +33 -0
  206. graphify/skills/vscode/references/query.md +311 -0
  207. graphify/skills/vscode/references/transcribe.md +52 -0
  208. graphify/skills/vscode/references/update.md +210 -0
  209. graphify/skills/windows/references/add-watch.md +56 -0
  210. graphify/skills/windows/references/exports.md +87 -0
  211. graphify/skills/windows/references/extraction-spec.md +70 -0
  212. graphify/skills/windows/references/github-and-merge.md +46 -0
  213. graphify/skills/windows/references/hooks.md +33 -0
  214. graphify/skills/windows/references/query.md +311 -0
  215. graphify/skills/windows/references/transcribe.md +52 -0
  216. graphify/skills/windows/references/update.md +210 -0
  217. graphify/symbol_resolution.py +556 -0
  218. graphify/transcribe.py +186 -0
  219. graphify/tree_html.py +603 -0
  220. graphify/validate.py +95 -0
  221. graphify/watch.py +2280 -0
  222. graphify/wiki.py +405 -0
  223. graphitect/__init__.py +28 -0
  224. graphitect/__main__.py +4 -0
  225. graphitect/_vendor/__init__.py +2 -0
  226. graphitect/_vendor/archify/LICENSE +22 -0
  227. graphitect/_vendor/archify/SKILL.md +137 -0
  228. graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
  229. graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
  230. graphitect/_vendor/archify/assets/template.html +14935 -0
  231. graphitect/_vendor/archify/bin/archify.mjs +2091 -0
  232. graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
  233. graphitect/_vendor/archify/bin/preview.mjs +653 -0
  234. graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
  235. graphitect/_vendor/archify/brand-marks/README.md +31 -0
  236. graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
  237. graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
  238. graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
  239. graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
  240. graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
  241. graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
  242. graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
  243. graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
  244. graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
  245. graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
  246. graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
  247. graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
  248. graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
  249. graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
  250. graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
  251. graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
  252. graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
  253. graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
  254. graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
  255. graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
  256. graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
  257. graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
  258. graphitect/_vendor/archify/package-lock.json +149 -0
  259. graphitect/_vendor/archify/package.json +39 -0
  260. graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
  261. graphitect/_vendor/archify/references/authoring-contract.md +243 -0
  262. graphitect/_vendor/archify/references/brand-marks.md +65 -0
  263. graphitect/_vendor/archify/references/delivery-contract.md +120 -0
  264. graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
  265. graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
  266. graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
  267. graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
  268. graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
  269. graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
  270. graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
  271. graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
  272. graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
  273. graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
  274. graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
  275. graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
  276. graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
  277. graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
  278. graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  279. graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
  280. graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
  281. graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
  282. graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
  283. graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
  284. graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
  285. graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
  286. graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
  287. graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
  288. graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
  289. graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
  290. graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
  291. graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
  292. graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
  293. graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  294. graphitect/_vendor/archify/schemas/README.md +211 -0
  295. graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
  296. graphitect/_vendor/archify/schemas/common.schema.json +115 -0
  297. graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
  298. graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
  299. graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
  300. graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
  301. graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
  302. graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
  303. graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
  304. graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
  305. graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
  306. graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
  307. graphitect/_vendor/archify/skill-release.json +10 -0
  308. graphitect/cli.py +981 -0
  309. graphitect/deliver/__init__.py +5 -0
  310. graphitect/deliver/archify_adapter.py +1877 -0
  311. graphitect/deliver/archify_ir.py +160 -0
  312. graphitect/deliver/archify_repair.py +135 -0
  313. graphitect/deliver/doc_compiler.py +916 -0
  314. graphitect/ground/__init__.py +5 -0
  315. graphitect/ground/describe_source.py +27 -0
  316. graphitect/ground/fullread_source.py +56 -0
  317. graphitect/ground/graphify_source.py +107 -0
  318. graphitect/models.py +118 -0
  319. graphitect/skill/SKILL.md +80 -0
  320. graphitect/skill/agents/openai.yaml +4 -0
  321. graphitect/synthesize/__init__.py +5 -0
  322. graphitect/synthesize/engine.py +281 -0
  323. graphitect/synthesize/llm_backend.py +331 -0
  324. graphitect/synthesize/questions.py +139 -0
  325. graphitect/synthesize/rubric.py +104 -0
  326. graphitect-0.2.0.dist-info/METADATA +284 -0
  327. graphitect-0.2.0.dist-info/RECORD +336 -0
  328. graphitect-0.2.0.dist-info/WHEEL +5 -0
  329. graphitect-0.2.0.dist-info/entry_points.txt +2 -0
  330. graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
  331. graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
  332. graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
  333. graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
  334. graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
  335. graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
  336. graphitect-0.2.0.dist-info/top_level.txt +2 -0
graphify/_minhash.py ADDED
@@ -0,0 +1,107 @@
1
+ """MinHash + band-LSH — datasketch-compatible drop-in (no scipy).
2
+
3
+ datasketch.lsh has `from scipy.integrate import quad` at module level.
4
+ scipy's array_api_compat layer then lazily loads numpy.testing, which calls
5
+ platform.machine() at import time to set test-skip decorator constants — and
6
+ that in turn spawns cmd.exe via subprocess, hanging for minutes under EDR
7
+ software in corporate Windows environments.
8
+
9
+ Covers the exact MinHash/MinHashLSH API surface used by dedup.py.
10
+ Hash family (Mersenne-prime permutations) and LSH band structure are
11
+ equivalent to datasketch so dedup quality is unchanged.
12
+ """
13
+ from __future__ import annotations
14
+ import hashlib
15
+ import struct
16
+
17
+ import numpy as np
18
+
19
+
20
+ _MP = np.uint64((1 << 61) - 1) # Mersenne prime for the hash family
21
+ _MH = np.uint64(0xFFFF_FFFF) # mask to 32-bit values
22
+
23
+ # One (a, b) coefficient array per num_perm, shared across all instances.
24
+ _MH_COEFFS: dict[int, tuple[np.ndarray, np.ndarray]] = {}
25
+
26
+
27
+ def _mh_coeffs(num_perm: int) -> tuple[np.ndarray, np.ndarray]:
28
+ if num_perm not in _MH_COEFFS:
29
+ rng = np.random.RandomState(1)
30
+ a = rng.randint(1, int(_MP), num_perm, dtype=np.uint64)
31
+ b = rng.randint(0, int(_MP), num_perm, dtype=np.uint64)
32
+ _MH_COEFFS[num_perm] = (a, b)
33
+ return _MH_COEFFS[num_perm]
34
+
35
+
36
+ class MinHash:
37
+ """MinHash sketch — same API as datasketch.MinHash for the subset used here."""
38
+
39
+ __slots__ = ("num_perm", "hashvalues", "_a", "_b")
40
+
41
+ def __init__(self, num_perm: int = 128) -> None:
42
+ self.num_perm = num_perm
43
+ self.hashvalues = np.full(num_perm, int(_MH), dtype=np.uint64)
44
+ self._a, self._b = _mh_coeffs(num_perm)
45
+
46
+ def update(self, v: bytes) -> None:
47
+ hv = np.uint64(struct.unpack("<I", hashlib.sha1(v).digest()[:4])[0])
48
+ phv = np.bitwise_and((self._a * hv + self._b) % _MP, _MH)
49
+ self.hashvalues = np.minimum(self.hashvalues, phv)
50
+
51
+
52
+ def _lsh_integrate(f, lo: float, hi: float, n: int = 128) -> float:
53
+ """Numerical integration — replaces scipy.integrate.quad for LSH param search."""
54
+ h = (hi - lo) / n
55
+ return h * sum(f(lo + i * h) for i in range(n))
56
+
57
+
58
+ _LSH_PARAMS_CACHE: dict[tuple[float, int], tuple[int, int]] = {}
59
+
60
+
61
+ def _optimal_lsh_params(threshold: float, num_perm: int) -> tuple[int, int]:
62
+ """Find (bands, rows) that minimise weighted FP+FN error, without scipy."""
63
+ key = (threshold, num_perm)
64
+ if key in _LSH_PARAMS_CACHE:
65
+ return _LSH_PARAMS_CACHE[key]
66
+ best_err, best = float("inf"), (1, 1)
67
+ for b in range(1, num_perm + 1):
68
+ for r in range(1, num_perm // b + 1):
69
+ fp = _lsh_integrate(
70
+ lambda s, _b=float(b), _r=float(r): 1 - (1 - s ** _r) ** _b,
71
+ 0.0, threshold,
72
+ )
73
+ fn = _lsh_integrate(
74
+ lambda s, _b=float(b), _r=float(r): 1 - (1 - (1 - s ** _r) ** _b),
75
+ threshold, 1.0,
76
+ )
77
+ err = 0.5 * fp + 0.5 * fn
78
+ if err < best_err:
79
+ best_err, best = err, (b, r)
80
+ _LSH_PARAMS_CACHE[key] = best
81
+ return best
82
+
83
+
84
+ class MinHashLSH:
85
+ """Band-hashing LSH — same API as datasketch.MinHashLSH for the subset used here."""
86
+
87
+ def __init__(self, threshold: float = 0.5, num_perm: int = 128) -> None:
88
+ self.b, self.r = _optimal_lsh_params(threshold, num_perm)
89
+ self._tables: list[dict[bytes, list[str]]] = [{} for _ in range(self.b)]
90
+ self._keys: set[str] = set()
91
+
92
+ def insert(self, key: str, minhash: MinHash) -> None:
93
+ if key in self._keys:
94
+ raise ValueError(f"Key {key!r} already exists in MinHashLSH")
95
+ self._keys.add(key)
96
+ hv = minhash.hashvalues
97
+ for i, table in enumerate(self._tables):
98
+ band = hv[i * self.r : (i + 1) * self.r].tobytes()
99
+ table.setdefault(band, []).append(key)
100
+
101
+ def query(self, minhash: MinHash) -> list[str]:
102
+ hv = minhash.hashvalues
103
+ candidates: set[str] = set()
104
+ for i, table in enumerate(self._tables):
105
+ band = hv[i * self.r : (i + 1) * self.r].tobytes()
106
+ candidates.update(table.get(band, []))
107
+ return list(candidates)
graphify/affected.py ADDED
@@ -0,0 +1,318 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Iterable
7
+ import unicodedata
8
+
9
+ import networkx as nx
10
+
11
+
12
+ DEFAULT_AFFECTED_RELATIONS = (
13
+ "calls",
14
+ "indirect_call",
15
+ "references",
16
+ "imports",
17
+ "imports_from",
18
+ # `import('…')` — emitted by the Svelte/Astro/Vue rescue passes and (since
19
+ # #2575) by plain JS/TS too. Omitting it made every dynamic import
20
+ # invisible to blast-radius traversal even where the edge WAS in the
21
+ # graph, and dynamic import is precisely how codebases break require
22
+ # cycles, so the missing edges sat under the most load-bearing modules.
23
+ "dynamic_import",
24
+ "re_exports",
25
+ "inherits",
26
+ "extends",
27
+ "implements",
28
+ "uses",
29
+ "mixes_in",
30
+ "embeds",
31
+ "requires",
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class AffectedHit:
37
+ node_id: str
38
+ depth: int
39
+ via_relation: str
40
+ # The traversed edge's location — the actual call/import/reference SITE in
41
+ # this node's file, not the node's own definition line (#BUG1). Defaults keep
42
+ # existing constructors/tests working; None falls back to the node's def line.
43
+ via_file: "str | None" = None
44
+ via_location: "str | None" = None
45
+
46
+
47
+ def _node_label(graph: nx.Graph, node_id: str) -> str:
48
+ data = graph.nodes[node_id]
49
+ return str(data.get("label") or node_id)
50
+
51
+
52
+ def _format_location(data: dict) -> str:
53
+ source_file = data.get("source_file") or "-"
54
+ source_location = data.get("source_location")
55
+ if source_location:
56
+ return f"{source_file}:{source_location}"
57
+ return str(source_file)
58
+
59
+
60
+ def _bare_name(label: str) -> str:
61
+ """Lowercased label with the callable decoration (trailing "()") removed."""
62
+ label = _normalize_label(label)
63
+ return label[:-2] if label.endswith("()") else label
64
+
65
+
66
+ def _normalize_label(label: str) -> str:
67
+ return unicodedata.normalize("NFC", label).casefold()
68
+
69
+
70
+ def _as_repo_relative(query: str, root: Path | None = None) -> str:
71
+ """Repo-relative form of a path query, for matching a stored `source_file`.
72
+
73
+ The graph stores repo-relative paths, so `./src/x.py` and
74
+ `/abs/repo/src/x.py` name the same file as `src/x.py` and yet matched
75
+ nothing. `affected` then printed an empty list and exited 0 — a blast-radius
76
+ tool answering "nothing depends on this" about a file with sixteen
77
+ dependents, and indistinguishable from a genuine zero or a typo.
78
+
79
+ An absolute path is anchored to `root` when given — the repo root derived
80
+ from the graph's own location — so a seed resolves regardless of the caller's
81
+ working directory (#2706: an absolute-path seed previously only matched when
82
+ cwd happened to be the analysed repo root, which no editor or script can
83
+ guarantee). `root` falls back to the current directory to preserve the prior
84
+ behaviour when a caller has no graph location to derive it from.
85
+
86
+ Non-path queries pass through unchanged: `Path("myFunc()").as_posix()` is
87
+ `"myFunc()"`, so label resolution is untouched. An absolute path rooted
88
+ outside `root` is left alone — no basename guessing.
89
+ """
90
+ path = Path(query)
91
+ if path.is_absolute():
92
+ anchor = root if root is not None else Path.cwd()
93
+ try:
94
+ return path.relative_to(anchor).as_posix()
95
+ except ValueError:
96
+ # Rooted outside the repo: nothing here can make it repo-relative,
97
+ # so leave it alone rather than guess at a basename that would match
98
+ # some unrelated file with the same name.
99
+ return query
100
+ return path.as_posix()
101
+
102
+
103
+ def _prefer_file_node(
104
+ graph: nx.Graph,
105
+ node_ids: list[str],
106
+ query: str,
107
+ ) -> str | None:
108
+ """Return the file-level node when a source_file query matches many nodes."""
109
+ query_basename = _normalize_label(Path(query).name)
110
+ exact_file_nodes = [
111
+ node_id
112
+ for node_id in node_ids
113
+ if str(graph.nodes[node_id].get("source_location", "")) == "L1"
114
+ and _normalize_label(str(graph.nodes[node_id].get("label", ""))) == query_basename
115
+ ]
116
+ if len(exact_file_nodes) == 1:
117
+ return exact_file_nodes[0]
118
+
119
+ l1_nodes = [
120
+ node_id
121
+ for node_id in node_ids
122
+ if str(graph.nodes[node_id].get("source_location", "")) == "L1"
123
+ ]
124
+ if len(l1_nodes) == 1:
125
+ return l1_nodes[0]
126
+
127
+ basename_nodes = [
128
+ node_id
129
+ for node_id in node_ids
130
+ if _normalize_label(str(graph.nodes[node_id].get("label", ""))) == query_basename
131
+ ]
132
+ if len(basename_nodes) == 1:
133
+ return basename_nodes[0]
134
+
135
+ return None
136
+
137
+
138
+ def resolve_seed(graph: nx.Graph, query: str, root: Path | None = None) -> str | None:
139
+ # A trailing path separator must not change a source-file match — serve's
140
+ # _find_node tokenizes the path (which drops it), so strip it here for parity
141
+ # (otherwise `affected "src/x.ts/"` returned None while `explain` resolved it).
142
+ query = query.rstrip("/\\") or query
143
+ if query in graph:
144
+ return query
145
+ query_lower = _normalize_label(query)
146
+ exact_label_matches = [
147
+ str(node_id)
148
+ for node_id, data in graph.nodes(data=True)
149
+ if _normalize_label(str(data.get("label", ""))) == query_lower
150
+ ]
151
+ if len(exact_label_matches) == 1:
152
+ return exact_label_matches[0]
153
+ # Callable labels are decorated ("name()"), so a bare "name" query falls
154
+ # through exact matching and then ties with any "name*" sibling in the
155
+ # contains pass. Match on the undecorated name before giving up.
156
+ query_bare = _bare_name(query_lower)
157
+ bare_name_matches = [
158
+ str(node_id)
159
+ for node_id, data in graph.nodes(data=True)
160
+ if _bare_name(str(data.get("label", ""))) == query_bare
161
+ ]
162
+ if len(bare_name_matches) == 1:
163
+ return bare_name_matches[0]
164
+ # Compare paths in repo-relative form. Only this branch is path-shaped; the
165
+ # label branches above keep the query verbatim.
166
+ query_path = _normalize_label(_as_repo_relative(query, root))
167
+ exact_source_matches = [
168
+ str(node_id)
169
+ for node_id, data in graph.nodes(data=True)
170
+ if _normalize_label(str(data.get("source_file", ""))) in (query_lower, query_path)
171
+ ]
172
+ if len(exact_source_matches) == 1:
173
+ return exact_source_matches[0]
174
+ if exact_source_matches:
175
+ preferred_file_node = _prefer_file_node(
176
+ graph, exact_source_matches, _as_repo_relative(query, root)
177
+ )
178
+ if preferred_file_node is not None:
179
+ return preferred_file_node
180
+ contains_matches = [
181
+ str(node_id)
182
+ for node_id, data in graph.nodes(data=True)
183
+ if query_lower in _normalize_label(str(data.get("label", "")))
184
+ ]
185
+ if len(contains_matches) == 1:
186
+ return contains_matches[0]
187
+ return None
188
+
189
+
190
+ def affected_nodes(
191
+ graph: nx.Graph,
192
+ seed: str,
193
+ *,
194
+ relations: Iterable[str] = DEFAULT_AFFECTED_RELATIONS,
195
+ depth: int = 2,
196
+ ) -> list[AffectedHit]:
197
+ relation_set = set(relations)
198
+ seen = {seed}
199
+ queue: deque[tuple[str, int]] = deque([(seed, 0)])
200
+ hits: list[AffectedHit] = []
201
+
202
+ # #1669: seed the reverse walk with the root's own member nodes (one outward
203
+ # `method`/`contains` hop). A caller can bind to a class's method node rather
204
+ # than the class node itself (e.g. `Service.call` resolves to the `def
205
+ # self.call` node, #1634), so those callers are unreachable from the class
206
+ # otherwise. The member nodes are seeds only (not reported as hits), and
207
+ # `method`/`contains` stay out of the general relation-filtered walk, so this
208
+ # adds no forward noise anywhere else.
209
+ if hasattr(graph, "out_edges"):
210
+ member_edges = graph.out_edges(seed, data=True)
211
+ else:
212
+ member_edges = (
213
+ (s, t, d) for s, t, d in graph.edges(data=True) if s == seed
214
+ )
215
+ for _s, member, data in member_edges:
216
+ if str(data.get("relation", "")) not in ("method", "contains"):
217
+ continue
218
+ member = str(member)
219
+ if member not in seen:
220
+ seen.add(member)
221
+ queue.append((member, 0))
222
+
223
+ while queue:
224
+ current, current_depth = queue.popleft()
225
+ if current_depth >= depth:
226
+ continue
227
+ if hasattr(graph, "in_edges"):
228
+ incoming = graph.in_edges(current, data=True)
229
+ else:
230
+ incoming = (
231
+ (source, target, data)
232
+ for source, target, data in graph.edges(data=True)
233
+ if target == current
234
+ )
235
+ for source, _target, data in incoming:
236
+ relation = str(data.get("relation", ""))
237
+ if relation not in relation_set:
238
+ continue
239
+ source = str(source)
240
+ if source in seen:
241
+ continue
242
+ seen.add(source)
243
+ # Carry the matched edge's location (taken from the SAME edge dict
244
+ # whose relation passed the filter, so relation and location stay
245
+ # consistent) — that is the call/import/reference site in `source`'s
246
+ # own file, which is where the user should click (#BUG1).
247
+ hit = AffectedHit(
248
+ source, current_depth + 1, relation,
249
+ via_file=str(data.get("source_file") or "") or None,
250
+ via_location=str(data.get("source_location") or "") or None,
251
+ )
252
+ hits.append(hit)
253
+ queue.append((source, current_depth + 1))
254
+
255
+ return hits
256
+
257
+
258
+ def format_affected(
259
+ graph: nx.Graph,
260
+ query: str,
261
+ *,
262
+ relations: Iterable[str] = DEFAULT_AFFECTED_RELATIONS,
263
+ depth: int = 2,
264
+ root: Path | None = None,
265
+ ) -> str:
266
+ relation_list = tuple(relations)
267
+ seed = resolve_seed(graph, query, root)
268
+ if seed is None:
269
+ return f"No unique node match for {query}"
270
+
271
+ hits = affected_nodes(graph, seed, relations=relation_list, depth=depth)
272
+ lines = [
273
+ f"Affected nodes for {_node_label(graph, seed)}",
274
+ f"Relations: {', '.join(relation_list)}",
275
+ f"Depth: {depth}",
276
+ ]
277
+ if not hits:
278
+ lines.append("No affected nodes found.")
279
+ return "\n".join(lines)
280
+
281
+ for hit in hits:
282
+ data = graph.nodes[hit.node_id]
283
+ if hit.via_location:
284
+ # The relation SITE in this node's file (call/import/reference line),
285
+ # labeled by [via_relation] so it's never mistaken for a def line.
286
+ location = f"{hit.via_file or data.get('source_file') or '-'}:{hit.via_location}"
287
+ else:
288
+ location = _format_location(data) # honest fallback: the node's own def line
289
+ lines.append(
290
+ f"- {_node_label(graph, hit.node_id)} [{hit.via_relation}] {location}"
291
+ )
292
+ return "\n".join(lines)
293
+
294
+
295
+ def load_graph(path: Path) -> nx.Graph:
296
+ import json
297
+ from networkx.readwrite import json_graph
298
+
299
+ try:
300
+ raw = json.loads(path.read_text(encoding="utf-8"))
301
+ except (json.JSONDecodeError, OSError) as exc:
302
+ raise RuntimeError(
303
+ f"Cannot read graph file {path}: {exc}. "
304
+ "Re-run 'graphify extract' to regenerate it."
305
+ ) from exc
306
+ # Force directed so stored caller→callee direction survives the round-trip;
307
+ # mirrors serve.py and __main__.py (#1174).
308
+ raw = {**raw, "directed": True}
309
+ # Normalize the edge key: graphify's `extract` output uses "edges" while
310
+ # networkx's node_link_data default is "links". Without this, an edges-keyed
311
+ # graph.json raises an uncaught KeyError: 'links' here — every other loader
312
+ # (__main__.py) already normalizes this (#738; same class as #1198).
313
+ if "links" not in raw and "edges" in raw:
314
+ raw = dict(raw, links=raw["edges"])
315
+ try:
316
+ return json_graph.node_link_graph(raw, edges="links")
317
+ except TypeError:
318
+ return json_graph.node_link_graph(raw)
@@ -0,0 +1,12 @@
1
+ ## graphify
2
+
3
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
4
+
5
+ When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else.
6
+
7
+ Rules:
8
+ - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
9
+ - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
10
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
11
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
12
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
@@ -0,0 +1,14 @@
1
+ ---
2
+ trigger: always_on
3
+ description: Consult the graphify knowledge graph at graphify-out/ for codebase and architecture questions.
4
+ ---
5
+
6
+ ## graphify
7
+
8
+ This project has a graphify knowledge graph at graphify-out/.
9
+
10
+ Rules:
11
+ - For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (CLI) or `query_graph` (MCP). Use `graphify path "<A>" "<B>"` / `shortest_path` for relationships and `graphify explain "<concept>"` / `get_node` for focused concepts. These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output.
12
+ - If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
13
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context
14
+ - After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
@@ -0,0 +1,9 @@
1
+ ## graphify
2
+
3
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
4
+
5
+ Rules:
6
+ - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
7
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
8
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
9
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
@@ -0,0 +1,9 @@
1
+ ## graphify
2
+
3
+ This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
4
+
5
+ Rules:
6
+ - For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
7
+ - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
8
+ - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
9
+ - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
@@ -0,0 +1,5 @@
1
+ ---
2
+ inclusion: always
3
+ ---
4
+
5
+ graphify: A knowledge graph of this project lives in `graphify-out/`. For codebase, architecture, or dependency questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (or `graphify path "<A>" "<B>"` / `graphify explain "<concept>"`). These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output. Read `GRAPH_REPORT.md` only for broad architecture review or when those commands do not surface enough context.
@@ -0,0 +1,17 @@
1
+ ## graphify
2
+
3
+ For any question about this repo's architecture, structure, components, or how to add/modify/find
4
+ code, your first action should be `graphify query "<question>"` when `graphify-out/graph.json`
5
+ exists. Use `graphify path "<A>" "<B>"` for relationship questions and `graphify explain "<concept>"`
6
+ for focused-concept questions. These return a scoped subgraph, usually much smaller than the full
7
+ report or raw grep output.
8
+
9
+ Triggers: "how do I…", "where is…", "what does … do", "add/modify a <component>",
10
+ "explain the architecture", or anything that depends on how files or classes relate.
11
+
12
+ If `graphify-out/wiki/index.md` exists, use it for broad navigation. Read `graphify-out/GRAPH_REPORT.md`
13
+ only for broad architecture review or when query/path/explain do not surface enough context. Only read
14
+ source files when (a) modifying/debugging specific code, (b) the graph lacks the needed detail, or
15
+ (c) the graph is missing or stale.
16
+
17
+ Type `/graphify` in Copilot Chat to build or update the graph.