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,522 @@
1
+ """Powershell 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_powershell(path: Path) -> dict:
12
+ """Extract functions, classes, methods, and using statements from a .ps1 file."""
13
+ try:
14
+ import tree_sitter_powershell as tsps
15
+ from tree_sitter import Language, Parser
16
+ except ImportError:
17
+ return {"nodes": [], "edges": [], "error": "tree_sitter_powershell not installed"}
18
+
19
+ try:
20
+ language = Language(tsps.language())
21
+ parser = Parser(language)
22
+ source = path.read_bytes()
23
+ tree = parser.parse(source)
24
+ root = tree.root_node
25
+ except Exception as e:
26
+ return {"nodes": [], "edges": [], "error": str(e)}
27
+
28
+ stem = _file_stem(path)
29
+ str_path = str(path)
30
+ nodes: list[dict] = []
31
+ edges: list[dict] = []
32
+ seen_ids: set[str] = set()
33
+ function_bodies: list[tuple[str, Any]] = []
34
+
35
+ def add_node(nid: str, label: str, line: int) -> None:
36
+ if nid not in seen_ids:
37
+ seen_ids.add(nid)
38
+ nodes.append({"id": nid, "label": label, "file_type": "code",
39
+ "source_file": str_path, "source_location": f"L{line}"})
40
+
41
+ def add_edge(src: str, tgt: str, relation: str, line: int,
42
+ confidence: str = "EXTRACTED", weight: float = 1.0,
43
+ context: str | None = None) -> None:
44
+ edge = {"source": src, "target": tgt, "relation": relation,
45
+ "confidence": confidence, "source_file": str_path,
46
+ "source_location": f"L{line}", "weight": weight}
47
+ if context:
48
+ edge["context"] = context
49
+ edges.append(edge)
50
+
51
+ file_nid = _make_id(str(path))
52
+ add_node(file_nid, path.name, 1)
53
+
54
+ _PS_SKIP = frozenset({
55
+ "using", "return", "if", "else", "elseif", "foreach", "for",
56
+ "while", "do", "switch", "try", "catch", "finally", "throw",
57
+ "break", "continue", "exit", "param", "begin", "process", "end",
58
+ # Import commands — handled as import edges, not function calls
59
+ "import-module",
60
+ })
61
+
62
+ def _find_script_block_body(node):
63
+ for child in node.children:
64
+ if child.type == "script_block":
65
+ for sc in child.children:
66
+ if sc.type == "script_block_body":
67
+ return sc
68
+ return child
69
+ return None
70
+
71
+ def ensure_named_node(name: str, line: int) -> str:
72
+ nid = _make_id(stem, name)
73
+ if nid in seen_ids:
74
+ return nid
75
+ nid = _make_id(name)
76
+ if nid not in seen_ids:
77
+ # The name isn't defined in this file, so this is a cross-file reference
78
+ # (e.g. a `Thing` type annotation imported from another module). Emit a
79
+ # SOURCELESS stub — like the inheritance-base path below — so the
80
+ # corpus-level rewire can collapse it onto the real definition. A sourced
81
+ # stub here makes _disambiguate_colliding_node_ids bake the referencing
82
+ # file's path (with extension) into the id and blocks the rewire, which is
83
+ # the phantom-duplicate-node bug (#1402).
84
+ seen_ids.add(nid)
85
+ nodes.append({
86
+ "id": nid,
87
+ "label": name,
88
+ "file_type": "code",
89
+ "source_file": "",
90
+ "source_location": "",
91
+ "origin_file": str_path,
92
+ })
93
+ return nid
94
+
95
+ def _ps_type_name(type_literal_node) -> str | None:
96
+ """Drill into a type_literal node and return the inner type_identifier text."""
97
+ if type_literal_node is None:
98
+ return None
99
+ for spec in type_literal_node.children:
100
+ if spec.type != "type_spec":
101
+ continue
102
+ for tname in spec.children:
103
+ if tname.type != "type_name":
104
+ continue
105
+ for tid in tname.children:
106
+ if tid.type == "type_identifier":
107
+ return _read_text(tid, source)
108
+ return None
109
+
110
+ def walk(node, parent_class_nid: str | None = None) -> None:
111
+ t = node.type
112
+
113
+ if t == "function_statement":
114
+ name_node = next((c for c in node.children if c.type == "function_name"), None)
115
+ if name_node:
116
+ func_name = _read_text(name_node, source)
117
+ line = node.start_point[0] + 1
118
+ func_nid = _make_id(stem, func_name)
119
+ add_node(func_nid, f"{func_name}()", line)
120
+ add_edge(file_nid, func_nid, "contains", line)
121
+ body = _find_script_block_body(node)
122
+ if body:
123
+ function_bodies.append((func_nid, body))
124
+ # Also walk the body during the main pass so that
125
+ # Import-Module / dot-source inside functions emit
126
+ # file-level imports_from edges (#1331).
127
+ walk(body, parent_class_nid)
128
+ return
129
+
130
+ if t == "enum_statement":
131
+ # PowerShell enums are first-class type definitions (referenced via
132
+ # `[Color]` type literals) but were not extracted at all, so those
133
+ # references resolved to a sourceless phantom stub instead of the real
134
+ # in-file definition — and the enum's members were lost entirely.
135
+ name_node = next((c for c in node.children if c.type == "simple_name"), None)
136
+ if name_node:
137
+ enum_name = _read_text(name_node, source)
138
+ line = node.start_point[0] + 1
139
+ enum_nid = _make_id(stem, enum_name)
140
+ add_node(enum_nid, enum_name, line)
141
+ add_edge(file_nid, enum_nid, "contains", line)
142
+ for member in node.children:
143
+ if member.type != "enum_member":
144
+ continue
145
+ mname_node = next(
146
+ (c for c in member.children if c.type == "simple_name"), None)
147
+ if mname_node is None:
148
+ continue
149
+ member_name = _read_text(mname_node, source)
150
+ m_line = member.start_point[0] + 1
151
+ member_nid = _make_id(enum_nid, member_name)
152
+ add_node(member_nid, member_name, m_line)
153
+ add_edge(enum_nid, member_nid, "contains", m_line)
154
+ return
155
+
156
+ if t == "class_statement":
157
+ name_node = next((c for c in node.children if c.type == "simple_name"), None)
158
+ if name_node:
159
+ class_name = _read_text(name_node, source)
160
+ line = node.start_point[0] + 1
161
+ class_nid = _make_id(stem, class_name)
162
+ add_node(class_nid, class_name, line)
163
+ add_edge(file_nid, class_nid, "contains", line)
164
+ # Base type(s) after ':'. PowerShell has no syntactic base vs
165
+ # interface split, so (matching the C# convention) treat the
166
+ # first base as the superclass (inherits) and the rest as
167
+ # interfaces (implements). Bases are the simple_name children
168
+ # after the ':' token.
169
+ colon_seen = False
170
+ base_index = 0
171
+ for child in node.children:
172
+ if child.type == ":":
173
+ colon_seen = True
174
+ elif colon_seen and child.type == "simple_name":
175
+ base_nid = ensure_named_node(_read_text(child, source), line)
176
+ if base_nid != class_nid:
177
+ rel = "inherits" if base_index == 0 else "implements"
178
+ add_edge(class_nid, base_nid, rel, line)
179
+ base_index += 1
180
+ for child in node.children:
181
+ walk(child, parent_class_nid=class_nid)
182
+ return
183
+
184
+ if t == "class_property_definition" and parent_class_nid:
185
+ type_literal = next((c for c in node.children if c.type == "type_literal"), None)
186
+ type_name = _ps_type_name(type_literal)
187
+ if type_name:
188
+ line = node.start_point[0] + 1
189
+ target_nid = ensure_named_node(type_name, line)
190
+ if target_nid != parent_class_nid:
191
+ add_edge(parent_class_nid, target_nid, "references",
192
+ line, context="field")
193
+ return
194
+
195
+ if t == "class_method_definition":
196
+ name_node = next((c for c in node.children if c.type == "simple_name"), None)
197
+ if name_node:
198
+ method_name = _read_text(name_node, source)
199
+ line = node.start_point[0] + 1
200
+ if parent_class_nid:
201
+ method_nid = _make_id(parent_class_nid, method_name)
202
+ add_node(method_nid, f".{method_name}()", line)
203
+ add_edge(parent_class_nid, method_nid, "method", line)
204
+ else:
205
+ method_nid = _make_id(stem, method_name)
206
+ add_node(method_nid, f"{method_name}()", line)
207
+ add_edge(file_nid, method_nid, "contains", line)
208
+ # Return type: type_literal sibling of simple_name
209
+ return_type_literal = next(
210
+ (c for c in node.children if c.type == "type_literal"), None)
211
+ return_type_name = _ps_type_name(return_type_literal)
212
+ if return_type_name:
213
+ target_nid = ensure_named_node(return_type_name, line)
214
+ if target_nid != method_nid:
215
+ add_edge(method_nid, target_nid, "references",
216
+ line, context="return_type")
217
+ # Parameter types: class_method_parameter_list
218
+ param_list = next(
219
+ (c for c in node.children if c.type == "class_method_parameter_list"), None)
220
+ if param_list is not None:
221
+ for p in param_list.children:
222
+ if p.type != "class_method_parameter":
223
+ continue
224
+ ptype_literal = next(
225
+ (c for c in p.children if c.type == "type_literal"), None)
226
+ ptype_name = _ps_type_name(ptype_literal)
227
+ if not ptype_name:
228
+ continue
229
+ p_line = p.start_point[0] + 1
230
+ target_nid = ensure_named_node(ptype_name, p_line)
231
+ if target_nid != method_nid:
232
+ add_edge(method_nid, target_nid, "references",
233
+ p_line, context="parameter_type")
234
+ body = _find_script_block_body(node)
235
+ if body:
236
+ function_bodies.append((method_nid, body))
237
+ return
238
+
239
+ if t == "command":
240
+ # Dot-sourcing: `. ./Shared.psm1`
241
+ # Uses command_invokation_operator '.' + command_name_expr (not command_name)
242
+ invoke_op = next(
243
+ (c for c in node.children if c.type == "command_invokation_operator"), None
244
+ )
245
+ if invoke_op is not None and _read_text(invoke_op, source).strip() == ".":
246
+ name_expr = next(
247
+ (c for c in node.children if c.type == "command_name_expr"), None
248
+ )
249
+ if name_expr is not None:
250
+ name_node = next(
251
+ (c for c in name_expr.children if c.type == "command_name"), None
252
+ )
253
+ if name_node:
254
+ raw_path = _read_text(name_node, source)
255
+ # Strip relative path prefix (./ or .\ or just the dot)
256
+ module_stem = re.sub(r'^[./\\]+', '', raw_path)
257
+ # Drop extension to get bare module name
258
+ module_stem = re.sub(r'\.[^.]+$', '', module_stem).replace('\\', '/')
259
+ module_name = module_stem.split('/')[-1]
260
+ if module_name:
261
+ add_edge(file_nid, _make_id(module_name), "imports_from",
262
+ node.start_point[0] + 1)
263
+ return
264
+
265
+ cmd_name_node = next((c for c in node.children if c.type == "command_name"), None)
266
+ if cmd_name_node:
267
+ cmd_text = _read_text(cmd_name_node, source).lower()
268
+ if cmd_text == "using":
269
+ tokens = []
270
+ for child in node.children:
271
+ if child.type == "command_elements":
272
+ for el in child.children:
273
+ if el.type == "generic_token":
274
+ tokens.append(_read_text(el, source))
275
+ module_tokens = [t for t in tokens
276
+ if t.lower() not in ("namespace", "module", "assembly")]
277
+ if module_tokens:
278
+ module_name = module_tokens[-1].split(".")[-1]
279
+ add_edge(file_nid, _make_id(module_name), "imports_from",
280
+ node.start_point[0] + 1)
281
+ elif cmd_text == "import-module":
282
+ # Collect generic_token args; skip command_parameter flags like -Name
283
+ # The module name is the first generic_token (or the one after -Name)
284
+ module_name: str | None = None
285
+ expect_name = False
286
+ for child in node.children:
287
+ if child.type != "command_elements":
288
+ continue
289
+ for el in child.children:
290
+ if el.type == "command_parameter":
291
+ param_text = _read_text(el, source).lstrip("-").lower()
292
+ expect_name = param_text in ("name", "n")
293
+ elif el.type == "generic_token":
294
+ token = _read_text(el, source)
295
+ if module_name is None or expect_name:
296
+ module_name = token
297
+ expect_name = False
298
+ if module_name:
299
+ # Strip extension; keep only the stem for the node ID
300
+ bare = re.sub(r'\.[^.]+$', '', module_name).split('/')[-1].split('\\')[-1]
301
+ if bare:
302
+ add_edge(file_nid, _make_id(bare), "imports_from",
303
+ node.start_point[0] + 1)
304
+ return
305
+
306
+ for child in node.children:
307
+ walk(child, parent_class_nid)
308
+
309
+ walk(root)
310
+
311
+ label_to_nid = {n["label"].strip("()").lstrip(".").lower(): n["id"] for n in nodes}
312
+ seen_call_pairs: set[tuple[str, str]] = set()
313
+ raw_calls: list[dict] = []
314
+
315
+ def walk_calls(node, caller_nid: str) -> None:
316
+ if node.type in ("function_statement", "class_statement"):
317
+ return
318
+ if node.type == "command":
319
+ cmd_name_node = next((c for c in node.children if c.type == "command_name"), None)
320
+ if cmd_name_node:
321
+ cmd_text = _read_text(cmd_name_node, source)
322
+ if cmd_text.lower() not in _PS_SKIP:
323
+ tgt_nid = label_to_nid.get(cmd_text.lower())
324
+ if tgt_nid and tgt_nid != caller_nid:
325
+ pair = (caller_nid, tgt_nid)
326
+ if pair not in seen_call_pairs:
327
+ seen_call_pairs.add(pair)
328
+ add_edge(caller_nid, tgt_nid, "calls",
329
+ node.start_point[0] + 1,
330
+ confidence="EXTRACTED", weight=1.0)
331
+ elif cmd_text:
332
+ raw_calls.append({
333
+ "caller_nid": caller_nid,
334
+ "callee": cmd_text,
335
+ "is_member_call": False,
336
+ "source_file": str_path,
337
+ "source_location": f"L{node.start_point[0] + 1}",
338
+ })
339
+ for child in node.children:
340
+ walk_calls(child, caller_nid)
341
+
342
+ for caller_nid, body_node in function_bodies:
343
+ walk_calls(body_node, caller_nid)
344
+
345
+ clean_edges = [e for e in edges if e["source"] in seen_ids and
346
+ (e["target"] in seen_ids or e["relation"] in ("imports_from", "imports"))]
347
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
348
+
349
+ _PSD1_IMPORT_KEYS = frozenset({"RootModule", "NestedModules", "RequiredModules"})
350
+
351
+ def _psd1_collect_string_literals(node, source: bytes) -> list[str]:
352
+ """Recursively collect all string_literal text values under *node*."""
353
+ results: list[str] = []
354
+
355
+ def _walk(n) -> None:
356
+ if n.type == "string_literal":
357
+ raw = source[n.start_byte:n.end_byte].decode(errors="replace")
358
+ # Strip surrounding quote chars (' or ")
359
+ results.append(raw.strip("'\""))
360
+ return
361
+ for child in n.children:
362
+ _walk(child)
363
+
364
+ _walk(node)
365
+ return results
366
+
367
+ def _psd1_module_name(raw: str) -> str:
368
+ """Derive a bare module name from a raw string value.
369
+
370
+ e.g. 'MyModule.psm1' → 'MyModule', './sub/Util.psm1' → 'Util', 'PSReadLine' → 'PSReadLine'
371
+ """
372
+ # Strip path prefix and extension
373
+ name = raw.replace("\\", "/").split("/")[-1]
374
+ name = re.sub(r"\.[^.]+$", "", name) # remove last extension
375
+ return name.strip()
376
+
377
+ def extract_powershell_manifest(path: Path) -> dict:
378
+ """Extract module dependency edges from a PowerShell .psd1 manifest file.
379
+
380
+ .psd1 files are PowerShell data hashtables, not scripts. tree-sitter-powershell
381
+ parses them correctly (they are syntactically valid PS). We walk the AST looking
382
+ for RootModule, NestedModules, and RequiredModules keys and emit imports_from
383
+ edges for every referenced module.
384
+
385
+ RequiredModules supports two forms:
386
+ - Simple string: 'PSReadLine'
387
+ - Module specification: @{ ModuleName = 'Pester'; ModuleVersion = '5.0' }
388
+ For the hashtable form we only follow the ModuleName key.
389
+ """
390
+ try:
391
+ import tree_sitter_powershell as tsps
392
+ from tree_sitter import Language, Parser
393
+ except ImportError:
394
+ return {"nodes": [], "edges": [], "error": "tree_sitter_powershell not installed"}
395
+
396
+ try:
397
+ language = Language(tsps.language())
398
+ parser = Parser(language)
399
+ source = path.read_bytes()
400
+ tree = parser.parse(source)
401
+ root = tree.root_node
402
+ except Exception as e:
403
+ return {"nodes": [], "edges": [], "error": str(e)}
404
+
405
+ str_path = str(path)
406
+ nodes: list[dict] = []
407
+ edges: list[dict] = []
408
+ seen_ids: set[str] = set()
409
+
410
+ def add_node(nid: str, label: str, line: int) -> None:
411
+ if nid not in seen_ids:
412
+ seen_ids.add(nid)
413
+ nodes.append({"id": nid, "label": label, "file_type": "code",
414
+ "source_file": str_path, "source_location": f"L{line}"})
415
+
416
+ def add_import_edge(src: str, module_raw: str, line: int) -> None:
417
+ name = _psd1_module_name(module_raw)
418
+ if not name:
419
+ return
420
+ tgt_nid = _make_id(name)
421
+ edges.append({
422
+ "source": src,
423
+ "target": tgt_nid,
424
+ "relation": "imports_from",
425
+ "confidence": "EXTRACTED",
426
+ "source_file": str_path,
427
+ "source_location": f"L{line}",
428
+ "weight": 1.0,
429
+ "context": "import",
430
+ })
431
+
432
+ file_nid = _make_id(str(path))
433
+ add_node(file_nid, path.name, 1)
434
+
435
+ def walk_manifest(node) -> None:
436
+ """Walk the AST and emit edges for import-relevant hash_entry nodes."""
437
+ if node.type != "hash_entry":
438
+ for child in node.children:
439
+ walk_manifest(child)
440
+ return
441
+
442
+ # Identify the key
443
+ key_node = next((c for c in node.children if c.type == "key_expression"), None)
444
+ if key_node is None:
445
+ return
446
+ key_text = source[key_node.start_byte:key_node.end_byte].decode(errors="replace").strip()
447
+
448
+ if key_text not in _PSD1_IMPORT_KEYS:
449
+ # Still recurse in case there are nested hashes (e.g. ModuleVersion entries
450
+ # contain sub-hashes, but we only care about top-level keys for imports)
451
+ return
452
+
453
+ line = node.start_point[0] + 1
454
+ value_node = next((c for c in node.children if c.type == "pipeline"), None)
455
+ if value_node is None:
456
+ return
457
+
458
+ if key_text == "RootModule":
459
+ # Value is a single string
460
+ strings = _psd1_collect_string_literals(value_node, source)
461
+ for s in strings:
462
+ add_import_edge(file_nid, s, line)
463
+
464
+ elif key_text == "NestedModules":
465
+ # Value is a string or @('a', 'b', ...) array — collect all string literals
466
+ strings = _psd1_collect_string_literals(value_node, source)
467
+ for s in strings:
468
+ add_import_edge(file_nid, s, line)
469
+
470
+ elif key_text == "RequiredModules":
471
+ # Two forms:
472
+ # 1) 'SimpleModule' — direct string literals in the array
473
+ # 2) @{ ModuleName = 'Foo'; ModuleVersion = '2.0' } — use ModuleName only
474
+ #
475
+ # Strategy: walk the value for hash_entry nodes whose key is 'ModuleName';
476
+ # collect their string values. For the remaining string_literal nodes that
477
+ # are NOT inside a hash_entry subtree, treat them as simple module names.
478
+ module_name_strings: list[str] = []
479
+ inside_hash_entries: set[int] = set() # byte offsets of handled strings
480
+
481
+ def find_modulename_entries(n) -> None:
482
+ if n.type == "hash_entry":
483
+ sub_key = next((c for c in n.children if c.type == "key_expression"), None)
484
+ if sub_key is not None:
485
+ sk_text = source[sub_key.start_byte:sub_key.end_byte].decode(errors="replace").strip()
486
+ # Collect strings inside *all* sub-keys so we can exclude them
487
+ for c in n.children:
488
+ if c.type == "pipeline":
489
+ for s_node in _collect_string_nodes(c):
490
+ inside_hash_entries.add(s_node.start_byte)
491
+ if sk_text == "ModuleName":
492
+ for c in n.children:
493
+ if c.type == "pipeline":
494
+ for s in _psd1_collect_string_literals(c, source):
495
+ module_name_strings.append(s)
496
+ return # don't recurse further into this hash_entry
497
+ for child in n.children:
498
+ find_modulename_entries(child)
499
+
500
+ def _collect_string_nodes(n):
501
+ """Return all string_literal nodes in subtree."""
502
+ if n.type == "string_literal":
503
+ yield n
504
+ return
505
+ for child in n.children:
506
+ yield from _collect_string_nodes(child)
507
+
508
+ find_modulename_entries(value_node)
509
+
510
+ # Now gather direct string literals not inside hash entries
511
+ direct_strings: list[str] = []
512
+ for s_node in _collect_string_nodes(value_node):
513
+ if s_node.start_byte not in inside_hash_entries:
514
+ raw = source[s_node.start_byte:s_node.end_byte].decode(errors="replace")
515
+ direct_strings.append(raw.strip("'\""))
516
+
517
+ for s in direct_strings + module_name_strings:
518
+ add_import_edge(file_nid, s, line)
519
+
520
+ walk_manifest(root)
521
+
522
+ return {"nodes": nodes, "edges": edges, "raw_calls": []}