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,408 @@
1
+ """Markdown extractor. Moved verbatim from graphify/extract.py."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import os
6
+ import unicodedata
7
+
8
+ from pathlib import Path
9
+ from graphify.extractors.base import _file_stem, _make_id
10
+ from graphify.security import sanitize_metadata
11
+
12
+
13
+ _MD_INLINE_LINK_RE = re.compile(r'(?<!\!)\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+[^)]*)?\)')
14
+
15
+ _MD_REF_DEF_RE = re.compile(r'^\s{0,3}\[[^\]]+\]:\s*<?([^\s>]+)>?')
16
+
17
+ _MD_WIKILINK_RE = re.compile(r'(?<!\!)\[\[([^\]|#]+)(?:[#|][^\]]*)?\]\]')
18
+
19
+ _MD_LINKABLE_EXTS = {".md", ".mdx", ".qmd", ".markdown", ".rst", ".txt"}
20
+
21
+ # A YAML frontmatter block is only frontmatter when the opening `---` is the
22
+ # very first line of the file. A `---` further down is a horizontal rule and
23
+ # must not be mistaken for one. Bounded so a file that opens a fence and never
24
+ # closes it cannot swallow the whole document.
25
+ _MD_FRONTMATTER_CLOSE = ("---", "...")
26
+ _MD_FRONTMATTER_MAX_LINES = 200
27
+
28
+ # Flat `key: value` fallback, used only when PyYAML is unavailable. PyYAML is
29
+ # not a declared dependency of graphify (see manifest_ingest._parse_apm for the
30
+ # same defensive-import pattern), so the extractor must degrade rather than
31
+ # fail.
32
+ _MD_FM_SCALAR_RE = re.compile(r'^([A-Za-z0-9_][A-Za-z0-9_\-. ]*):\s*(.*)$')
33
+
34
+
35
+ def _split_frontmatter(lines: list[str]) -> tuple[list[str], int]:
36
+ """Split leading YAML frontmatter off *lines*.
37
+
38
+ Returns ``(frontmatter_lines, body_start_index)``. When the file has no
39
+ frontmatter — the common case — returns ``([], 0)`` and the caller parses
40
+ from line 0 exactly as before.
41
+ """
42
+ if not lines or lines[0].strip() != "---":
43
+ return [], 0
44
+ limit = min(len(lines), _MD_FRONTMATTER_MAX_LINES + 1)
45
+ for i in range(1, limit):
46
+ if lines[i].strip() in _MD_FRONTMATTER_CLOSE:
47
+ return lines[1:i], i + 1
48
+ # Unterminated fence: treat the `---` as ordinary content, not frontmatter.
49
+ return [], 0
50
+
51
+
52
+ def _parse_frontmatter(fm_lines: list[str]) -> dict:
53
+ """Parse frontmatter lines into a plain dict.
54
+
55
+ Values are passed through ``sanitize_metadata`` by the caller, so nested
56
+ dicts and lists survive (a review workflow's ``coherence_check:`` block,
57
+ Obsidian ``aliases:``) while staying bounded and HTML-safe.
58
+ """
59
+ if not fm_lines:
60
+ return {}
61
+ text = "\n".join(fm_lines)
62
+ try:
63
+ import yaml
64
+ except ImportError:
65
+ return _parse_frontmatter_fallback(fm_lines)
66
+ try:
67
+ data = yaml.safe_load(text)
68
+ except Exception:
69
+ # Malformed YAML in one document must not fail the whole extraction.
70
+ return _parse_frontmatter_fallback(fm_lines)
71
+ return data if isinstance(data, dict) else {}
72
+
73
+
74
+ def _parse_frontmatter_fallback(fm_lines: list[str]) -> dict:
75
+ """Flat `key: value` parser for when PyYAML is not installed.
76
+
77
+ Nested blocks and list items are skipped rather than guessed at; the keys
78
+ that matter for graph filtering (``type``, ``review_status``, ``title``)
79
+ are flat scalars in practice.
80
+ """
81
+ out: dict = {}
82
+ for raw in fm_lines:
83
+ if not raw[:1].strip():
84
+ continue # indented -> belongs to a nested block, skip
85
+ m = _MD_FM_SCALAR_RE.match(raw.strip())
86
+ if not m:
87
+ continue
88
+ key, value = m.group(1).strip(), m.group(2).strip()
89
+ if not value:
90
+ continue # a bare `key:` opens a nested block
91
+ out[key] = value.strip('"\'')
92
+ return out
93
+
94
+ # Vault-wide wikilink fallback. Obsidian-style vaults resolve a [[wikilink]]
95
+ # by vault-global filename lookup, not relative to the linking file, so a note
96
+ # in a subfolder linking [[hub]] reaches a root-level hub.md. The lexical
97
+ # sibling resolution below is correct when the target exists beside the
98
+ # source; when it does not, the reference edge kept an id derived from a
99
+ # nonexistent path, matched no node, and silently dropped at build time — the
100
+ # same lost-edge failure #1376 fixed for hub docs, reintroduced for every
101
+ # cross-folder vault link. The fallback consults a per-scan-root index of
102
+ # document files and only fires when the lexically resolved path does not
103
+ # exist, so non-vault corpora and resolvable relative links are untouched.
104
+ #
105
+ # Keyed by resolved scan root. extract() clears it at the start of each run
106
+ # (beside _WORKSPACE_PACKAGE_CACHE) so a serial rerun in one process sees
107
+ # files created since the last run; parallel workers are fresh processes.
108
+ _MD_LINK_INDEX_CACHE: "dict[str, dict[str, list[tuple[int, str, Path]]]]" = {}
109
+
110
+
111
+ def _active_scan_root() -> "Path | None":
112
+ """The scan root of the extraction in flight, or None outside extract().
113
+
114
+ _safe_extract_with_xaml_root sets this for every extraction despite its
115
+ XAML-era name; a direct extract_markdown() call has no root and the
116
+ fallback stays off.
117
+ """
118
+ import graphify.extract as _extract
119
+ return getattr(_extract, "_XAML_ACTIVE_EXTRACT_ROOT", None)
120
+
121
+
122
+ def _nfc(s: str) -> str:
123
+ # Filesystems disagree on Unicode normalization (macOS decomposes, others
124
+ # do not); a link typed in NFC must still find a file listed in NFD.
125
+ return unicodedata.normalize("NFC", s)
126
+
127
+
128
+ def _build_link_index(root: Path) -> "dict[str, list[tuple[int, str, Path]]]":
129
+ """Index every linkable document under *root* by NFC-normalized basename.
130
+
131
+ Each entry maps basename -> [(depth, root-relative posix path, absolute
132
+ path)]. Directories in detect._SKIP_DIRS and dot-directories are pruned —
133
+ the same corpus boundary the scanner draws, and Obsidian itself does not
134
+ index dot-folders.
135
+ """
136
+ from graphify.detect import _SKIP_DIRS
137
+ index: dict[str, list[tuple[int, str, Path]]] = {}
138
+ for dirpath, dirnames, filenames in os.walk(root):
139
+ dirnames[:] = sorted(
140
+ d for d in dirnames if not d.startswith(".") and d not in _SKIP_DIRS
141
+ )
142
+ for fname in filenames:
143
+ if Path(fname).suffix.lower() not in _MD_LINKABLE_EXTS:
144
+ continue
145
+ abs_path = Path(dirpath) / fname
146
+ rel = os.path.relpath(str(abs_path), str(root)).replace("\\", "/")
147
+ index.setdefault(_nfc(fname), []).append(
148
+ (rel.count("/"), _nfc(rel), abs_path)
149
+ )
150
+ return index
151
+
152
+
153
+ def _vault_lookup(target: str, root: Path) -> "Path | None":
154
+ """Resolve *target* (a normalized wikilink path, `.md` already appended)
155
+ against the corpus under *root*, or None when nothing matches.
156
+
157
+ A bare name matches by basename; a path-qualified target
158
+ (``folder/name.md``) must match its full segment suffix. Ties break to the
159
+ shallowest match, then lexicographically — mirroring Obsidian, where a
160
+ root-level file wins a bare-link name collision — so resolution is
161
+ deterministic regardless of walk order.
162
+ """
163
+ root_key = str(root)
164
+ index = _MD_LINK_INDEX_CACHE.get(root_key)
165
+ if index is None:
166
+ try:
167
+ index = _build_link_index(root)
168
+ except OSError:
169
+ index = {}
170
+ _MD_LINK_INDEX_CACHE[root_key] = index
171
+ parts = _nfc(target.replace("\\", "/")).split("/")
172
+ candidates = index.get(parts[-1])
173
+ if not candidates:
174
+ return None
175
+ suffix = "/".join(parts)
176
+ matches = [
177
+ c for c in candidates
178
+ if c[1] == suffix or c[1].endswith("/" + suffix)
179
+ ]
180
+ if not matches:
181
+ return None
182
+ return min(matches)[2]
183
+
184
+
185
+ def _resolve_markdown_link(raw: str, source_dir: Path,
186
+ wikilink: bool = False) -> "Path | None":
187
+ """Resolve a markdown link target to the absolute path of a sibling document.
188
+
189
+ Returns the resolved (normalized, not necessarily existing) path when the
190
+ target is a *local* relative/absolute file-path link to a document, or None
191
+ when it should be skipped: external URLs (http/https/mailto/protocol-
192
+ relative/data), pure in-page anchors (``#section``), and links to non-doc
193
+ file types (code/assets are handled by their own extractors).
194
+
195
+ The anchor fragment (``#section``) and query (``?x=1``) are stripped before
196
+ resolution so ``./repo.md#setup`` resolves to the same node as ``./repo.md``.
197
+ Extension-less targets (typical of wikilinks) are treated as sibling ``.md``.
198
+
199
+ With ``wikilink=True``, a target whose lexically resolved path does not
200
+ exist is retried as a vault-global lookup across the active scan root (see
201
+ _vault_lookup) — Obsidian's own resolution order for wikilinks. Inline and
202
+ reference-style links keep pure relative semantics: for them a missing
203
+ relative target is an authoring error, not an alternate link convention.
204
+ """
205
+ target = raw.strip()
206
+ if not target:
207
+ return None
208
+ # Drop anchor / query so #section links still resolve to the target doc.
209
+ target = target.split("#", 1)[0].split("?", 1)[0].strip()
210
+ if not target:
211
+ return None
212
+ low = target.lower()
213
+ if "://" in target or low.startswith(("mailto:", "tel:", "//", "data:")):
214
+ return None
215
+ suffix = Path(target).suffix.lower()
216
+ if suffix == "":
217
+ target = target + ".md"
218
+ suffix = ".md"
219
+ if suffix not in _MD_LINKABLE_EXTS:
220
+ return None
221
+ candidate = Path(target)
222
+ if not candidate.is_absolute():
223
+ candidate = source_dir / candidate
224
+ resolved = Path(os.path.normpath(str(candidate)))
225
+ if wikilink and not Path(target).is_absolute():
226
+ try:
227
+ missing = not resolved.is_file()
228
+ except OSError:
229
+ missing = False
230
+ if missing:
231
+ scan_root = _active_scan_root()
232
+ if scan_root is not None:
233
+ hit = _vault_lookup(target, scan_root)
234
+ if hit is not None:
235
+ return Path(os.path.normpath(str(hit)))
236
+ return resolved
237
+
238
+ def extract_markdown(path: Path) -> dict:
239
+ """Extract structural nodes and edges from a Markdown file.
240
+
241
+ Produces nodes for:
242
+ - The file itself, tagged ``node_kind: "page"``, carrying any YAML
243
+ frontmatter under ``frontmatter``
244
+ - Each heading (# / ## / ### etc.), tagged ``node_kind: "heading"``
245
+
246
+ ``node_kind`` exists because ``file_type`` cannot carry this distinction:
247
+ it is a closed enum (build.py rewrites anything outside
248
+ ``code|document|paper|image|rationale|concept`` to ``"concept"``) and
249
+ ``"document"`` on both endpoints is load-bearing for the twin-merge pass.
250
+ Without a separate field, headings — typically the large majority of nodes
251
+ in a docs-heavy corpus — cannot be filtered out by a consumer.
252
+
253
+ Produces edges for:
254
+ - file --contains--> heading
255
+ - parent heading --contains--> child heading (nesting by level)
256
+ - heading --references--> other node (when backtick `Name` matches a known pattern)
257
+ - file --references--> linked document, for inline ``[text](./other.md)``,
258
+ reference-style ``[label]: ./other.md`` and ``[[wikilink]]`` links, so a
259
+ hub doc (``index.md`` / ``table-of-contents.md``) becomes a real hub node
260
+ instead of an under-connected orphan (#1376). The target node ID is built
261
+ from the resolved target path with the same recipe as the target file's
262
+ own node, so the edge merges into that node (no ghost node). External
263
+ URLs, in-page anchors, images and non-document targets are skipped.
264
+
265
+ Fenced code blocks (``` ... ```) are skipped during parsing so their
266
+ contents don't get treated as headings, but no node is emitted for
267
+ them — they were always orphans (only a single contains edge to the
268
+ parent doc) and inflated the disconnected-component count (#1077).
269
+
270
+ Leading YAML frontmatter is parsed onto the page node and excluded from
271
+ heading detection (a `#` there is a YAML comment). Links inside it are
272
+ still followed: review workflows keep wikilinks in frontmatter and those
273
+ are genuine references.
274
+
275
+ No tree-sitter dependency — pure line-by-line parsing.
276
+ """
277
+ try:
278
+ source = path.read_text(encoding="utf-8", errors="replace")
279
+ except Exception as e:
280
+ return {"nodes": [], "edges": [], "error": str(e)}
281
+
282
+ stem = _file_stem(path)
283
+ str_path = str(path)
284
+ nodes: list[dict] = []
285
+ edges: list[dict] = []
286
+ seen_ids: set[str] = set()
287
+
288
+ def add_node(nid: str, label: str, line: int, file_type: str = "document",
289
+ node_kind: str = "heading", extra: "dict | None" = None) -> None:
290
+ if nid not in seen_ids:
291
+ seen_ids.add(nid)
292
+ node = {"id": nid, "label": label, "file_type": file_type,
293
+ "node_kind": node_kind,
294
+ "source_file": str_path, "source_location": f"L{line}"}
295
+ if extra:
296
+ node.update(extra)
297
+ nodes.append(node)
298
+
299
+ def add_edge(src: str, tgt: str, relation: str, line: int,
300
+ confidence: str = "EXTRACTED", weight: float = 1.0,
301
+ target_file: "str | None" = None) -> None:
302
+ edge = {"source": src, "target": tgt, "relation": relation,
303
+ "confidence": confidence, "source_file": str_path,
304
+ "source_location": f"L{line}", "weight": weight}
305
+ if target_file is not None:
306
+ edge["target_file"] = target_file
307
+ edges.append(edge)
308
+
309
+ lines = source.splitlines()
310
+ fm_lines, body_start = _split_frontmatter(lines)
311
+ frontmatter = sanitize_metadata(_parse_frontmatter(fm_lines))
312
+
313
+ file_nid = _make_id(str(path))
314
+ add_node(file_nid, path.name, 1, node_kind="page",
315
+ extra={"frontmatter": frontmatter} if frontmatter else None)
316
+
317
+ source_dir = path.parent
318
+ # Dedup link edges by resolved target node so a hub doc that links to the
319
+ # same sibling many times yields one edge, not N (keeps weights meaningful).
320
+ linked_targets: set[str] = set()
321
+
322
+ def add_link(raw: str, line: int, wikilink: bool = False) -> None:
323
+ resolved = _resolve_markdown_link(raw, source_dir, wikilink=wikilink)
324
+ if resolved is None:
325
+ return
326
+ # Build the target ID with the SAME recipe as the target file's own
327
+ # node (_make_id(str(path)) at extract time, canonicalized to
328
+ # _file_node_id(rel) by the extract() post-pass). Using the absolute
329
+ # resolved path means both endpoints get remapped identically, so the
330
+ # edge merges into the existing doc node instead of spawning a ghost.
331
+ tgt_nid = _make_id(str(resolved))
332
+ if tgt_nid == file_nid or tgt_nid in linked_targets:
333
+ return
334
+ linked_targets.add(tgt_nid)
335
+ # Stamp the resolved target file (mirroring the JS/Python import
336
+ # stamps, #1814/#2213) so the #2169 remap pass can canonicalize this
337
+ # edge's target on an incremental run where the linked doc is not in
338
+ # the batch — without it the target keeps an absolute-path-derived id
339
+ # that matches no node in the merged graph and the md->md reference
340
+ # silently drops (#2211). Existence-gated: a link to a nonexistent
341
+ # doc must stay dangling, exactly as before. The stamp is transient
342
+ # and popped before graph.json ships.
343
+ target_file = None
344
+ try:
345
+ if resolved.is_file():
346
+ target_file = str(resolved)
347
+ except OSError:
348
+ pass
349
+ add_edge(file_nid, tgt_nid, "references", line, target_file=target_file)
350
+
351
+ # Track heading stack for nesting: [(level, nid), ...]
352
+ heading_stack: list[tuple[int, str]] = []
353
+ in_code_block = False
354
+
355
+ for line_num_0, line_text in enumerate(lines):
356
+ line_num = line_num_0 + 1
357
+
358
+ # Skip over fenced code blocks so their contents are not parsed as
359
+ # headings, but do not emit nodes/edges for them (#1077).
360
+ stripped = line_text.strip()
361
+ if stripped.startswith("```"):
362
+ in_code_block = not in_code_block
363
+ continue
364
+
365
+ if in_code_block:
366
+ continue
367
+
368
+ # Markdown links -> document references (#1376). Scanned on every
369
+ # non-fenced line (including heading lines, which the heading branch
370
+ # below `continue`s past) so links anywhere in the doc are captured.
371
+ for m in _MD_INLINE_LINK_RE.finditer(line_text):
372
+ add_link(m.group(1), line_num)
373
+ for m in _MD_WIKILINK_RE.finditer(line_text):
374
+ add_link(m.group(1), line_num, wikilink=True)
375
+ ref_def = _MD_REF_DEF_RE.match(line_text)
376
+ if ref_def:
377
+ add_link(ref_def.group(1), line_num)
378
+
379
+ # Inside the frontmatter block a leading `#` is a YAML comment, not an
380
+ # H1. Links above are still scanned there on purpose: review workflows
381
+ # put wikilinks in frontmatter (e.g. a `consulted:` list), and those are
382
+ # real references. Only heading detection is suppressed.
383
+ if line_num_0 < body_start:
384
+ continue
385
+
386
+ # Detect headings: # Heading, ## Heading, etc.
387
+ heading_match = re.match(r'^(#{1,6})\s+(.+)', line_text)
388
+ if heading_match:
389
+ level = len(heading_match.group(1))
390
+ title = heading_match.group(2).strip()
391
+ h_nid = _make_id(stem, title)
392
+ # Avoid duplicate heading IDs by appending line number
393
+ if h_nid in seen_ids:
394
+ h_nid = _make_id(stem, title, str(line_num))
395
+ add_node(h_nid, title, line_num)
396
+
397
+ # Pop headings at same or deeper level
398
+ while heading_stack and heading_stack[-1][0] >= level:
399
+ heading_stack.pop()
400
+
401
+ # Connect to parent heading or file
402
+ parent = heading_stack[-1][1] if heading_stack else file_nid
403
+ add_edge(parent, h_nid, "contains", line_num)
404
+
405
+ heading_stack.append((level, h_nid))
406
+ continue
407
+
408
+ return {"nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0}
@@ -0,0 +1,131 @@
1
+ """models — moved verbatim from graphify/extract.py."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable
5
+ from pathlib import Path
6
+ from dataclasses import dataclass, field
7
+
8
+
9
+ _WORKSPACE_PACKAGE_CACHE: dict[str, dict[str, Path]] = {}
10
+
11
+ _JS_CACHE_BYPASS_SUFFIXES = {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte"}
12
+
13
+ @dataclass
14
+ class LanguageConfig:
15
+ ts_module: str # e.g. "tree_sitter_python"
16
+ ts_language_fn: str = "language" # attr to call: e.g. tslang.language()
17
+
18
+ class_types: frozenset = frozenset()
19
+ function_types: frozenset = frozenset()
20
+ import_types: frozenset = frozenset()
21
+ call_types: frozenset = frozenset()
22
+ static_prop_types: frozenset = frozenset()
23
+ helper_fn_names: frozenset = frozenset()
24
+ container_bind_methods: frozenset = frozenset()
25
+ event_listener_properties: frozenset = frozenset()
26
+
27
+ # Name extraction
28
+ name_field: str = "name"
29
+ name_fallback_child_types: tuple = ()
30
+
31
+ # Body detection
32
+ body_field: str = "body"
33
+ body_fallback_child_types: tuple = () # e.g. ("declaration_list", "compound_statement")
34
+
35
+ # Call name extraction
36
+ call_function_field: str = "function" # field on call node for callee
37
+ call_accessor_node_types: frozenset = frozenset() # member/attribute nodes
38
+ call_accessor_field: str = "attribute" # field on accessor for method name
39
+ call_accessor_object_field: str = "" # field on accessor for the receiver/object
40
+
41
+ # Stop recursion at these types in walk_calls
42
+ function_boundary_types: frozenset = frozenset()
43
+
44
+ # Import handler: called for import nodes instead of generic handling
45
+ import_handler: Callable | None = None
46
+
47
+ # Optional custom name resolver for functions (C, C++ declarator unwrapping)
48
+ resolve_function_name_fn: Callable | None = None
49
+
50
+ # Optional symbol name sanitizer for node ID generation (e.g. Ruby suffixed methods)
51
+ sanitize_symbol_name_fn: Callable[[str], str] | None = None
52
+
53
+ # Extra label formatting for functions: if True, functions get "name()" label
54
+ function_label_parens: bool = True
55
+
56
+ # Extra walk hook called after generic dispatch (for JS arrow functions, C# namespaces, etc.)
57
+ extra_walk_fn: Callable | None = None
58
+
59
+ @dataclass(frozen=True)
60
+ class _SymbolDeclarationFact:
61
+ file_path: Path
62
+ name: str
63
+ line: int
64
+
65
+ @dataclass(frozen=True)
66
+ class _SymbolImportFact:
67
+ file_path: Path
68
+ local_name: str
69
+ target_path: Path
70
+ imported_name: str
71
+ line: int
72
+
73
+ @dataclass(frozen=True)
74
+ class _SymbolAliasFact:
75
+ file_path: Path
76
+ alias: str
77
+ target_name: str
78
+ line: int
79
+
80
+ @dataclass(frozen=True)
81
+ class _SymbolExportFact:
82
+ file_path: Path
83
+ exported_name: str
84
+ line: int
85
+ local_name: str | None = None
86
+ target_path: Path | None = None
87
+ target_name: str | None = None
88
+ # `export type { X } from ...`: erased at compile time, so the re-export
89
+ # edge it produces is stamped type_only and excluded from Import Cycles
90
+ # (#3123). The fact itself still participates in symbol resolution - a
91
+ # type import resolved through a type-only barrel is itself type-only.
92
+ type_only: bool = False
93
+
94
+ @dataclass(frozen=True)
95
+ class _StarExportFact:
96
+ file_path: Path
97
+ target_path: Path
98
+ line: int
99
+ type_only: bool = False
100
+
101
+ @dataclass(frozen=True)
102
+ class _NamespaceExportFact:
103
+ file_path: Path
104
+ exported_name: str
105
+ target_path: Path
106
+ line: int
107
+ type_only: bool = False
108
+
109
+ @dataclass(frozen=True)
110
+ class _SymbolUseFact:
111
+ file_path: Path
112
+ source_id: str
113
+ local_name: str
114
+ relation: str
115
+ context: str
116
+ line: int
117
+
118
+ @dataclass
119
+ class _SymbolResolutionFacts:
120
+ declarations: list[_SymbolDeclarationFact] = field(default_factory=list)
121
+ imports: list[_SymbolImportFact] = field(default_factory=list)
122
+ aliases: list[_SymbolAliasFact] = field(default_factory=list)
123
+ exports: list[_SymbolExportFact] = field(default_factory=list)
124
+ star_exports: list[_StarExportFact] = field(default_factory=list)
125
+ namespace_exports: list[_NamespaceExportFact] = field(default_factory=list)
126
+ uses: list[_SymbolUseFact] = field(default_factory=list)
127
+ # File-to-file submodule imports from `from pkg import submod` (#1146).
128
+ # Each entry is (importing_file, submodule_file, line, local_name) -- local_name
129
+ # is the binding introduced in the importing file: the alias when `from pkg
130
+ # import submod as alias` is used, otherwise the submodule's own name (#2082).
131
+ module_imports: list[tuple[Path, Path, int, str]] = field(default_factory=list)