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,688 @@
1
+ """pascal — moved verbatim from graphify/extract.py."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from graphify.extractors.base import _file_stem, _make_id
6
+ from graphify.extractors.resolution import _pascal_resolve_class, _pascal_resolve_unit
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+
11
+ _PAS_TOKEN_RE = re.compile(
12
+ r"'(?:''|[^'])*'"
13
+ r"|\{[^}]*\}"
14
+ r"|\(\*.*?\*\)"
15
+ r"|//[^\n]*",
16
+ re.DOTALL,
17
+ )
18
+
19
+ _PAS_MODULE_RE = re.compile(
20
+ r"\b(unit|program|library)\s+([A-Za-z_][\w.]*)\s*;",
21
+ re.IGNORECASE,
22
+ )
23
+
24
+ _PAS_USES_RE = re.compile(
25
+ r"\buses\b\s*([^;]+);",
26
+ re.IGNORECASE | re.DOTALL,
27
+ )
28
+
29
+ _PAS_TYPE_HEADER_RE = re.compile(
30
+ r"\b(?P<name>[A-Za-z_]\w*)(?:\s*<[^>]+>)?\s*=\s*(?:packed\s+)?"
31
+ r"(?P<kind>class|interface)\b"
32
+ r"(?:\s*\(\s*(?P<bases>[^)]*)\s*\))?",
33
+ re.IGNORECASE,
34
+ )
35
+
36
+ _PAS_END_SEMI_RE = re.compile(r"\bend\s*;", re.IGNORECASE)
37
+
38
+ _PAS_METHOD_DECL_RE = re.compile(
39
+ r"\b(?:procedure|function|constructor|destructor)\s+"
40
+ r"(?P<name>[A-Za-z_]\w*)"
41
+ r"(?:\s*\([^)]*\))?"
42
+ r"(?:\s*:\s*[\w<>,\s.]+)?"
43
+ r"\s*;",
44
+ re.IGNORECASE,
45
+ )
46
+
47
+ _PAS_IMPL_HEADER_RE = re.compile(
48
+ r"\b(?:procedure|function|constructor|destructor)\s+"
49
+ r"(?P<qual>[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)"
50
+ r"(?:\s*<[^>]+>)?"
51
+ r"(?:\s*\([^)]*\))?"
52
+ r"(?:\s*:\s*[\w<>,\s.]+)?"
53
+ r"\s*;",
54
+ re.IGNORECASE,
55
+ )
56
+
57
+ _PAS_BEGIN_END_TOKEN_RE = re.compile(
58
+ r"\b(begin|end|case|try|asm|record)\b", re.IGNORECASE
59
+ )
60
+
61
+ _PAS_CALL_RE = re.compile(r"\b([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*[(;]")
62
+
63
+ _PAS_KEYWORDS = frozenset({
64
+ "begin", "end", "if", "then", "else", "while", "do", "for", "to",
65
+ "downto", "repeat", "until", "case", "of", "try", "finally", "except",
66
+ "with", "inherited", "result", "var", "const", "type", "nil", "true",
67
+ "false", "exit", "break", "continue", "uses", "unit", "program",
68
+ "library", "interface", "implementation", "initialization", "finalization",
69
+ "procedure", "function", "constructor", "destructor", "class", "record",
70
+ "object", "array", "string", "integer", "boolean", "real", "char",
71
+ "writeln", "write", "readln", "read", "assigned", "length", "high",
72
+ "low", "inc", "dec", "new", "dispose", "setlength", "copy", "pos",
73
+ "trim", "format", "inttostr", "strtoint", "ord", "chr", "sizeof",
74
+ "create", "free", "destroy",
75
+ })
76
+
77
+ def _pascal_strip_comments(text: str) -> str:
78
+ """Strip Pascal comments ({}, (* *), //) while preserving newlines."""
79
+ def _sub(m: re.Match) -> str:
80
+ tok = m.group(0)
81
+ if tok.startswith("'"):
82
+ return tok
83
+ return "".join(c if c == "\n" else " " for c in tok)
84
+ return _PAS_TOKEN_RE.sub(_sub, text)
85
+
86
+ def _pascal_split_sections(text: str) -> tuple[str, int, str, int]:
87
+ """Split into (iface_text, iface_offset, impl_text, impl_offset).
88
+ Files without interface/implementation sections (dpr/lpr/inc) return
89
+ the whole text as impl with offset 0.
90
+ """
91
+ iface_m = re.search(r"\binterface\b", text, re.IGNORECASE)
92
+ impl_m = re.search(r"\bimplementation\b", text, re.IGNORECASE)
93
+ if iface_m and impl_m:
94
+ iface_off = iface_m.end()
95
+ impl_off = impl_m.end()
96
+ end_m = re.search(
97
+ r"\b(initialization|finalization)\b", text[impl_off:], re.IGNORECASE
98
+ )
99
+ impl_end = impl_off + end_m.start() if end_m else len(text)
100
+ return text[iface_off:impl_m.start()], iface_off, text[impl_off:impl_end], impl_off
101
+ return "", 0, text, 0
102
+
103
+ def _pascal_split_uses(s: str) -> list[str]:
104
+ """Split a uses list string, handling 'Foo in ''bar.pas''' syntax."""
105
+ out = []
106
+ for chunk in s.split(","):
107
+ name = re.split(r"\s+in\s+", chunk.strip(), maxsplit=1, flags=re.IGNORECASE)[0]
108
+ name = name.strip().strip(";")
109
+ if name and re.match(r"[A-Za-z_][\w.]*$", name):
110
+ out.append(name)
111
+ return out
112
+
113
+ def _pascal_split_bases(s: str) -> list[str]:
114
+ """Split inheritance list, handling generics like TList<T, U>."""
115
+ out, depth, buf = [], 0, []
116
+ for ch in s:
117
+ if ch == "<":
118
+ depth += 1
119
+ buf.append(ch)
120
+ elif ch == ">":
121
+ depth -= 1
122
+ buf.append(ch)
123
+ elif ch == "," and depth == 0:
124
+ name = re.sub(r"<.*$", "", "".join(buf).strip())
125
+ if name:
126
+ out.append(name)
127
+ buf = []
128
+ else:
129
+ buf.append(ch)
130
+ name = re.sub(r"<.*$", "", "".join(buf).strip())
131
+ if name:
132
+ out.append(name)
133
+ return [n for n in out if re.match(r"[A-Za-z_]\w*$", n)]
134
+
135
+ def _pascal_find_body(text: str, start: int) -> tuple[int, int]:
136
+ """Find balanced begin..end after start. Returns (body_start, body_end).
137
+ Returns (0, 0) if no begin found.
138
+ """
139
+ m = re.search(r"\bbegin\b", text[start:], re.IGNORECASE)
140
+ if not m:
141
+ return (0, 0)
142
+ body_start = start + m.end()
143
+ depth = 1
144
+ for tok in _PAS_BEGIN_END_TOKEN_RE.finditer(text, body_start):
145
+ kw = tok.group(1).lower()
146
+ if kw in ("begin", "case", "try", "asm", "record"):
147
+ depth += 1
148
+ elif kw == "end":
149
+ depth -= 1
150
+ if depth == 0:
151
+ return (body_start, tok.start())
152
+ return (body_start, len(text))
153
+
154
+ def _resolve_pascal_callee_factory(
155
+ records: list[tuple],
156
+ edges: list[dict],
157
+ module_nid: str,
158
+ ) -> Callable[[str, str], str | None]:
159
+ """Build a scoped call resolver for a single Pascal/Delphi file.
160
+
161
+ ``records`` is the list of raw per-procedure tuples produced by either
162
+ Pascal extractor; only the trailing ``(..., container, name_lower)``
163
+ fields and the leading ``proc_nid`` are used, so both extractors' tuple
164
+ shapes work unmodified (regex: proc_nid, line, body_text, container,
165
+ name_lower; tree-sitter: proc_nid, body_node, container, name_lower).
166
+
167
+ Resolution order for a call to ``name_lower`` from ``caller_nid``:
168
+ 1. A method declared on the caller's own class.
169
+ 2. A method declared on an ancestor class (BFS up ``inherits`` edges,
170
+ which are already resolved by this point).
171
+ 3. A file-level free function (declared directly under the module).
172
+ 4. A global by-name match, but only when unambiguous (exactly one
173
+ procedure with that name anywhere in the file).
174
+
175
+ Returns None (no edge emitted) when the name is ambiguous at every
176
+ level -- guessing at a same-named method on an unrelated class is worse
177
+ than omitting the edge. Same-named methods on unrelated classes are a
178
+ common Pascal/Delphi pattern (property accessors, generated wrapper
179
+ classes such as TLB import units): without this scoping, a flat
180
+ file-wide by-name lookup silently collapses onto whichever declaration
181
+ happens to be inserted last, producing wrong cross-class edges. Mirrors
182
+ the "god-node guard" already used by ``resolve_ruby_member_calls`` for
183
+ the analogous Ruby ambiguous-method-name problem.
184
+ """
185
+ class_bases: dict[str, list[str]] = {}
186
+ for e in edges:
187
+ if e.get("relation") == "inherits":
188
+ class_bases.setdefault(e["source"], []).append(e["target"])
189
+
190
+ class_procs: dict[str, dict[str, list[str]]] = {}
191
+ module_procs: dict[str, list[str]] = {}
192
+ global_procs: dict[str, list[str]] = {}
193
+ proc_owner: dict[str, str] = {}
194
+ for rec in records:
195
+ proc_nid, container, name_lower = rec[0], rec[-2], rec[-1]
196
+ proc_owner[proc_nid] = container
197
+ global_procs.setdefault(name_lower, []).append(proc_nid)
198
+ if container == module_nid:
199
+ module_procs.setdefault(name_lower, []).append(proc_nid)
200
+ else:
201
+ class_procs.setdefault(container, {}).setdefault(name_lower, []).append(proc_nid)
202
+
203
+ def _resolve(caller_nid: str, name_lower: str) -> str | None:
204
+ owner = proc_owner.get(caller_nid)
205
+ if owner is not None:
206
+ candidates = class_procs.get(owner, {}).get(name_lower)
207
+ if candidates:
208
+ return candidates[0] if len(candidates) == 1 else None
209
+ seen_bases: set[str] = set()
210
+ queue = list(class_bases.get(owner, []))
211
+ while queue:
212
+ base = queue.pop(0)
213
+ if base in seen_bases:
214
+ continue
215
+ seen_bases.add(base)
216
+ candidates = class_procs.get(base, {}).get(name_lower)
217
+ if candidates:
218
+ return candidates[0] if len(candidates) == 1 else None
219
+ queue.extend(class_bases.get(base, []))
220
+ candidates = module_procs.get(name_lower)
221
+ if candidates:
222
+ return candidates[0] if len(candidates) == 1 else None
223
+ candidates = global_procs.get(name_lower)
224
+ if candidates and len(candidates) == 1:
225
+ return candidates[0]
226
+ return None
227
+
228
+ return _resolve
229
+
230
+
231
+ def _extract_pascal_regex(path: Path) -> dict:
232
+ """Regex fallback for Pascal/Delphi extraction when tree-sitter-pascal
233
+ is unavailable. Produces the same node/edge schema as the tree-sitter pass.
234
+ """
235
+ try:
236
+ raw = path.read_text(encoding="utf-8", errors="replace")
237
+ except Exception as exc:
238
+ return {"nodes": [], "edges": [], "error": str(exc)}
239
+
240
+ str_path = str(path)
241
+ stem = _file_stem(path)
242
+ nodes: list[dict] = []
243
+ edges: list[dict] = []
244
+ seen_ids: set[str] = set()
245
+ seen_call_pairs: set[tuple[str, str]] = set()
246
+ seen_edges: set[tuple[str, str, str]] = set()
247
+
248
+ def _add_node(nid: str, label: str, line: int) -> None:
249
+ if nid not in seen_ids:
250
+ seen_ids.add(nid)
251
+ nodes.append({
252
+ "id": nid,
253
+ "label": label,
254
+ "file_type": "code",
255
+ "source_file": str_path,
256
+ "source_location": f"L{line}",
257
+ })
258
+
259
+ def _add_edge(src: str, tgt: str, relation: str, line: int, context: str | None = None) -> None:
260
+ # A class method declared in the interface section and defined in the
261
+ # implementation section both emit a `method` edge to the same node, so
262
+ # dedup on (src, tgt, relation) to keep the graph from carrying doubled
263
+ # method/contains/inherits edges (mirrors _add_node's seen_ids guard).
264
+ key = (src, tgt, relation)
265
+ if key in seen_edges:
266
+ return
267
+ seen_edges.add(key)
268
+ edge: dict = {
269
+ "source": src,
270
+ "target": tgt,
271
+ "relation": relation,
272
+ "confidence": "EXTRACTED",
273
+ "source_file": str_path,
274
+ "source_location": f"L{line}",
275
+ "weight": 1.0,
276
+ }
277
+ if context:
278
+ edge["context"] = context
279
+ edges.append(edge)
280
+
281
+ def _lineno(text: str, offset: int) -> int:
282
+ return text.count("\n", 0, offset) + 1
283
+
284
+ file_nid = _make_id(str_path)
285
+ _add_node(file_nid, path.name, 1)
286
+
287
+ stripped = _pascal_strip_comments(raw)
288
+
289
+ # Module header
290
+ module_nid = file_nid
291
+ mod_m = _PAS_MODULE_RE.search(stripped)
292
+ if mod_m:
293
+ mod_name = mod_m.group(2)
294
+ module_nid = _make_id(stem, mod_name)
295
+ _add_node(module_nid, mod_name, _lineno(stripped, mod_m.start()))
296
+ _add_edge(file_nid, module_nid, "contains", _lineno(stripped, mod_m.start()))
297
+
298
+ iface_text, iface_off, impl_text, impl_off = _pascal_split_sections(stripped)
299
+
300
+ # Uses clauses
301
+ for section_text, section_off in ((iface_text, iface_off), (impl_text, impl_off)):
302
+ for um in _PAS_USES_RE.finditer(section_text):
303
+ line = _lineno(stripped, section_off + um.start())
304
+ for unit_name in _pascal_split_uses(um.group(1)):
305
+ tgt_nid = _pascal_resolve_unit(path, unit_name)
306
+ _add_edge(module_nid, tgt_nid, "imports", line, context="import")
307
+
308
+ # Type declarations (classes / interfaces) in interface section
309
+ search_text = iface_text if iface_text else stripped
310
+ search_off = iface_off if iface_text else 0
311
+ pos = 0
312
+ while pos < len(search_text):
313
+ hm = _PAS_TYPE_HEADER_RE.search(search_text, pos)
314
+ if not hm:
315
+ break
316
+ type_name = hm.group("name")
317
+ bases_raw = hm.group("bases") or ""
318
+ line = _lineno(stripped, search_off + hm.start())
319
+ cls_nid = _make_id(stem, type_name)
320
+ _add_node(cls_nid, type_name, line)
321
+ _add_edge(module_nid, cls_nid, "contains", line)
322
+
323
+ for base_name in _pascal_split_bases(bases_raw):
324
+ same_file_nid = _make_id(stem, base_name)
325
+ if same_file_nid in seen_ids:
326
+ # Base class already declared earlier in this same file --
327
+ # reuse its real node instead of the cross-file/stub lookup
328
+ # below (which assumes one-class-per-file and would create a
329
+ # duplicate node for a base class that shares this file).
330
+ base_nid = same_file_nid
331
+ else:
332
+ resolved = _pascal_resolve_class(path, base_name)
333
+ if resolved:
334
+ # Cross-file base class found on disk -- its real node
335
+ # arrives via THAT file's own extraction. Do not add a
336
+ # duplicate stub here: it would carry this file's
337
+ # source_file (wrong -- it belongs to the base class's
338
+ # own file) and collide with the real node under
339
+ # cross-file id disambiguation, producing two different
340
+ # salted ids for what should be one class (breaks
341
+ # cross-file `inherits`-chain resolution downstream).
342
+ base_nid = resolved
343
+ else:
344
+ base_nid = _make_id(base_name)
345
+ if base_nid not in seen_ids:
346
+ _add_node(base_nid, base_name, line)
347
+ _add_edge(cls_nid, base_nid, "inherits", line)
348
+
349
+ # Find class body (up to next end;)
350
+ end_m = _PAS_END_SEMI_RE.search(search_text, hm.end())
351
+ body_text = search_text[hm.end():end_m.start()] if end_m else ""
352
+ body_off = search_off + hm.end()
353
+
354
+ # Forward method declarations inside the class body
355
+ for mm in _PAS_METHOD_DECL_RE.finditer(body_text):
356
+ mname = mm.group("name")
357
+ mline = _lineno(stripped, body_off + mm.start())
358
+ method_nid = _make_id(cls_nid, mname)
359
+ _add_node(method_nid, f"{mname}()", mline)
360
+ _add_edge(cls_nid, method_nid, "method", mline)
361
+
362
+ pos = end_m.end() if end_m else len(search_text)
363
+
364
+ # Implementation headers (procedure/function/constructor/destructor)
365
+ impl_records: list[tuple[str, int, str, str, str]] = []
366
+ # (proc_nid, line, body_text, container, name_lower)
367
+ for fm in _PAS_IMPL_HEADER_RE.finditer(impl_text):
368
+ qualified = fm.group("qual")
369
+ line = _lineno(stripped, impl_off + fm.start())
370
+ if "." in qualified:
371
+ cls_part, method_part = qualified.split(".", 1)
372
+ cls_nid = _make_id(stem, cls_part)
373
+ container = cls_nid if cls_nid in seen_ids else module_nid
374
+ relation = "method" if cls_nid in seen_ids else "contains"
375
+ label = f"{method_part}()"
376
+ name_lower = method_part.lower()
377
+ else:
378
+ container, relation = module_nid, "contains"
379
+ label = f"{qualified}()"
380
+ name_lower = qualified.lower()
381
+ proc_nid = _make_id(stem, qualified)
382
+ _add_node(proc_nid, label, line)
383
+ _add_edge(container, proc_nid, relation, line)
384
+
385
+ body_start, body_end = _pascal_find_body(impl_text, fm.end())
386
+ body_text = impl_text[body_start:body_end] if body_start else ""
387
+ impl_records.append((proc_nid, line, body_text, container, name_lower))
388
+
389
+ # Intra-file call edges, scoped by the caller's own class, then its
390
+ # ancestor chain (via `inherits` edges already emitted above), then
391
+ # file-level free functions; fall back to a global by-name match only
392
+ # when it is unambiguous (single owner across the file). Prevents
393
+ # same-named methods on unrelated classes (property accessors, generated
394
+ # wrapper classes such as TLB import units, etc. -- a common Pascal/Delphi
395
+ # pattern) from collapsing into an arbitrary cross-class edge.
396
+ callee_nid = _resolve_pascal_callee_factory(impl_records, edges, module_nid)
397
+ raw_calls: list[dict] = []
398
+ for caller_nid, caller_line, body_text, _container, _name_lower in impl_records:
399
+ for cm in _PAS_CALL_RE.finditer(body_text):
400
+ callee_name = cm.group(1).split(".")[-1].lower()
401
+ if callee_name in _PAS_KEYWORDS:
402
+ continue
403
+ call_line = caller_line + body_text.count("\n", 0, cm.start())
404
+ target_nid = callee_nid(caller_nid, callee_name)
405
+ if target_nid == caller_nid:
406
+ continue
407
+ if not target_nid:
408
+ # Not resolvable within this file (e.g. inherited from a base
409
+ # class declared in another file) -- report for the
410
+ # cross-file resolver (graphify.pascal_resolution) instead of
411
+ # guessing or dropping it silently.
412
+ raw_calls.append({
413
+ "source_file": str_path,
414
+ "source_location": f"L{call_line}",
415
+ "caller_nid": caller_nid,
416
+ "callee": callee_name,
417
+ })
418
+ continue
419
+ pair = (caller_nid, target_nid)
420
+ if pair in seen_call_pairs:
421
+ continue
422
+ seen_call_pairs.add(pair)
423
+ _add_edge(caller_nid, target_nid, "calls", call_line, context="call")
424
+
425
+ return {
426
+ "nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0,
427
+ "raw_calls": raw_calls,
428
+ }
429
+
430
+ def extract_pascal(path: Path) -> dict:
431
+ """Extract units, classes, procedures, uses-imports, and calls from Pascal/Delphi files.
432
+
433
+ Produces nodes for:
434
+ - The file itself
435
+ - unit / program / library declarations
436
+ - class and interface type declarations
437
+ - procedure / function implementations (including qualified TClass.Method names)
438
+
439
+ Produces edges for:
440
+ - file --contains--> module
441
+ - module --imports--> other file node (via uses clause, resolved to path-based IDs)
442
+ - class --inherits--> base class
443
+ - class/module --contains--> method forward declaration
444
+ - class/module --contains--> procedure/function implementation
445
+ - procedure --calls--> other procedure (within the same file)
446
+
447
+ Uses tree-sitter-pascal when available; falls back to a regex-based extractor
448
+ (_extract_pascal_regex) when it isn't installed or fails to parse, so Pascal
449
+ extraction works out of the box without an extra pip install.
450
+ """
451
+ try:
452
+ import tree_sitter_pascal as tspascal
453
+ from tree_sitter import Language, Parser
454
+ except ImportError:
455
+ return _extract_pascal_regex(path)
456
+
457
+ try:
458
+ language = Language(tspascal.language())
459
+ parser = Parser(language)
460
+ source = path.read_bytes()
461
+ tree = parser.parse(source)
462
+ root = tree.root_node
463
+ except Exception:
464
+ return _extract_pascal_regex(path)
465
+
466
+ stem = _file_stem(path)
467
+ str_path = str(path)
468
+ nodes: list[dict] = []
469
+ edges: list[dict] = []
470
+ seen_ids: set[str] = set()
471
+ seen_edges: set[tuple[str, str, str]] = set()
472
+ proc_bodies: list[tuple[str, Any, str, str]] = []
473
+ # (proc_nid, body_node, container, name_lower)
474
+
475
+ def _read(node) -> str: # type: ignore[no-untyped-def]
476
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
477
+
478
+ def add_node(nid: str, label: str, line: int) -> None:
479
+ if nid not in seen_ids:
480
+ seen_ids.add(nid)
481
+ nodes.append({
482
+ "id": nid, "label": label, "file_type": "code",
483
+ "source_file": str_path, "source_location": f"L{line}",
484
+ })
485
+
486
+ def add_edge(
487
+ src: str, tgt: str, relation: str, line: int,
488
+ confidence: str = "EXTRACTED", weight: float = 1.0,
489
+ context: str | None = None,
490
+ ) -> None:
491
+ # A class method declared in the interface section and defined in the
492
+ # implementation section both emit a `method` edge to the same node, so
493
+ # dedup on (src, tgt, relation) to keep the graph from carrying doubled
494
+ # method/contains/inherits edges (mirrors add_node's seen_ids guard).
495
+ key = (src, tgt, relation)
496
+ if key in seen_edges:
497
+ return
498
+ seen_edges.add(key)
499
+ edge: dict[str, Any] = {
500
+ "source": src, "target": tgt, "relation": relation,
501
+ "confidence": confidence, "source_file": str_path,
502
+ "source_location": f"L{line}", "weight": weight,
503
+ }
504
+ if context:
505
+ edge["context"] = context
506
+ edges.append(edge)
507
+
508
+ file_nid = _make_id(str(path))
509
+ add_node(file_nid, path.name, 1)
510
+ module_nid = file_nid
511
+
512
+ def _proc_name(header_node) -> str | None: # type: ignore[no-untyped-def]
513
+ name_node = header_node.child_by_field_name("name")
514
+ if name_node:
515
+ return _read(name_node)
516
+ for child in header_node.children:
517
+ if child.type in ("identifier", "genericDot", "genericTpl"):
518
+ return _read(child)
519
+ return None
520
+
521
+ def walk(node, parent_nid: str) -> None: # type: ignore[no-untyped-def]
522
+ nonlocal module_nid
523
+ t = node.type
524
+ line = node.start_point[0] + 1
525
+
526
+ if t in ("unit", "program", "library"):
527
+ name_node = next((c for c in node.children if c.type == "moduleName"), None)
528
+ mod_name = _read(name_node) if name_node else path.stem
529
+ mod_nid = _make_id(stem, mod_name)
530
+ add_node(mod_nid, mod_name, line)
531
+ add_edge(file_nid, mod_nid, "contains", line)
532
+ module_nid = mod_nid
533
+ for child in node.children:
534
+ walk(child, mod_nid)
535
+ return
536
+
537
+ if t == "declUses":
538
+ for child in node.children:
539
+ if child.type == "moduleName":
540
+ mod_name = _read(child)
541
+ tgt_nid = _pascal_resolve_unit(path, mod_name)
542
+ add_edge(parent_nid, tgt_nid, "imports", line, context="import")
543
+ return
544
+
545
+ if t == "declType":
546
+ type_name = None
547
+ kind_node = None
548
+ for child in node.children:
549
+ if child.type == "identifier" and type_name is None:
550
+ type_name = _read(child)
551
+ elif child.type in ("declClass", "declIntf", "declHelper") and kind_node is None:
552
+ kind_node = child
553
+ if type_name and kind_node:
554
+ cls_nid = _make_id(stem, type_name)
555
+ add_node(cls_nid, type_name, line)
556
+ add_edge(parent_nid, cls_nid, "contains", line)
557
+ for child in kind_node.children:
558
+ if child.type == "typeref":
559
+ base_name = _read(child)
560
+ base_nid = _make_id(stem, base_name)
561
+ if base_nid not in seen_ids:
562
+ # Try cross-file resolution (TFooBar → FooBar.pas)
563
+ resolved = _pascal_resolve_class(path, base_name)
564
+ if resolved:
565
+ # Cross-file base class found on disk -- its
566
+ # real node arrives via THAT file's own
567
+ # extraction. Do not add a duplicate stub
568
+ # here: it would carry this file's
569
+ # source_file (wrong) and collide with the
570
+ # real node under cross-file id
571
+ # disambiguation, producing two different
572
+ # salted ids for what should be one class.
573
+ base_nid = resolved
574
+ else:
575
+ base_nid = _make_id(base_name)
576
+ if base_nid not in seen_ids:
577
+ # Stub for RTL/external base classes.
578
+ add_node(base_nid, base_name, line)
579
+ add_edge(cls_nid, base_nid, "inherits", line)
580
+ for child in kind_node.children:
581
+ walk(child, cls_nid)
582
+ return
583
+ for child in node.children:
584
+ walk(child, parent_nid)
585
+ return
586
+
587
+ if t == "declProcFwd":
588
+ header = next((c for c in node.children if c.type == "declProc"), None)
589
+ if header:
590
+ name = _proc_name(header)
591
+ if name and "." not in name:
592
+ method_nid = _make_id(parent_nid, name)
593
+ add_node(method_nid, f"{name}()", line)
594
+ add_edge(parent_nid, method_nid, "method", line)
595
+ return
596
+
597
+ if t == "defProc":
598
+ header = next((c for c in node.children if c.type == "declProc"), None)
599
+ body_node = next((c for c in node.children if c.type == "block"), None)
600
+ if not header:
601
+ for child in node.children:
602
+ walk(child, parent_nid)
603
+ return
604
+ name = _proc_name(header)
605
+ if not name:
606
+ for child in node.children:
607
+ walk(child, parent_nid)
608
+ return
609
+ container = parent_nid
610
+ if "." in name:
611
+ parts = name.split(".", 1)
612
+ cls_nid = _make_id(stem, parts[0])
613
+ if cls_nid in seen_ids:
614
+ container = cls_nid
615
+ label = f"{parts[-1]}()"
616
+ else:
617
+ label = f"{name}()"
618
+ proc_nid = _make_id(stem, name)
619
+ add_node(proc_nid, label, line)
620
+ add_edge(
621
+ container, proc_nid,
622
+ "method" if container != parent_nid else "contains",
623
+ line,
624
+ )
625
+ if body_node:
626
+ proc_bodies.append((proc_nid, body_node, container, label.removesuffix("()").lower()))
627
+ return
628
+
629
+ for child in node.children:
630
+ walk(child, parent_nid)
631
+
632
+ walk(root, file_nid)
633
+
634
+ # Second pass: resolve calls inside procedure/function bodies, scoped by
635
+ # the caller's own class, then its ancestor chain, then file-level free
636
+ # functions, falling back to an unambiguous global match (see
637
+ # _resolve_pascal_callee_factory).
638
+ resolve_callee = _resolve_pascal_callee_factory(proc_bodies, edges, module_nid)
639
+ seen_call_pairs: set[tuple[str, str]] = set()
640
+ raw_calls: list[dict] = []
641
+
642
+ def _emit_or_report(caller_nid: str, name_lower: str, line: int) -> None:
643
+ target = resolve_callee(caller_nid, name_lower)
644
+ if target == caller_nid:
645
+ return
646
+ if not target:
647
+ # Not resolvable within this file (e.g. inherited from a base
648
+ # class declared in another file) -- report for the cross-file
649
+ # resolver (graphify.pascal_resolution) instead of guessing or
650
+ # dropping it silently.
651
+ raw_calls.append({
652
+ "source_file": str_path,
653
+ "source_location": f"L{line}",
654
+ "caller_nid": caller_nid,
655
+ "callee": name_lower,
656
+ })
657
+ return
658
+ pair = (caller_nid, target)
659
+ if pair not in seen_call_pairs:
660
+ seen_call_pairs.add(pair)
661
+ add_edge(caller_nid, target, "calls", line, context="call")
662
+
663
+ def walk_calls(node, caller_nid: str) -> None: # type: ignore[no-untyped-def]
664
+ if node.type == "exprCall":
665
+ callee_text = None
666
+ for child in node.children:
667
+ if child.is_named and child.type not in ("exprArgs",):
668
+ callee_text = _read(child).split(".")[-1]
669
+ break
670
+ if callee_text:
671
+ _emit_or_report(caller_nid, callee_text.lower(), node.start_point[0] + 1)
672
+ elif node.type == "statement":
673
+ # Pascal bare procedure calls with no args: `Reset;`
674
+ # tree-sitter represents these as statement → identifier (no exprCall wrapper)
675
+ named = [c for c in node.children if c.is_named]
676
+ if len(named) == 1 and named[0].type == "identifier":
677
+ callee_text = _read(named[0])
678
+ _emit_or_report(caller_nid, callee_text.lower(), node.start_point[0] + 1)
679
+ for child in node.children:
680
+ walk_calls(child, caller_nid)
681
+
682
+ for proc_nid, body_node, _container, _name_lower in proc_bodies:
683
+ walk_calls(body_node, proc_nid)
684
+
685
+ return {
686
+ "nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0,
687
+ "raw_calls": raw_calls,
688
+ }