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,296 @@
1
+ """Robot Framework extractor for .robot/.resource files (issue #3192)."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import re
6
+
7
+ from pathlib import Path
8
+ from graphify.extractors.base import _file_stem, _make_id
9
+
10
+
11
+ # Robot Framework standard libraries - imports of these are noise (like the
12
+ # Python built-ins in _LANGUAGE_BUILTIN_GLOBALS), so only third-party
13
+ # libraries get stub nodes.
14
+ _ROBOT_STDLIBS = frozenset({
15
+ "BuiltIn", "Collections", "DateTime", "Dialogs", "Easter", "OperatingSystem",
16
+ "Process", "Remote", "Reserved", "Screenshot", "String", "Telnet", "XML",
17
+ })
18
+
19
+
20
+ _ROBOT_VAR_RE = re.compile(r"\$\{([^}]*)\}")
21
+
22
+
23
+ def _resolve_robot_import(raw: str, source_path: Path) -> Path | None:
24
+ """Resolve a Settings-section import path relative to the importing file.
25
+
26
+ Preserves the relative/absolute form of source_path so the target ID
27
+ matches the ID the imported file's own extraction produces (same approach
28
+ as the JS relative-import resolver). Robot matches variable names case-,
29
+ space-, and underscore-insensitively, so ``${curdir}`` and ``${Cur_Dir}``
30
+ resolve like ``${CURDIR}``. Returns None when the path holds any other
31
+ (unresolvable) ${VARIABLE} or %{ENV_VAR}.
32
+ """
33
+ s = raw.strip()
34
+ # ${CURDIR}/${EXECDIR} already anchor the path - substituting and then
35
+ # re-joining below would prefix source_path.parent twice when the scan
36
+ # uses relative paths, so track anchoring explicitly.
37
+ anchored = False
38
+ parts = []
39
+ last = 0
40
+ for m in _ROBOT_VAR_RE.finditer(s):
41
+ name = m.group(1).replace(" ", "").replace("_", "").lower()
42
+ if name == "/":
43
+ repl = "/"
44
+ elif name == "curdir":
45
+ repl = str(source_path.parent)
46
+ anchored = True
47
+ elif name == "execdir":
48
+ # ${EXECDIR} is the directory Robot was launched from - statically
49
+ # knowable only for a relative scan, where "." is the scan root
50
+ # and matches how relative file-node IDs are built. For an
51
+ # absolute source path the resolved ID could never match a node,
52
+ # so emit nothing rather than a guaranteed-dangling edge.
53
+ if source_path.is_absolute():
54
+ return None
55
+ repl = "."
56
+ anchored = True
57
+ else:
58
+ return None
59
+ parts.append(s[last:m.start()])
60
+ parts.append(repl)
61
+ last = m.end()
62
+ parts.append(s[last:])
63
+ s = "".join(parts)
64
+ if "%{" in s:
65
+ return None
66
+ p = Path(s)
67
+ if not anchored and not p.is_absolute():
68
+ p = source_path.parent / p
69
+ # normpath unconditionally so ../ segments collapse and the ID matches
70
+ # the imported file's own node ID (absolute paths included)
71
+ return Path(os.path.normpath(p))
72
+
73
+
74
+ # Robot resolves BDD-style calls (Given/When/Then/And/But <keyword>) by trying
75
+ # the full name first, then the name with one prefix stripped. English prefixes
76
+ # only - Robot's localized BDD prefixes are out of scope here.
77
+ _BDD_PREFIXES = ("given ", "when ", "then ", "and ", "but ")
78
+
79
+
80
+ def _kw_id(name: str) -> str:
81
+ """Node ID for a keyword, normalized the way Robot Framework matches
82
+ keyword names: case-, space-, and underscore-insensitive. Stripping
83
+ spaces/underscores before _make_id means ``Open Session``,
84
+ ``open_session``, and ``OpenSession`` all share one node ID, exactly as
85
+ Robot resolves them to one keyword."""
86
+ return _make_id(name.replace(" ", "").replace("_", ""))
87
+
88
+
89
+ def extract_robot(path: Path) -> dict:
90
+ """Extract suites, test cases, user keywords, imports, and keyword calls
91
+ from Robot Framework .robot/.resource files via the official robot.api
92
+ parser (no maintained tree-sitter grammar exists for Robot Framework).
93
+
94
+ Nodes: the suite/resource file, its test cases, and its user keywords.
95
+ Edges: `contains` (file -> test case / keyword), `imports` (file -> the
96
+ Resource/Library/Variables files it pulls in, resolved onto the imported
97
+ file's own node; non-stdlib named libraries like SeleniumLibrary get stub
98
+ nodes so suites sharing a library cluster together), and `calls`
99
+ (test case / keyword / suite fixture -> the keyword it invokes).
100
+
101
+ Keyword nodes are keyed by bare keyword name (not stem-qualified),
102
+ normalized the way Robot matches keywords (case-, space-, and
103
+ underscore-insensitive, see _kw_id): Robot resolves keywords globally by
104
+ name across imported resources, so bare IDs make cross-file call edges
105
+ land on the defining node without a separate resolution pass. Two files
106
+ defining the same keyword name deliberately merge into one node - that
107
+ mirrors Robot's own single global namespace (where such duplicates are
108
+ ambiguous). Test case nodes stay stem-qualified because test names repeat
109
+ across suites. Calls to keywords never defined in the corpus (e.g.
110
+ BuiltIn's Log) are dropped by build_from_json like any external reference.
111
+ """
112
+ try:
113
+ from robot.api import get_model, get_resource_model
114
+ from robot.api.parsing import ModelVisitor
115
+ except ImportError:
116
+ return {"nodes": [], "edges": [],
117
+ "error": "robotframework not installed. Run: pip install robotframework"}
118
+
119
+ stem = _file_stem(path)
120
+ str_path = str(path)
121
+ nodes: list[dict] = []
122
+ edges: list[dict] = []
123
+ seen_ids: set[str] = set()
124
+ seen_edges: set[tuple] = set()
125
+
126
+ def add_node(nid: str, label: str, line: int) -> None:
127
+ if nid not in seen_ids:
128
+ seen_ids.add(nid)
129
+ nodes.append({
130
+ "id": nid,
131
+ "label": label,
132
+ "file_type": "code",
133
+ "source_file": str_path,
134
+ "source_location": f"L{line}",
135
+ })
136
+
137
+ def add_edge(src: str, tgt: str, relation: str, line: int,
138
+ confidence: str = "EXTRACTED", context: str | None = None) -> None:
139
+ if not src or not tgt or src == tgt:
140
+ return
141
+ key = (src, tgt, relation)
142
+ if key in seen_edges:
143
+ return
144
+ seen_edges.add(key)
145
+ edge = {
146
+ "source": src,
147
+ "target": tgt,
148
+ "relation": relation,
149
+ "confidence": confidence,
150
+ "source_file": str_path,
151
+ "source_location": f"L{line}",
152
+ "weight": 1.0,
153
+ }
154
+ if context:
155
+ edge["context"] = context
156
+ edges.append(edge)
157
+
158
+ file_nid = _make_id(str_path)
159
+ add_node(file_nid, path.name, 1)
160
+
161
+ try:
162
+ if path.suffix.lower() == ".resource":
163
+ model = get_resource_model(str_path)
164
+ else:
165
+ model = get_model(str_path)
166
+ except Exception as e:
167
+ return {"nodes": nodes, "edges": edges, "error": str(e)}
168
+
169
+ def kw_targets(name: str) -> list:
170
+ # "SSHLibrary.Open Connection" -> keyword part only; explicit
171
+ # library/resource prefixes are common, dots inside keyword names are not.
172
+ name = name.rsplit(".", 1)[-1].strip()
173
+ targets = [_kw_id(name)]
174
+ # BDD calls: emit an edge for BOTH the full and the prefix-stripped
175
+ # candidate, mirroring Robot's try-full-then-stripped resolution.
176
+ # Whichever keyword exists receives the edge; the other candidate
177
+ # dangles and is dropped by the graph builder.
178
+ low = name.lower()
179
+ for prefix in _BDD_PREFIXES:
180
+ if low.startswith(prefix) and len(name) > len(prefix):
181
+ targets.append(_kw_id(name[len(prefix):]))
182
+ break
183
+ return targets
184
+
185
+ def add_call_edges(src: str, name: str, line: int) -> None:
186
+ for tgt in kw_targets(name):
187
+ add_edge(src, tgt, "calls", line, context="call")
188
+
189
+ class _RobotVisitor(ModelVisitor):
190
+ def __init__(self):
191
+ self.scope_nid = file_nid
192
+
193
+ # Settings-section imports
194
+ def visit_ResourceImport(self, node):
195
+ raw = (node.name or "").strip()
196
+ if not raw:
197
+ return
198
+ target = _resolve_robot_import(raw, path)
199
+ if target is not None:
200
+ add_edge(file_nid, _make_id(str(target)), "imports", node.lineno)
201
+
202
+ def visit_LibraryImport(self, node):
203
+ raw = (node.name or "").strip()
204
+ if not raw:
205
+ return
206
+ if raw.endswith(".py") or "/" in raw or "\\" in raw:
207
+ # Path-form library import -> edge onto the Python file's node
208
+ target = _resolve_robot_import(raw, path)
209
+ if target is not None:
210
+ add_edge(file_nid, _make_id(str(target)), "imports", node.lineno)
211
+ elif raw not in _ROBOT_STDLIBS:
212
+ # External library (SeleniumLibrary, RequestsLibrary, ...) -
213
+ # stub node so suites sharing a library cluster together.
214
+ # Namespace the id so a user keyword that happens to share the
215
+ # library's name cannot collide with this stub (the label keeps
216
+ # the raw library name for display).
217
+ nid = _make_id("robot_library", raw)
218
+ add_node(nid, raw, node.lineno)
219
+ add_edge(file_nid, nid, "imports", node.lineno)
220
+
221
+ def visit_VariablesImport(self, node):
222
+ raw = (node.name or "").strip()
223
+ if not raw:
224
+ return
225
+ target = _resolve_robot_import(raw, path)
226
+ if target is not None:
227
+ add_edge(file_nid, _make_id(str(target)), "imports", node.lineno)
228
+
229
+ # Suite-level fixtures (file scope)
230
+ def visit_SuiteSetup(self, node):
231
+ if node.name:
232
+ add_call_edges(file_nid, node.name, node.lineno)
233
+
234
+ def visit_SuiteTeardown(self, node):
235
+ if node.name:
236
+ add_call_edges(file_nid, node.name, node.lineno)
237
+
238
+ def visit_TestSetup(self, node):
239
+ if node.name:
240
+ add_call_edges(file_nid, node.name, node.lineno)
241
+
242
+ def visit_TestTeardown(self, node):
243
+ if node.name:
244
+ add_call_edges(file_nid, node.name, node.lineno)
245
+
246
+ def visit_TestTemplate(self, node):
247
+ # Template statements carry the keyword in .value, not .name
248
+ if node.value:
249
+ add_call_edges(file_nid, node.value, node.lineno)
250
+
251
+ # Definitions
252
+ def visit_TestCase(self, node):
253
+ if not node.name:
254
+ return
255
+ tc_nid = _make_id(stem, node.name)
256
+ add_node(tc_nid, node.name, node.lineno)
257
+ add_edge(file_nid, tc_nid, "contains", node.lineno)
258
+ prev, self.scope_nid = self.scope_nid, tc_nid
259
+ self.generic_visit(node)
260
+ self.scope_nid = prev
261
+
262
+ def visit_Keyword(self, node):
263
+ if not node.name:
264
+ return
265
+ kw_nid = _kw_id(node.name)
266
+ add_node(kw_nid, node.name, node.lineno)
267
+ add_edge(file_nid, kw_nid, "contains", node.lineno)
268
+ prev, self.scope_nid = self.scope_nid, kw_nid
269
+ self.generic_visit(node)
270
+ self.scope_nid = prev
271
+
272
+ # Calls (current test/keyword scope, file scope for suite fixtures)
273
+ def visit_KeywordCall(self, node):
274
+ if node.keyword:
275
+ add_call_edges(self.scope_nid, node.keyword, node.lineno)
276
+ self.generic_visit(node)
277
+
278
+ def visit_Setup(self, node):
279
+ if node.name:
280
+ add_call_edges(self.scope_nid, node.name, node.lineno)
281
+
282
+ def visit_Teardown(self, node):
283
+ if node.name:
284
+ add_call_edges(self.scope_nid, node.name, node.lineno)
285
+
286
+ def visit_Template(self, node):
287
+ # Template statements carry the keyword in .value, not .name
288
+ if node.value:
289
+ add_call_edges(self.scope_nid, node.value, node.lineno)
290
+
291
+ try:
292
+ _RobotVisitor().visit(model)
293
+ except Exception as e:
294
+ return {"nodes": nodes, "edges": edges, "error": str(e)}
295
+
296
+ return {"nodes": nodes, "edges": edges}