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,494 @@
1
+ """Dm extractor. Moved verbatim from graphify/extract.py."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from graphify.extractors.base import _file_stem, _make_id, _read_text
9
+
10
+
11
+ def extract_dm(path: Path) -> dict:
12
+ """Extract types, procs, includes, and calls from a .dm/.dme file."""
13
+ try:
14
+ import tree_sitter_dm as tsdm
15
+ from tree_sitter import Language, Parser
16
+ except ImportError:
17
+ return {"nodes": [], "edges": [], "error": "tree-sitter-dm not installed"}
18
+ try:
19
+ language = Language(tsdm.language())
20
+ parser = Parser(language)
21
+ source = path.read_bytes()
22
+ tree = parser.parse(source)
23
+ root = tree.root_node
24
+ except Exception as e:
25
+ return {"nodes": [], "edges": [], "error": str(e)}
26
+
27
+ stem = _file_stem(path)
28
+ str_path = str(path)
29
+ nodes: list[dict] = []
30
+ edges: list[dict] = []
31
+ seen_ids: set[str] = set()
32
+ function_bodies: list[tuple[str, Any, "str | None"]] = []
33
+
34
+ def add_node(nid: str, label: str, line: int) -> None:
35
+ if nid and nid not in seen_ids:
36
+ seen_ids.add(nid)
37
+ nodes.append({"id": nid, "label": label, "file_type": "code",
38
+ "source_file": str_path, "source_location": f"L{line}"})
39
+
40
+ def add_edge(src: str, tgt: str, relation: str, line: int,
41
+ confidence: str = "EXTRACTED", weight: float = 1.0,
42
+ context: str | None = None) -> None:
43
+ if not src or not tgt or src == tgt:
44
+ return
45
+ edge: dict = {"source": src, "target": tgt, "relation": relation,
46
+ "confidence": confidence, "source_file": str_path,
47
+ "source_location": f"L{line}", "weight": weight}
48
+ if context:
49
+ edge["context"] = context
50
+ edges.append(edge)
51
+
52
+ file_nid = _make_id(str(path))
53
+ add_node(file_nid, path.name, 1)
54
+
55
+ def _type_path_text(node) -> str:
56
+ return _read_text(node, source).strip()
57
+
58
+ def _ensure_type(path_text: str, line: int) -> str:
59
+ nid = _make_id(stem, path_text)
60
+ add_node(nid, path_text, line)
61
+ return nid
62
+
63
+ def _find_child(node, type_name: str):
64
+ for c in node.children:
65
+ if c.type == type_name:
66
+ return c
67
+ return None
68
+
69
+ def _read_include_path(file_node) -> str:
70
+ if file_node is None:
71
+ return ""
72
+ if file_node.type == "string_literal":
73
+ parts = []
74
+ for c in file_node.children:
75
+ if c.type == "string_content":
76
+ parts.append(_read_text(c, source))
77
+ return "".join(parts)
78
+ return _read_text(file_node, source).strip("'\"")
79
+
80
+ def walk(node, parent_type_path: "str | None" = None,
81
+ parent_type_nid: "str | None" = None) -> None:
82
+ t = node.type
83
+ line = node.start_point[0] + 1
84
+
85
+ if t == "preproc_include":
86
+ file_node = node.child_by_field_name("file")
87
+ raw = _read_include_path(file_node)
88
+ if raw:
89
+ norm = re.sub(r"^\./", "", raw.replace("\\", "/"))
90
+ resolved = (path.parent / norm).resolve()
91
+ edge: dict = {
92
+ "source": file_nid,
93
+ "target": _make_id(str(resolved)) if resolved.exists() else _make_id(norm),
94
+ "relation": "imports_from" if resolved.exists() else "imports",
95
+ "context": "import",
96
+ "confidence": "EXTRACTED",
97
+ "source_file": str_path,
98
+ "source_location": f"L{line}",
99
+ "weight": 1.0,
100
+ }
101
+ if not resolved.exists():
102
+ edge["external"] = True
103
+ edges.append(edge)
104
+ return
105
+
106
+ if t == "type_definition":
107
+ tp_node = _find_child(node, "type_path")
108
+ if tp_node is None:
109
+ return
110
+ type_path_str = _type_path_text(tp_node)
111
+ type_nid = _ensure_type(type_path_str, line)
112
+ add_edge(file_nid, type_nid, "contains", line)
113
+ body = _find_child(node, "type_body")
114
+ if body is not None:
115
+ for c in body.children:
116
+ walk(c, parent_type_path=type_path_str, parent_type_nid=type_nid)
117
+ return
118
+
119
+ if t in ("type_body_intended", "type_body_braced"):
120
+ for c in node.children:
121
+ walk(c, parent_type_path, parent_type_nid)
122
+ return
123
+
124
+ if t in ("type_proc_definition", "type_proc_override"):
125
+ if parent_type_nid is None or parent_type_path is None:
126
+ return
127
+ name_node = node.child_by_field_name("name")
128
+ if name_node is None:
129
+ return
130
+ proc_name = _read_text(name_node, source)
131
+ proc_nid = _make_id(stem, parent_type_path, proc_name)
132
+ add_node(proc_nid, f"{parent_type_path}/{proc_name}()", line)
133
+ add_edge(parent_type_nid, proc_nid, "method", line)
134
+ block = _find_child(node, "block")
135
+ if block is not None:
136
+ function_bodies.append((proc_nid, block, parent_type_path))
137
+ return
138
+
139
+ if t in ("proc_definition", "proc_override"):
140
+ tp_node = _find_child(node, "type_path")
141
+ owner_path: "str | None" = None
142
+ owner_nid: "str | None" = None
143
+ if tp_node is not None:
144
+ owner_path = _type_path_text(tp_node)
145
+ owner_nid = _ensure_type(owner_path, line)
146
+ add_edge(file_nid, owner_nid, "contains", line)
147
+ name_node = node.child_by_field_name("name")
148
+ if name_node is None:
149
+ return
150
+ proc_name = _read_text(name_node, source)
151
+ if owner_path and owner_nid:
152
+ proc_nid = _make_id(stem, owner_path, proc_name)
153
+ add_node(proc_nid, f"{owner_path}/{proc_name}()", line)
154
+ add_edge(owner_nid, proc_nid, "method", line)
155
+ else:
156
+ proc_nid = _make_id(stem, proc_name)
157
+ add_node(proc_nid, f"{proc_name}()", line)
158
+ add_edge(file_nid, proc_nid, "contains", line)
159
+ block = _find_child(node, "block")
160
+ if block is not None:
161
+ function_bodies.append((proc_nid, block, owner_path))
162
+ return
163
+
164
+ if t in ("operator_override", "type_operator_override"):
165
+ return
166
+
167
+ for child in node.children:
168
+ walk(child, parent_type_path, parent_type_nid)
169
+
170
+ walk(root)
171
+
172
+ label_to_nids: dict[str, list[str]] = {}
173
+ path_to_nids: dict[str, list[str]] = {}
174
+ for n in nodes:
175
+ label = n["label"].strip("()")
176
+ last = label.rsplit("/", 1)[-1] if "/" in label else label
177
+ if last:
178
+ label_to_nids.setdefault(last.lower(), []).append(n["id"])
179
+ if label.startswith("/"):
180
+ path_to_nids.setdefault(label.lower(), []).append(n["id"])
181
+
182
+ seen_call_pairs: set[tuple[str, str]] = set()
183
+ raw_calls: list[dict] = []
184
+
185
+ def _emit_call(caller_nid: str, callee: str, line: int, is_member: bool) -> None:
186
+ candidates = label_to_nids.get(callee.lower(), [])
187
+ tgt_nid = candidates[0] if len(candidates) == 1 else None
188
+ if tgt_nid and tgt_nid != caller_nid:
189
+ pair = (caller_nid, tgt_nid)
190
+ if pair in seen_call_pairs:
191
+ return
192
+ seen_call_pairs.add(pair)
193
+ edges.append({
194
+ "source": caller_nid, "target": tgt_nid, "relation": "calls",
195
+ "context": "call", "confidence": "EXTRACTED",
196
+ "source_file": str_path, "source_location": f"L{line}", "weight": 1.0,
197
+ })
198
+ else:
199
+ raw_calls.append({
200
+ "caller_nid": caller_nid, "callee": callee,
201
+ "is_member_call": is_member, "source_file": str_path,
202
+ "source_location": f"L{line}",
203
+ })
204
+
205
+ def walk_calls(body_node, caller_nid: str) -> None:
206
+ if body_node is None:
207
+ return
208
+ t = body_node.type
209
+ if t in ("proc_definition", "proc_override", "type_proc_definition",
210
+ "type_proc_override", "type_definition"):
211
+ return
212
+ if t == "call_expression":
213
+ name_node = body_node.child_by_field_name("name")
214
+ if name_node is not None:
215
+ callee = _read_text(name_node, source)
216
+ if callee and callee != "..":
217
+ _emit_call(caller_nid, callee, body_node.start_point[0] + 1,
218
+ is_member=False)
219
+ elif t == "field_proc_expression":
220
+ proc_field = body_node.child_by_field_name("proc")
221
+ if proc_field is not None:
222
+ callee = _read_text(proc_field, source)
223
+ if callee:
224
+ _emit_call(caller_nid, callee, body_node.start_point[0] + 1,
225
+ is_member=True)
226
+ elif t == "new_expression":
227
+ tp_node = _find_child(body_node, "type_path")
228
+ if tp_node is not None:
229
+ target_text = _type_path_text(tp_node)
230
+ candidates = path_to_nids.get(target_text.lower(), [])
231
+ tgt_nid = candidates[0] if len(candidates) == 1 else None
232
+ if tgt_nid and tgt_nid != caller_nid:
233
+ pair = (caller_nid, tgt_nid)
234
+ if pair not in seen_call_pairs:
235
+ seen_call_pairs.add(pair)
236
+ edges.append({
237
+ "source": caller_nid, "target": tgt_nid,
238
+ "relation": "instantiates", "context": "call",
239
+ "confidence": "EXTRACTED", "source_file": str_path,
240
+ "source_location": f"L{body_node.start_point[0] + 1}",
241
+ "weight": 1.0,
242
+ })
243
+ for child in body_node.children:
244
+ walk_calls(child, caller_nid)
245
+
246
+ for proc_nid, block, _owner_path in function_bodies:
247
+ walk_calls(block, proc_nid)
248
+
249
+ return {"nodes": nodes, "edges": edges, "raw_calls": raw_calls}
250
+
251
+ def _read_dmi_description(data: bytes) -> str:
252
+ """Pull the BYOND metadata text out of a .dmi PNG, or empty string on failure."""
253
+ import struct
254
+ import zlib as _zlib
255
+ if not data.startswith(b"\x89PNG\r\n\x1a\n"):
256
+ return ""
257
+ i = 8
258
+ while i + 8 <= len(data):
259
+ length = struct.unpack(">I", data[i:i + 4])[0]
260
+ chunk_type = data[i + 4:i + 8]
261
+ payload = data[i + 8:i + 8 + length]
262
+ if chunk_type in (b"tEXt", b"zTXt"):
263
+ try:
264
+ null = payload.index(b"\x00")
265
+ except ValueError:
266
+ return ""
267
+ keyword = payload[:null]
268
+ if keyword == b"Description":
269
+ if chunk_type == b"zTXt":
270
+ return _zlib.decompressobj().decompress(payload[null + 2:], max_length=1024 * 1024).decode("utf-8", errors="replace")
271
+ return payload[null + 1:].decode("utf-8", errors="replace")
272
+ i += 8 + length + 4
273
+ return ""
274
+
275
+ def extract_dmi(path: Path) -> dict:
276
+ """Extract icon state names from a .dmi (BYOND PNG icon sheet)."""
277
+ try:
278
+ data = path.read_bytes()
279
+ except Exception as e:
280
+ return {"nodes": [], "edges": [], "error": str(e)}
281
+
282
+ str_path = str(path)
283
+ stem = _file_stem(path)
284
+ file_nid = _make_id(str(path))
285
+ nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code",
286
+ "source_file": str_path, "source_location": "L1"}]
287
+ edges: list[dict] = []
288
+ seen: set[str] = {file_nid}
289
+
290
+ description = _read_dmi_description(data)
291
+ if not description:
292
+ return {"nodes": nodes, "edges": edges}
293
+
294
+ line_no = 0
295
+ for raw_line in description.splitlines():
296
+ line_no += 1
297
+ stripped = raw_line.strip()
298
+ if not stripped.startswith("state ="):
299
+ continue
300
+ value = stripped.split("=", 1)[1].strip()
301
+ if value.startswith('"') and value.endswith('"') and len(value) >= 2:
302
+ state_name = value[1:-1]
303
+ else:
304
+ state_name = value
305
+ if not state_name:
306
+ continue
307
+ nid = _make_id(stem, "state", state_name)
308
+ if nid in seen:
309
+ continue
310
+ seen.add(nid)
311
+ nodes.append({"id": nid, "label": f'"{state_name}"', "file_type": "code",
312
+ "source_file": str_path, "source_location": f"L{line_no}"})
313
+ edges.append({"source": file_nid, "target": nid, "relation": "contains",
314
+ "confidence": "EXTRACTED", "source_file": str_path,
315
+ "source_location": f"L{line_no}", "weight": 1.0})
316
+
317
+ return {"nodes": nodes, "edges": edges}
318
+
319
+ _DMM_GRID_RE = re.compile(r"^\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)\s*=", re.MULTILINE)
320
+
321
+ def _split_dmm_tile(body: str) -> list[str]:
322
+ out: list[str] = []
323
+ buf: list[str] = []
324
+ depth = 0
325
+ in_string = False
326
+ escape = False
327
+ for ch in body:
328
+ if escape:
329
+ buf.append(ch)
330
+ escape = False
331
+ continue
332
+ if in_string:
333
+ buf.append(ch)
334
+ if ch == "\\":
335
+ escape = True
336
+ elif ch == '"':
337
+ in_string = False
338
+ continue
339
+ if ch == '"':
340
+ in_string = True
341
+ buf.append(ch)
342
+ elif ch in "({[":
343
+ depth += 1
344
+ buf.append(ch)
345
+ elif ch in ")}]":
346
+ depth -= 1
347
+ buf.append(ch)
348
+ elif ch == "," and depth == 0:
349
+ out.append("".join(buf).strip())
350
+ buf = []
351
+ else:
352
+ buf.append(ch)
353
+ tail = "".join(buf).strip()
354
+ if tail:
355
+ out.append(tail)
356
+ return out
357
+
358
+ def _dmm_type_path(entry: str) -> str:
359
+ brace = entry.find("{")
360
+ if brace != -1:
361
+ entry = entry[:brace]
362
+ return entry.strip()
363
+
364
+ def extract_dmm(path: Path) -> dict:
365
+ """Extract type-path references from a .dmm map file's tile dictionary."""
366
+ try:
367
+ if path.stat().st_size > 50 * 1024 * 1024:
368
+ return {"nodes": [], "edges": [], "error": "file too large (>50 MB)"}
369
+ text = path.read_text(encoding="utf-8", errors="replace")
370
+ except Exception as e:
371
+ return {"nodes": [], "edges": [], "error": str(e)}
372
+
373
+ str_path = str(path)
374
+ file_nid = _make_id(str(path))
375
+ nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code",
376
+ "source_file": str_path, "source_location": "L1"}]
377
+ edges: list[dict] = []
378
+
379
+ grid_match = _DMM_GRID_RE.search(text)
380
+ dict_text = text[:grid_match.start()] if grid_match else text
381
+
382
+ seen_targets: set[str] = set()
383
+ buf: list[str] = []
384
+ open_line = 0
385
+ depth = 0
386
+ in_string = False
387
+ escape = False
388
+ for line_idx, line in enumerate(dict_text.splitlines(), start=1):
389
+ for ch in line:
390
+ if escape:
391
+ escape = False
392
+ elif in_string:
393
+ if ch == "\\":
394
+ escape = True
395
+ elif ch == '"':
396
+ in_string = False
397
+ elif ch == '"':
398
+ in_string = True
399
+ elif ch == "(":
400
+ if depth == 0:
401
+ open_line = line_idx
402
+ depth += 1
403
+ elif ch == ")":
404
+ depth -= 1
405
+ buf.append(ch)
406
+ buf.append("\n")
407
+ if depth == 0 and buf:
408
+ chunk = "".join(buf)
409
+ buf = []
410
+ lp = chunk.find("(")
411
+ rp = chunk.rfind(")")
412
+ if lp == -1 or rp == -1 or rp <= lp:
413
+ continue
414
+ inner = chunk[lp + 1:rp]
415
+ for entry in _split_dmm_tile(inner):
416
+ tpath = _dmm_type_path(entry)
417
+ if not tpath.startswith("/"):
418
+ continue
419
+ tgt = _make_id(tpath)
420
+ if tgt in seen_targets:
421
+ continue
422
+ seen_targets.add(tgt)
423
+ edges.append({"source": file_nid, "target": tgt, "relation": "uses",
424
+ "context": "map", "confidence": "EXTRACTED",
425
+ "source_file": str_path,
426
+ "source_location": f"L{open_line}", "weight": 1.0})
427
+
428
+ return {"nodes": nodes, "edges": edges}
429
+
430
+ _DMF_WINDOW_RE = re.compile(r'^\s*window\s+"([^"]+)"\s*$')
431
+
432
+ _DMF_ELEM_RE = re.compile(r'^\s*elem\s+"([^"]+)"\s*$')
433
+
434
+ _DMF_TYPE_RE = re.compile(r'^\s*type\s*=\s*(\S+)\s*$')
435
+
436
+ def extract_dmf(path: Path) -> dict:
437
+ """Extract windows and controls from a .dmf interface file."""
438
+ try:
439
+ text = path.read_text(encoding="utf-8", errors="replace")
440
+ except Exception as e:
441
+ return {"nodes": [], "edges": [], "error": str(e)}
442
+
443
+ str_path = str(path)
444
+ stem = _file_stem(path)
445
+ file_nid = _make_id(str(path))
446
+ nodes: list[dict] = [{"id": file_nid, "label": path.name, "file_type": "code",
447
+ "source_file": str_path, "source_location": "L1"}]
448
+ edges: list[dict] = []
449
+ seen: set[str] = {file_nid}
450
+
451
+ current_window_nid: str | None = None
452
+ current_elem_nid: str | None = None
453
+ current_elem_name: str | None = None
454
+
455
+ for line_idx, line in enumerate(text.splitlines(), start=1):
456
+ m = _DMF_WINDOW_RE.match(line)
457
+ if m:
458
+ name = m.group(1)
459
+ nid = _make_id(stem, "window", name)
460
+ if nid not in seen:
461
+ seen.add(nid)
462
+ nodes.append({"id": nid, "label": f'window "{name}"', "file_type": "code",
463
+ "source_file": str_path, "source_location": f"L{line_idx}"})
464
+ edges.append({"source": file_nid, "target": nid, "relation": "contains",
465
+ "confidence": "EXTRACTED", "source_file": str_path,
466
+ "source_location": f"L{line_idx}", "weight": 1.0})
467
+ current_window_nid = nid
468
+ current_elem_nid = None
469
+ current_elem_name = None
470
+ continue
471
+ m = _DMF_ELEM_RE.match(line)
472
+ if m and current_window_nid is not None:
473
+ name = m.group(1)
474
+ nid = _make_id(stem, "elem", current_window_nid, name)
475
+ if nid not in seen:
476
+ seen.add(nid)
477
+ nodes.append({"id": nid, "label": f'elem "{name}"', "file_type": "code",
478
+ "source_file": str_path, "source_location": f"L{line_idx}"})
479
+ edges.append({"source": current_window_nid, "target": nid,
480
+ "relation": "contains", "confidence": "EXTRACTED",
481
+ "source_file": str_path, "source_location": f"L{line_idx}",
482
+ "weight": 1.0})
483
+ current_elem_nid = nid
484
+ current_elem_name = name
485
+ continue
486
+ m = _DMF_TYPE_RE.match(line)
487
+ if m and current_elem_nid is not None and current_elem_name is not None:
488
+ ctype = m.group(1)
489
+ for n in nodes:
490
+ if n["id"] == current_elem_nid and " [" not in n["label"]:
491
+ n["label"] = f'elem "{current_elem_name}" [{ctype}]'
492
+ break
493
+
494
+ return {"nodes": nodes, "edges": edges}