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
@@ -0,0 +1,566 @@
1
+ """objc — moved verbatim from graphify/extract.py."""
2
+ from __future__ import annotations
3
+
4
+ from graphify.extractors.base import _file_stem, _make_id, _read_text
5
+ from graphify.extractors.engine import _cpp_declarator_name, _semantic_reference_edge
6
+ from graphify.extractors.resolution import _resolve_c_include_path
7
+ from pathlib import Path
8
+ from typing import Any
9
+ import re
10
+
11
+ # A category stem splits only when both halves look like plain ObjC identifiers, so
12
+ # `C++Bridge.h` and `Foo+.h` are left intact.
13
+ _OBJC_STEM_PART = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
14
+
15
+
16
+ def _objc_category_base_stem(stem: str) -> str:
17
+ """Strip an ObjC category/extension suffix from a file stem (``Foo+Cat`` -> ``Foo``).
18
+
19
+ A category (``@interface Foo (Cat)``) or class extension (``@interface Foo ()``)
20
+ declares members of an EXISTING class, so its class node must key to the same id
21
+ as ``Foo.h``'s ``@interface Foo`` instead of minting a second class node (#1556).
22
+ Only the final path segment is considered, and only a well-formed
23
+ ``Name+Suffix`` pair is split.
24
+
25
+ Mirrors ``_decldef_class_stem``, which already compares sibling header/impl files
26
+ by the stem before ``+``.
27
+ """
28
+ head, sep, tail = stem.rpartition("/")
29
+ base, plus, suffix = tail.partition("+")
30
+ if not plus or not _OBJC_STEM_PART.fullmatch(base) or not _OBJC_STEM_PART.fullmatch(suffix):
31
+ return stem
32
+ return f"{head}{sep}{base}"
33
+
34
+
35
+ def _objc_is_category(node) -> bool:
36
+ """True for ``@interface/@implementation Foo (Cat)`` and ``Foo ()``.
37
+
38
+ The grammar emits the parentheses as anonymous children only for a category or
39
+ class extension; a generic class (``@interface Box<T>``) uses
40
+ ``parameterized_arguments`` instead, so it is not matched.
41
+ """
42
+ return any(c.type == "(" for c in node.children)
43
+
44
+
45
+ def _objc_local_var_types(body_node, source: bytes, table: dict[str, str]) -> None:
46
+ """Collect ``var -> ClassName`` from ObjC local declarations (``Foo *f = ...;``)
47
+ in a method body, for receiver typing in the cross-file message-send pass
48
+ (#1556). Only a capitalized ``type_identifier`` with a single named declarator
49
+ is recorded; a built-in/lower-cased type or an un-nameable declarator is skipped
50
+ (precision over recall). Reuses the C++ declarator unwrapper (identical grammar).
51
+ """
52
+ stack = [body_node]
53
+ while stack:
54
+ n = stack.pop()
55
+ if n.type == "method_definition" and n is not body_node:
56
+ continue
57
+ if n.type == "declaration":
58
+ type_node = n.child_by_field_name("type")
59
+ if type_node is None:
60
+ for c in n.children:
61
+ if c.type == "type_identifier":
62
+ type_node = c
63
+ break
64
+ if type_node is not None and type_node.type == "type_identifier":
65
+ type_name = _read_text(type_node, source).strip()
66
+ declarators = [
67
+ c for c in n.children
68
+ if c.type in ("identifier", "pointer_declarator", "init_declarator")
69
+ ]
70
+ if type_name and type_name[:1].isupper() and len(declarators) == 1:
71
+ var = _cpp_declarator_name(declarators[0], source)
72
+ if var and var not in table:
73
+ table[var] = type_name
74
+ for c in n.children:
75
+ stack.append(c)
76
+
77
+ def extract_objc(path: Path) -> dict:
78
+ """Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files."""
79
+ try:
80
+ import tree_sitter_objc as tsobjc
81
+ from tree_sitter import Language, Parser
82
+ except ImportError:
83
+ return {"nodes": [], "edges": [], "error": "tree_sitter_objc not installed"}
84
+
85
+ try:
86
+ language = Language(tsobjc.language())
87
+ parser = Parser(language)
88
+ source = path.read_bytes()
89
+ # tree-sitter-objc cannot expand these argument-less annotation macros (no
90
+ # trailing ';'), and their presence before @interface makes the parser fail to
91
+ # emit a class_interface node (#1475). Blank them to equal-length spaces so byte
92
+ # offsets / line numbers are preserved and the interface parses.
93
+ _OBJC_BLANK_MACROS = (b"NS_ASSUME_NONNULL_BEGIN", b"NS_ASSUME_NONNULL_END")
94
+ for _m in _OBJC_BLANK_MACROS:
95
+ source = source.replace(_m, b" " * len(_m))
96
+ tree = parser.parse(source)
97
+ root = tree.root_node
98
+ except Exception as e:
99
+ return {"nodes": [], "edges": [], "error": str(e)}
100
+
101
+ stem = _file_stem(path)
102
+ str_path = str(path)
103
+ nodes: list[dict] = []
104
+ edges: list[dict] = []
105
+ seen_ids: set[str] = set()
106
+ method_bodies: list[tuple[str, Any, str]] = []
107
+ # #1556: unresolved message sends saved for the cross-file ObjC resolver, plus a
108
+ # per-file `var -> ClassName` table from `Foo *f = ...;` local declarations.
109
+ raw_calls: list[dict] = []
110
+ objc_type_table: dict[str, str] = {}
111
+ # #1556: per-CLASS `field -> ClassName` tables from `@property Bar *bar;` and
112
+ # ivar `Bar *_bar;` declarations, keyed by the class nid the .h/.m pair share
113
+ # (preserved by _merge_decl_def_classes), so the cross-file resolver can type a
114
+ # `[self.bar doIt]` / `[_bar doIt]` receiver. A conflicting redeclaration of the
115
+ # same (class, field) tombstones the entry (None) — drop, don't guess.
116
+ objc_field_types: dict[str, dict[str, str | None]] = {}
117
+
118
+ def add_node(nid: str, label: str, line: int) -> None:
119
+ if nid not in seen_ids:
120
+ seen_ids.add(nid)
121
+ nodes.append({"id": nid, "label": label, "file_type": "code",
122
+ "source_file": str_path, "source_location": f"L{line}"})
123
+
124
+ def add_edge(src: str, tgt: str, relation: str, line: int,
125
+ confidence: str = "EXTRACTED", weight: float = 1.0,
126
+ context: str | None = None) -> None:
127
+ edge = {"source": src, "target": tgt, "relation": relation,
128
+ "confidence": confidence, "source_file": str_path,
129
+ "source_location": f"L{line}", "weight": weight}
130
+ if context:
131
+ edge["context"] = context
132
+ edges.append(edge)
133
+
134
+ file_nid = _make_id(str(path))
135
+ add_node(file_nid, path.name, 1)
136
+
137
+ def _read(node) -> str:
138
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
139
+
140
+ def _get_name(node, field: str) -> str | None:
141
+ n = node.child_by_field_name(field)
142
+ return _read(n) if n else None
143
+
144
+ def _type_identifiers(node):
145
+ """Yield every type_identifier under a property's type node, descending
146
+ through generic_specifier/type_name so NSArray<Product *> yields both
147
+ NSArray and the element type Product (the generic case was invisible
148
+ because the type was wrapped in a generic_specifier, not a bare
149
+ type_identifier child) (#1475)."""
150
+ if node.type == "type_identifier":
151
+ yield node
152
+ return
153
+ for c in node.children:
154
+ yield from _type_identifiers(c)
155
+
156
+ def ensure_named_node(name: str, line: int) -> str:
157
+ nid = _make_id(stem, name)
158
+ if nid in seen_ids:
159
+ return nid
160
+ nid = _make_id(name)
161
+ if nid not in seen_ids:
162
+ # The name isn't defined in this file, so this is a cross-file reference
163
+ # (e.g. a `Thing` type annotation imported from another module). Emit a
164
+ # SOURCELESS stub — like the inheritance-base path below — so the
165
+ # corpus-level rewire can collapse it onto the real definition. A sourced
166
+ # stub here makes _disambiguate_colliding_node_ids bake the referencing
167
+ # file's path (with extension) into the id and blocks the rewire, which is
168
+ # the phantom-duplicate-node bug (#1402).
169
+ seen_ids.add(nid)
170
+ nodes.append({
171
+ "id": nid,
172
+ "label": name,
173
+ "file_type": "code",
174
+ "source_file": "",
175
+ "source_location": "",
176
+ "origin_file": str_path,
177
+ })
178
+ return nid
179
+
180
+ def _field_decl_entry(sd) -> tuple[str, str] | None:
181
+ """``(field, TypeName)`` from a property/ivar ``struct_declaration``, else None.
182
+
183
+ Precision gates (#1556): the type must be a BARE capitalized
184
+ ``type_identifier`` DIRECTLY under the struct_declaration — a
185
+ ``generic_specifier`` (``NSArray<Bar *>``) or ``typedefed_specifier``
186
+ (``id<P>``) wraps its type_identifier, so both are naturally excluded and
187
+ never type a receiver — and there must be exactly one ``struct_declarator``
188
+ whose name unwraps via the C++ declarator unwrapper (identical grammar).
189
+ """
190
+ type_ids = [s for s in sd.children if s.type == "type_identifier"]
191
+ declarators = [s for s in sd.children if s.type == "struct_declarator"]
192
+ if len(type_ids) != 1 or len(declarators) != 1:
193
+ return None
194
+ type_name = _read(type_ids[0]).strip()
195
+ if not type_name or not type_name[:1].isupper():
196
+ return None
197
+ # struct_declarator wraps ONE pointer_declarator (`*bar`) or identifier
198
+ # (`bar`); anything else (bitfields, arrays) has more children -> bail.
199
+ inner = declarators[0].children
200
+ if len(inner) != 1:
201
+ return None
202
+ field = _cpp_declarator_name(inner[0], source)
203
+ if not field:
204
+ return None
205
+ return field, type_name
206
+
207
+ def _record_field_type(cls_nid: str, field: str, type_name: str) -> None:
208
+ table = objc_field_types.setdefault(cls_nid, {})
209
+ if field in table:
210
+ if table[field] != type_name:
211
+ table[field] = None # conflicting redeclaration -> drop, don't guess
212
+ else:
213
+ table[field] = type_name
214
+
215
+ def _collect_instance_variables(ivars_node, cls_nid: str) -> None:
216
+ """Record field types from an ``instance_variables`` block (``{ Bar *_b; }``)
217
+ under an @interface or @implementation. No nodes/edges are emitted here;
218
+ the table only feeds receiver typing in the cross-file resolver (#1556).
219
+ """
220
+ for iv in ivars_node.children:
221
+ if iv.type != "instance_variable":
222
+ continue
223
+ for sd in iv.children:
224
+ if sd.type == "struct_declaration":
225
+ entry = _field_decl_entry(sd)
226
+ if entry is not None:
227
+ _record_field_type(cls_nid, *entry)
228
+
229
+ def walk(node, parent_nid: str | None = None) -> None:
230
+ t = node.type
231
+ line = node.start_point[0] + 1
232
+
233
+ if t == "preproc_include":
234
+ # #import <Foundation/Foundation.h> or #import "MyClass.h"
235
+ for child in node.children:
236
+ if child.type == "system_lib_string":
237
+ raw = _read(child).strip("<>")
238
+ module = raw.split("/")[-1].replace(".h", "")
239
+ if module:
240
+ tgt_nid = _make_id(module)
241
+ add_edge(file_nid, tgt_nid, "imports", line, context="import")
242
+ elif child.type == "string_literal":
243
+ # recurse into string_literal to find string_content
244
+ for sub in child.children:
245
+ if sub.type == "string_content":
246
+ raw = _read(sub)
247
+ # Resolve the quoted include to a real file so the target id
248
+ # matches the (possibly disambiguated) node id _make_id gives
249
+ # that file; the bare-stem id never survives
250
+ # _disambiguate_colliding_node_ids when a .h/.m pair exists,
251
+ # so the edge dangled and was dropped (#1475).
252
+ resolved = _resolve_c_include_path(raw, str_path)
253
+ if resolved is not None:
254
+ add_edge(file_nid, _make_id(str(resolved)), "imports", line, context="import")
255
+ else:
256
+ module = raw.split("/")[-1].replace(".h", "")
257
+ if module:
258
+ add_edge(file_nid, _make_id(module), "imports", line, context="import")
259
+ return
260
+
261
+ if t == "module_import":
262
+ # @import Foundation; / @import Foundation.NSString;
263
+ path_node = node.child_by_field_name("path")
264
+ if path_node is not None:
265
+ module = _read(path_node).split(".")[0].strip()
266
+ if module:
267
+ add_edge(file_nid, _make_id(module), "imports", line, context="import")
268
+ return
269
+
270
+ if t == "class_interface":
271
+ # @interface ClassName : SuperClass <Protocols>
272
+ # children: @interface, identifier(name), ':', identifier(super), parameterized_arguments, ...
273
+ identifiers = [c for c in node.children if c.type == "identifier"]
274
+ if not identifiers:
275
+ for child in node.children:
276
+ walk(child, parent_nid)
277
+ return
278
+ name = _read(identifiers[0])
279
+ # A category / class extension extends an existing class, so key its
280
+ # class node off the BASE stem (`Foo+Cat.h` -> `Foo`). Without this,
281
+ # `Foo+Cat.h` minted a SECOND node labelled `Foo`, which made every
282
+ # `[Foo ...]` receiver ambiguous and tripped the resolver's
283
+ # single-definition god-node guard — destroying edges the same corpus
284
+ # produced fine when the members lived in `Foo.h` (#1556).
285
+ cls_stem = _objc_category_base_stem(stem) if _objc_is_category(node) else stem
286
+ cls_nid = _make_id(cls_stem, name)
287
+ add_node(cls_nid, name, line)
288
+ add_edge(file_nid, cls_nid, "contains", line)
289
+ # superclass is second identifier after ':'
290
+ colon_seen = False
291
+ for child in node.children:
292
+ if child.type == ":":
293
+ colon_seen = True
294
+ elif colon_seen and child.type == "identifier":
295
+ super_nid = ensure_named_node(_read(child), line)
296
+ add_edge(cls_nid, super_nid, "inherits", line)
297
+ colon_seen = False
298
+ elif child.type == "parameterized_arguments":
299
+ # protocols adopted: @interface Foo : Bar <Proto1, Proto2>
300
+ for sub in child.children:
301
+ if sub.type == "type_name":
302
+ for s in sub.children:
303
+ if s.type == "type_identifier":
304
+ proto_nid = ensure_named_node(_read(s), line)
305
+ add_edge(cls_nid, proto_nid, "implements", line)
306
+ elif child.type == "property_declaration":
307
+ prop_line = child.start_point[0] + 1
308
+ for sub in child.children:
309
+ if sub.type == "struct_declaration":
310
+ # The type is either a direct type_identifier
311
+ # (NSString *x) or wrapped in a generic_specifier
312
+ # (NSArray<Product *> *xs). Walk every type name in the
313
+ # type portion, skipping the declarator (the *field
314
+ # name), so generic collections are no longer invisible.
315
+ seen_types: set[str] = set()
316
+ for s in sub.children:
317
+ if s.type in ("struct_declarator", ";"):
318
+ continue
319
+ for ti in _type_identifiers(s):
320
+ tname = _read(ti)
321
+ if tname in seen_types:
322
+ continue
323
+ seen_types.add(tname)
324
+ type_nid = ensure_named_node(tname, prop_line)
325
+ edges.append(_semantic_reference_edge(
326
+ cls_nid, type_nid, "field", str_path, prop_line))
327
+ # #1556: a bare capitalized property type also types the
328
+ # `[self.field msg]` receiver in the cross-file resolver.
329
+ entry = _field_decl_entry(sub)
330
+ if entry is not None:
331
+ _record_field_type(cls_nid, *entry)
332
+ elif child.type == "instance_variables":
333
+ _collect_instance_variables(child, cls_nid)
334
+ elif child.type == "method_declaration":
335
+ walk(child, cls_nid)
336
+ return
337
+
338
+ if t == "class_implementation":
339
+ # @implementation ClassName
340
+ name = None
341
+ for child in node.children:
342
+ if child.type == "identifier":
343
+ name = _read(child)
344
+ break
345
+ if not name:
346
+ for child in node.children:
347
+ walk(child, parent_nid)
348
+ return
349
+ impl_stem = _objc_category_base_stem(stem) if _objc_is_category(node) else stem
350
+ impl_nid = _make_id(impl_stem, name)
351
+ if impl_nid not in seen_ids:
352
+ add_node(impl_nid, name, line)
353
+ add_edge(file_nid, impl_nid, "contains", line)
354
+ for child in node.children:
355
+ if child.type == "instance_variables":
356
+ _collect_instance_variables(child, impl_nid)
357
+ elif child.type == "implementation_definition":
358
+ for sub in child.children:
359
+ walk(sub, impl_nid)
360
+ return
361
+
362
+ if t == "protocol_declaration":
363
+ name = None
364
+ for child in node.children:
365
+ if child.type == "identifier":
366
+ name = _read(child)
367
+ break
368
+ if name:
369
+ proto_nid = _make_id(stem, name)
370
+ add_node(proto_nid, f"<{name}>", line)
371
+ add_edge(file_nid, proto_nid, "contains", line)
372
+ # Adopted protocols: `@protocol Derived <Base, Other>`. These
373
+ # nest under a protocol_reference_list node (distinct from the
374
+ # parameterized_arguments node used by @interface adoption), so
375
+ # they were never emitted. Emit an `implements` edge for each,
376
+ # matching how @interface protocol adoption is handled.
377
+ for child in node.children:
378
+ if child.type == "protocol_reference_list":
379
+ for sub in child.children:
380
+ if sub.type == "identifier":
381
+ base_nid = ensure_named_node(_read(sub), line)
382
+ if base_nid != proto_nid:
383
+ add_edge(proto_nid, base_nid, "implements", line)
384
+ for child in node.children:
385
+ walk(child, proto_nid)
386
+ return
387
+
388
+ if t in ("method_declaration", "method_definition"):
389
+ container = parent_nid or file_nid
390
+ # Class methods start with '+', instance methods with '-' (the grammar
391
+ # emits the sigil as the first child). The selector is the concatenation
392
+ # of the direct identifier children: one for a simple selector (-go),
393
+ # several for a compound one (-tableView:numberOfRowsInSection: ->
394
+ # "tableViewnumberOfRowsInSection"); method_parameter holds the arg
395
+ # types/names, not selector keywords, so it is correctly skipped.
396
+ prefix = "-"
397
+ for child in node.children:
398
+ if child.type in ("+", "-"):
399
+ prefix = child.type
400
+ break
401
+ parts = [_read(c) for c in node.children if c.type == "identifier"]
402
+ method_name = "".join(parts) if parts else None
403
+ if method_name:
404
+ method_nid = _make_id(container, method_name)
405
+ add_node(method_nid, f"{prefix}{method_name}", line)
406
+ add_edge(container, method_nid, "method", line)
407
+ if t == "method_definition":
408
+ method_bodies.append((method_nid, node, container))
409
+ return
410
+
411
+ for child in node.children:
412
+ walk(child, parent_nid)
413
+
414
+ walk(root)
415
+
416
+ # Second pass: resolve calls inside method bodies
417
+ all_method_nids = {n["id"] for n in nodes if n["id"] != file_nid}
418
+ class_method_nids: dict[str, set[str]] = {}
419
+ for m_nid, _, container_nid in method_bodies:
420
+ class_method_nids.setdefault(container_nid, set()).add(m_nid)
421
+ seen_calls: set[tuple[str, str]] = set()
422
+ # #1556: per-file `var -> ClassName` table from local declarations in every
423
+ # method body, so the cross-file resolver can type a `[f doThing]` receiver.
424
+ for _m_nid, body_node, _container in method_bodies:
425
+ _objc_local_var_types(body_node, source, objc_type_table)
426
+
427
+ for caller_nid, body_node, container_nid in method_bodies:
428
+ sibling_nids = class_method_nids.get(container_nid, set())
429
+
430
+ def walk_calls(n) -> None:
431
+ if n.type == "message_expression":
432
+ # `[[Foo alloc] init]` is a message_expression whose method is the
433
+ # identifier `alloc` and whose receiver is the bare class identifier
434
+ # `Foo`; resolve that class name and emit a `references` edge so the
435
+ # allocating method links to the allocated type. ensure_named_node
436
+ # emits a sourceless stub for unknown names, which the corpus rewire
437
+ # collapses ONLY when exactly one real class of that name exists, so an
438
+ # unknown/ambiguous class produces no false resolved edge (#1475).
439
+ meth = n.child_by_field_name("method")
440
+ recv = n.child_by_field_name("receiver")
441
+ if (meth is not None and meth.type == "identifier" and _read(meth) == "alloc"
442
+ and recv is not None and recv.type == "identifier"):
443
+ tname = _read(recv)
444
+ ref_line = n.start_point[0] + 1
445
+ type_nid = ensure_named_node(tname, ref_line)
446
+ if type_nid != caller_nid:
447
+ edges.append(_semantic_reference_edge(
448
+ caller_nid, type_nid, "type", str_path, ref_line))
449
+ # [receiver sel] and [receiver kw1:a kw2:b] both parse to a
450
+ # message_expression whose selector parts carry the field name
451
+ # "method" (one for a simple selector, several for a compound one);
452
+ # the receiver carries field name "receiver". Reconstruct the
453
+ # selector from every "method" child so self/super/ClassName
454
+ # receivers are never mistaken for a selector, and compound sends
455
+ # resolve too (the whole second pass was previously dead code for
456
+ # ObjC because the grammar emits these as `identifier`, not
457
+ # `selector`/`keyword_argument_list`) (#1475).
458
+ sel_parts = [
459
+ _read(child)
460
+ for i, child in enumerate(n.children)
461
+ if n.field_name_for_child(i) == "method" and child.type == "identifier"
462
+ ]
463
+ method_name = "".join(sel_parts)
464
+ if method_name:
465
+ needle = _make_id("", method_name).lstrip("_")
466
+ for candidate in all_method_nids:
467
+ if candidate.endswith(needle):
468
+ pair = (caller_nid, candidate)
469
+ if pair not in seen_calls and caller_nid != candidate:
470
+ seen_calls.add(pair)
471
+ add_edge(caller_nid, candidate, "calls", n.start_point[0] + 1,
472
+ confidence="EXTRACTED", weight=1.0, context="call")
473
+ # #1556: also emit a raw_call so the cross-file resolver can type
474
+ # the receiver and link to a method in ANOTHER file. A bare
475
+ # identifier receiver (`f`, `self`, `Foo`) is captured; a nested
476
+ # message send (`[[Foo alloc] init]`) has no simple receiver name
477
+ # to type, so it is left to the alloc/init `references` edge above.
478
+ if recv is not None and recv.type == "identifier":
479
+ raw_calls.append({
480
+ "caller_nid": caller_nid,
481
+ "callee": method_name,
482
+ "is_member_call": True,
483
+ "source_file": str_path,
484
+ "source_location": f"L{n.start_point[0] + 1}",
485
+ "receiver": _read(recv),
486
+ "lang": "objc",
487
+ })
488
+ elif recv is not None and recv.type == "field_expression":
489
+ # `[self.bar doIt]`: capture ONLY the exact `self.<field>`
490
+ # shape and stamp the BARE field name, typed via the class's
491
+ # property/ivar table. Anything else (`obj.prop`, chains,
492
+ # `Foo.shared`) stays dropped: passing the dotted text
493
+ # through would let a capitalized `Foo.shared` enter the
494
+ # explicit-class arm, where _key strips the dot and collides
495
+ # with a real class `FooShared` — a fabricated edge (#1556).
496
+ kids = recv.children
497
+ if (len(kids) == 3 and kids[0].type == "identifier"
498
+ and _read(kids[0]) == "self" and kids[1].type == "."
499
+ and kids[2].type == "field_identifier"):
500
+ raw_calls.append({
501
+ "caller_nid": caller_nid,
502
+ "callee": method_name,
503
+ "is_member_call": True,
504
+ "source_file": str_path,
505
+ "source_location": f"L{n.start_point[0] + 1}",
506
+ "receiver": _read(kids[2]),
507
+ "receiver_kind": "self_field",
508
+ "lang": "objc",
509
+ })
510
+ elif n.type == "field_expression":
511
+ # self.name / self.product.name — dot-syntax sugar for [self name].
512
+ # Resolve to a sibling method of the SAME class, matched by EXACT
513
+ # node id (a method id is _make_id(container, name)). A suffix
514
+ # substring match would mis-resolve self.name -> -surname and would
515
+ # let a substring-colliding sibling (-surname) suppress the real
516
+ # -name edge, so it must be an exact match (#1475).
517
+ for child in n.children:
518
+ if child.type == "field_identifier":
519
+ field_name = _read(child)
520
+ target = _make_id(container_nid, field_name)
521
+ if target in sibling_nids and target != caller_nid:
522
+ pair = (caller_nid, target)
523
+ if pair not in seen_calls:
524
+ seen_calls.add(pair)
525
+ add_edge(caller_nid, target, "accesses",
526
+ n.start_point[0] + 1,
527
+ confidence="EXTRACTED", weight=1.0)
528
+ elif n.type == "selector_expression":
529
+ # @selector(doSomething:withParam:) — compile-time method ref.
530
+ # Match the selector name EXACTLY (a method id is
531
+ # _make_id(container, name)) against every class's methods, and emit
532
+ # only when exactly one method matches, to avoid ambiguous fan-out.
533
+ # Exact match (not a suffix) keeps -doThing distinct from
534
+ # -reallyDoThing (#1475).
535
+ sel_parts = [_read(c) for c in n.children if c.type == "identifier"]
536
+ sel_name = "".join(sel_parts)
537
+ if sel_name:
538
+ matches = sorted({
539
+ m for m, _, cont in method_bodies
540
+ if m == _make_id(cont, sel_name) and m != caller_nid
541
+ })
542
+ if len(matches) == 1:
543
+ pair = (caller_nid, matches[0])
544
+ if pair not in seen_calls:
545
+ seen_calls.add(pair)
546
+ add_edge(caller_nid, matches[0], "calls",
547
+ n.start_point[0] + 1,
548
+ confidence="EXTRACTED", weight=1.0,
549
+ context="call")
550
+ for child in n.children:
551
+ walk_calls(child)
552
+ walk_calls(body_node)
553
+
554
+ result = {"nodes": nodes, "edges": edges, "raw_calls": raw_calls,
555
+ "input_tokens": 0, "output_tokens": 0}
556
+ if objc_type_table:
557
+ result["objc_type_table"] = {"path": str_path, "table": objc_type_table}
558
+ # Drop tombstoned (conflicting) entries and empty tables before export.
559
+ field_tables = {
560
+ cls: {f: t for f, t in tbl.items() if t}
561
+ for cls, tbl in objc_field_types.items()
562
+ }
563
+ field_tables = {cls: tbl for cls, tbl in field_tables.items() if tbl}
564
+ if field_tables:
565
+ result["objc_field_types"] = {"path": str_path, "tables": field_tables}
566
+ return result