srcly 0.1.8__tar.gz → 0.1.11__tar.gz

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 (310) hide show
  1. {srcly-0.1.8 → srcly-0.1.11}/PKG-INFO +5 -1
  2. srcly-0.1.11/app/config.py +71 -0
  3. srcly-0.1.11/app/models.py +50 -0
  4. {srcly-0.1.8 → srcly-0.1.11}/app/routers/analysis.py +32 -10
  5. {srcly-0.1.8 → srcly-0.1.11}/app/run.py +35 -5
  6. srcly-0.1.11/app/services/analysis.py +406 -0
  7. srcly-0.1.11/app/services/tree_sitter_analysis.py +712 -0
  8. srcly-0.1.11/app/static/assets/index-BjRxOY6r.css +1 -0
  9. srcly-0.1.11/app/static/assets/index-poSkqVZY.js +14 -0
  10. {srcly-0.1.8 → srcly-0.1.11}/app/static/index.html +2 -2
  11. {srcly-0.1.8 → srcly-0.1.11}/pyproject.toml +5 -1
  12. srcly-0.1.8/app/config.py +0 -32
  13. srcly-0.1.8/app/models.py +0 -21
  14. srcly-0.1.8/app/services/analysis.py +0 -170
  15. srcly-0.1.8/app/static/assets/index-DsiiW2OM.css +0 -1
  16. srcly-0.1.8/app/static/assets/index-LJRR2obN.js +0 -13
  17. {srcly-0.1.8 → srcly-0.1.11}/.gitignore +0 -0
  18. {srcly-0.1.8 → srcly-0.1.11}/README.md +0 -0
  19. {srcly-0.1.8 → srcly-0.1.11}/app/.gitignore +0 -0
  20. {srcly-0.1.8 → srcly-0.1.11}/app/__init__.py +0 -0
  21. {srcly-0.1.8 → srcly-0.1.11}/app/main.py +0 -0
  22. {srcly-0.1.8 → srcly-0.1.11}/app/routers/files.py +0 -0
  23. {srcly-0.1.8 → srcly-0.1.11}/app/services/cache.py +0 -0
  24. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/abap-BdImnpbu.js +0 -0
  25. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/actionscript-3-CfeIJUat.js +0 -0
  26. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ada-bCR0ucgS.js +0 -0
  27. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/andromeeda-C-Jbm3Hp.js +0 -0
  28. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/angular-html-CU67Zn6k.js +0 -0
  29. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/angular-ts-BwZT4LLn.js +0 -0
  30. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/apache-Pmp26Uib.js +0 -0
  31. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/apex-DhZLUxFE.js +0 -0
  32. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/apl-dKokRX4l.js +0 -0
  33. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/applescript-Co6uUVPk.js +0 -0
  34. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ara-BRHolxvo.js +0 -0
  35. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/asciidoc-Dv7Oe6Be.js +0 -0
  36. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/asm-D_Q5rh1f.js +0 -0
  37. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/astro-CbQHKStN.js +0 -0
  38. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/aurora-x-D-2ljcwZ.js +0 -0
  39. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/awk-DMzUqQB5.js +0 -0
  40. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ayu-dark-Cv9koXgw.js +0 -0
  41. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ballerina-BFfxhgS-.js +0 -0
  42. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/bat-BkioyH1T.js +0 -0
  43. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/beancount-k_qm7-4y.js +0 -0
  44. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/berry-D08WgyRC.js +0 -0
  45. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/bibtex-CHM0blh-.js +0 -0
  46. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/bicep-Bmn6On1c.js +0 -0
  47. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/blade-DVc8C-J4.js +0 -0
  48. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/bsl-BO_Y6i37.js +0 -0
  49. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/c-BIGW1oBm.js +0 -0
  50. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cadence-Bv_4Rxtq.js +0 -0
  51. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cairo-KRGpt6FW.js +0 -0
  52. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/catppuccin-frappe-DFWUc33u.js +0 -0
  53. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/catppuccin-latte-C9dUb6Cb.js +0 -0
  54. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/catppuccin-macchiato-DQyhUUbL.js +0 -0
  55. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/catppuccin-mocha-D87Tk5Gz.js +0 -0
  56. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/clarity-D53aC0YG.js +0 -0
  57. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/clojure-P80f7IUj.js +0 -0
  58. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cmake-D1j8_8rp.js +0 -0
  59. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cobol-nwyudZeR.js +0 -0
  60. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/codeowners-Bp6g37R7.js +0 -0
  61. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/codeql-DsOJ9woJ.js +0 -0
  62. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/coffee-Ch7k5sss.js +0 -0
  63. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/common-lisp-Cg-RD9OK.js +0 -0
  64. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/coq-DkFqJrB1.js +0 -0
  65. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cpp-CofmeUqb.js +0 -0
  66. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/crystal-tKQVLTB8.js +0 -0
  67. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/csharp-CX12Zw3r.js +0 -0
  68. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/css-DPfMkruS.js +0 -0
  69. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/csv-fuZLfV_i.js +0 -0
  70. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cue-D82EKSYY.js +0 -0
  71. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/cypher-COkxafJQ.js +0 -0
  72. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/d-85-TOEBH.js +0 -0
  73. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dark-plus-eOWES_5F.js +0 -0
  74. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dart-CF10PKvl.js +0 -0
  75. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dax-CEL-wOlO.js +0 -0
  76. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/desktop-BmXAJ9_W.js +0 -0
  77. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/diff-D97Zzqfu.js +0 -0
  78. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/docker-BcOcwvcX.js +0 -0
  79. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dotenv-Da5cRb03.js +0 -0
  80. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dracula-BzJJZx-M.js +0 -0
  81. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dracula-soft-BXkSAIEj.js +0 -0
  82. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/dream-maker-BtqSS_iP.js +0 -0
  83. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/edge-BkV0erSs.js +0 -0
  84. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/elixir-CDX3lj18.js +0 -0
  85. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/elm-DbKCFpqz.js +0 -0
  86. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/emacs-lisp-C9XAeP06.js +0 -0
  87. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/erb-BOJIQeun.js +0 -0
  88. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/erlang-DsQrWhSR.js +0 -0
  89. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/everforest-dark-BgDCqdQA.js +0 -0
  90. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/everforest-light-C8M2exoo.js +0 -0
  91. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/fennel-BYunw83y.js +0 -0
  92. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/fish-BvzEVeQv.js +0 -0
  93. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/fluent-C4IJs8-o.js +0 -0
  94. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/fortran-fixed-form-BZjJHVRy.js +0 -0
  95. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/fortran-free-form-D22FLkUw.js +0 -0
  96. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/fsharp-CXgrBDvD.js +0 -0
  97. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gdresource-B7Tvp0Sc.js +0 -0
  98. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gdscript-DTMYz4Jt.js +0 -0
  99. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gdshader-DkwncUOv.js +0 -0
  100. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/genie-D0YGMca9.js +0 -0
  101. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gherkin-DyxjwDmM.js +0 -0
  102. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/git-commit-F4YmCXRG.js +0 -0
  103. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/git-rebase-r7XF79zn.js +0 -0
  104. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-dark-DHJKELXO.js +0 -0
  105. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-dark-default-Cuk6v7N8.js +0 -0
  106. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-dark-dimmed-DH5Ifo-i.js +0 -0
  107. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-dark-high-contrast-E3gJ1_iC.js +0 -0
  108. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-light-DAi9KRSo.js +0 -0
  109. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-light-default-D7oLnXFd.js +0 -0
  110. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/github-light-high-contrast-BfjtVDDH.js +0 -0
  111. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gleam-BspZqrRM.js +0 -0
  112. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/glimmer-js-Rg0-pVw9.js +0 -0
  113. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/glimmer-ts-U6CK756n.js +0 -0
  114. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/glsl-DplSGwfg.js +0 -0
  115. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gnuplot-DdkO51Og.js +0 -0
  116. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/go-Dn2_MT6a.js +0 -0
  117. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/graphql-ChdNCCLP.js +0 -0
  118. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/groovy-gcz8RCvz.js +0 -0
  119. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gruvbox-dark-hard-CFHQjOhq.js +0 -0
  120. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gruvbox-dark-medium-GsRaNv29.js +0 -0
  121. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gruvbox-dark-soft-CVdnzihN.js +0 -0
  122. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gruvbox-light-hard-CH1njM8p.js +0 -0
  123. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gruvbox-light-medium-DRw_LuNl.js +0 -0
  124. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/gruvbox-light-soft-hJgmCMqR.js +0 -0
  125. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hack-CaT9iCJl.js +0 -0
  126. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/haml-B8DHNrY2.js +0 -0
  127. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/handlebars-BL8al0AC.js +0 -0
  128. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/haskell-Df6bDoY_.js +0 -0
  129. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/haxe-CzTSHFRz.js +0 -0
  130. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hcl-BWvSN4gD.js +0 -0
  131. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hjson-D5-asLiD.js +0 -0
  132. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hlsl-D3lLCCz7.js +0 -0
  133. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/houston-DnULxvSX.js +0 -0
  134. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/html-GMplVEZG.js +0 -0
  135. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/html-derivative-BFtXZ54Q.js +0 -0
  136. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/http-jrhK8wxY.js +0 -0
  137. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hurl-irOxFIW8.js +0 -0
  138. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hxml-Bvhsp5Yf.js +0 -0
  139. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/hy-DFXneXwc.js +0 -0
  140. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/imba-DGztddWO.js +0 -0
  141. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ini-BEwlwnbL.js +0 -0
  142. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/java-CylS5w8V.js +0 -0
  143. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/javascript-wDzz0qaB.js +0 -0
  144. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jinja-4LBKfQ-Z.js +0 -0
  145. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jison-wvAkD_A8.js +0 -0
  146. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/json-Cp-IABpG.js +0 -0
  147. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/json5-C9tS-k6U.js +0 -0
  148. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jsonc-Des-eS-w.js +0 -0
  149. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jsonl-DcaNXYhu.js +0 -0
  150. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jsonnet-DFQXde-d.js +0 -0
  151. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jssm-C2t-YnRu.js +0 -0
  152. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/jsx-g9-lgVsj.js +0 -0
  153. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/julia-C8NyazO9.js +0 -0
  154. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/kanagawa-dragon-CkXjmgJE.js +0 -0
  155. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/kanagawa-lotus-CfQXZHmo.js +0 -0
  156. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/kanagawa-wave-DWedfzmr.js +0 -0
  157. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/kdl-DV7GczEv.js +0 -0
  158. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/kotlin-BdnUsdx6.js +0 -0
  159. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/kusto-BvAqAH-y.js +0 -0
  160. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/laserwave-DUszq2jm.js +0 -0
  161. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/latex-BUKiar2Z.js +0 -0
  162. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/lean-DP1Csr6i.js +0 -0
  163. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/less-B1dDrJ26.js +0 -0
  164. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/light-plus-B7mTdjB0.js +0 -0
  165. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/liquid-DYVedYrR.js +0 -0
  166. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/llvm-BtvRca6l.js +0 -0
  167. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/log-2UxHyX5q.js +0 -0
  168. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/logo-BtOb2qkB.js +0 -0
  169. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/lua-BbnMAYS6.js +0 -0
  170. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/luau-CXu1NL6O.js +0 -0
  171. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/make-CHLpvVh8.js +0 -0
  172. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/markdown-Cvjx9yec.js +0 -0
  173. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/marko-CPi9NSCl.js +0 -0
  174. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/material-theme-D5KoaKCx.js +0 -0
  175. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/material-theme-darker-BfHTSMKl.js +0 -0
  176. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/material-theme-lighter-B0m2ddpp.js +0 -0
  177. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/material-theme-ocean-CyktbL80.js +0 -0
  178. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/material-theme-palenight-Csfq5Kiy.js +0 -0
  179. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/matlab-D7o27uSR.js +0 -0
  180. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/mdc-DUICxH0z.js +0 -0
  181. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/mdx-Cmh6b_Ma.js +0 -0
  182. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/mermaid-DKYwYmdq.js +0 -0
  183. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/min-dark-CafNBF8u.js +0 -0
  184. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/min-light-CTRr51gU.js +0 -0
  185. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/mipsasm-CKIfxQSi.js +0 -0
  186. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/mojo-1DNp92w6.js +0 -0
  187. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/monokai-D4h5O-jR.js +0 -0
  188. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/move-Bu9oaDYs.js +0 -0
  189. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/narrat-DRg8JJMk.js +0 -0
  190. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/nextflow-CUEJCptM.js +0 -0
  191. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/nginx-DknmC5AR.js +0 -0
  192. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/night-owl-C39BiMTA.js +0 -0
  193. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/nim-CVrawwO9.js +0 -0
  194. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/nix-BbRYJGeE.js +0 -0
  195. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/nord-Ddv68eIx.js +0 -0
  196. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/nushell-C-sUppwS.js +0 -0
  197. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/objective-c-DXmwc3jG.js +0 -0
  198. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/objective-cpp-CLxacb5B.js +0 -0
  199. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ocaml-C0hk2d4L.js +0 -0
  200. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/one-dark-pro-DVMEJ2y_.js +0 -0
  201. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/one-light-PoHY5YXO.js +0 -0
  202. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/pascal-D93ZcfNL.js +0 -0
  203. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/perl-C0TMdlhV.js +0 -0
  204. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/php-CDn_0X-4.js +0 -0
  205. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/pkl-u5AG7uiY.js +0 -0
  206. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/plastic-3e1v2bzS.js +0 -0
  207. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/plsql-ChMvpjG-.js +0 -0
  208. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/po-BTJTHyun.js +0 -0
  209. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/poimandres-CS3Unz2-.js +0 -0
  210. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/polar-C0HS_06l.js +0 -0
  211. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/postcss-CXtECtnM.js +0 -0
  212. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/powerquery-CEu0bR-o.js +0 -0
  213. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/powershell-Dpen1YoG.js +0 -0
  214. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/prisma-Dd19v3D-.js +0 -0
  215. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/prolog-CbFg5uaA.js +0 -0
  216. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/proto-DyJlTyXw.js +0 -0
  217. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/pug-CGlum2m_.js +0 -0
  218. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/puppet-BMWR74SV.js +0 -0
  219. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/purescript-CklMAg4u.js +0 -0
  220. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/python-B6aJPvgy.js +0 -0
  221. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/qml-3beO22l8.js +0 -0
  222. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/qmldir-C8lEn-DE.js +0 -0
  223. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/qss-IeuSbFQv.js +0 -0
  224. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/r-DiinP2Uv.js +0 -0
  225. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/racket-BqYA7rlc.js +0 -0
  226. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/raku-DXvB9xmW.js +0 -0
  227. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/razor-WgofotgN.js +0 -0
  228. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/red-bN70gL4F.js +0 -0
  229. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/reg-C-SQnVFl.js +0 -0
  230. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/regexp-CDVJQ6XC.js +0 -0
  231. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rel-C3B-1QV4.js +0 -0
  232. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/riscv-BM1_JUlF.js +0 -0
  233. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rose-pine-BHrmToEH.js +0 -0
  234. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rose-pine-dawn-CnK8MTSM.js +0 -0
  235. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rose-pine-moon-NleAzG8P.js +0 -0
  236. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rosmsg-BJDFO7_C.js +0 -0
  237. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rst-B0xPkSld.js +0 -0
  238. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ruby-BvKwtOVI.js +0 -0
  239. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/rust-B1yitclQ.js +0 -0
  240. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/sas-cz2c8ADy.js +0 -0
  241. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/sass-Cj5Yp3dK.js +0 -0
  242. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/scala-C151Ov-r.js +0 -0
  243. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/scheme-C98Dy4si.js +0 -0
  244. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/scss-OYdSNvt2.js +0 -0
  245. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/sdbl-DVxCFoDh.js +0 -0
  246. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/shaderlab-Dg9Lc6iA.js +0 -0
  247. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/shellscript-Yzrsuije.js +0 -0
  248. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/shellsession-BADoaaVG.js +0 -0
  249. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/slack-dark-BthQWCQV.js +0 -0
  250. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/slack-ochin-DqwNpetd.js +0 -0
  251. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/smalltalk-BERRCDM3.js +0 -0
  252. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/snazzy-light-Bw305WKR.js +0 -0
  253. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/solarized-dark-DXbdFlpD.js +0 -0
  254. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/solarized-light-L9t79GZl.js +0 -0
  255. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/solidity-BbcW6ACK.js +0 -0
  256. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/soy-Brmx7dQM.js +0 -0
  257. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/sparql-rVzFXLq3.js +0 -0
  258. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/splunk-BtCnVYZw.js +0 -0
  259. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/sql-BLtJtn59.js +0 -0
  260. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ssh-config-_ykCGR6B.js +0 -0
  261. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/stata-BH5u7GGu.js +0 -0
  262. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/stylus-BEDo0Tqx.js +0 -0
  263. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/svelte-3Dk4HxPD.js +0 -0
  264. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/swift-Dg5xB15N.js +0 -0
  265. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/synthwave-84-CbfX1IO0.js +0 -0
  266. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/system-verilog-CnnmHF94.js +0 -0
  267. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/systemd-4A_iFExJ.js +0 -0
  268. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/talonscript-CkByrt1z.js +0 -0
  269. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/tasl-QIJgUcNo.js +0 -0
  270. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/tcl-dwOrl1Do.js +0 -0
  271. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/templ-W15q3VgB.js +0 -0
  272. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/terraform-BETggiCN.js +0 -0
  273. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/tex-Cppo0RY3.js +0 -0
  274. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/tokyo-night-hegEt444.js +0 -0
  275. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/toml-vGWfd6FD.js +0 -0
  276. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/ts-tags-zn1MmPIZ.js +0 -0
  277. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/tsv-B_m7g4N7.js +0 -0
  278. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/tsx-COt5Ahok.js +0 -0
  279. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/turtle-BsS91CYL.js +0 -0
  280. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/twig-CO9l9SDP.js +0 -0
  281. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/typescript-BPQ3VLAy.js +0 -0
  282. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/typespec-Df68jz8_.js +0 -0
  283. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/typst-DHCkPAjA.js +0 -0
  284. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/v-BcVCzyr7.js +0 -0
  285. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vala-CsfeWuGM.js +0 -0
  286. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vb-D17OF-Vu.js +0 -0
  287. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/verilog-BQ8w6xss.js +0 -0
  288. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vesper-DU1UobuO.js +0 -0
  289. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vhdl-CeAyd5Ju.js +0 -0
  290. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/viml-CJc9bBzg.js +0 -0
  291. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vitesse-black-Bkuqu6BP.js +0 -0
  292. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vitesse-dark-D0r3Knsf.js +0 -0
  293. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vitesse-light-CVO1_9PV.js +0 -0
  294. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vue-CCoi5OLL.js +0 -0
  295. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vue-html-DAAvJJDi.js +0 -0
  296. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vue-vine-_Ih-lPRR.js +0 -0
  297. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/vyper-CDx5xZoG.js +0 -0
  298. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wasm-CG6Dc4jp.js +0 -0
  299. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wasm-MzD3tlZU.js +0 -0
  300. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wenyan-BV7otONQ.js +0 -0
  301. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wgsl-Dx-B1_4e.js +0 -0
  302. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wikitext-BhOHFoWU.js +0 -0
  303. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wit-5i3qLPDT.js +0 -0
  304. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/wolfram-lXgVvXCa.js +0 -0
  305. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/xml-sdJ4AIDG.js +0 -0
  306. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/xsl-CtQFsRM5.js +0 -0
  307. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/yaml-Buea-lGh.js +0 -0
  308. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/zenscript-DVFEvuxE.js +0 -0
  309. {srcly-0.1.8 → srcly-0.1.11}/app/static/assets/zig-VOosw3JB.js +0 -0
  310. {srcly-0.1.8 → srcly-0.1.11}/app/static/vite.svg +0 -0
@@ -1,11 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: srcly
3
- Version: 0.1.8
3
+ Version: 0.1.11
4
4
  Summary: Interactive codebase treemap and metrics viewer.
5
5
  Requires-Python: >=3.13
6
6
  Requires-Dist: fastapi>=0.121.3
7
7
  Requires-Dist: lizard>=1.19.0
8
+ Requires-Dist: pathspec>=0.12.1
8
9
  Requires-Dist: pydantic>=2.12.4
10
+ Requires-Dist: pytest>=9.0.1
11
+ Requires-Dist: tree-sitter-typescript>=0.23.2
12
+ Requires-Dist: tree-sitter>=0.25.2
9
13
  Requires-Dist: uvicorn>=0.38.0
10
14
  Description-Content-Type: text/markdown
11
15
 
@@ -0,0 +1,71 @@
1
+ from typing import Set
2
+
3
+ IGNORE_DIRS: Set[str] = {
4
+ ".git",
5
+ "node_modules",
6
+ "venv",
7
+ ".venv",
8
+ "__pycache__",
9
+ "dist",
10
+ "build",
11
+ ".next",
12
+ "coverage",
13
+ ".idea",
14
+ ".vscode",
15
+ "target",
16
+ "out",
17
+ "android",
18
+ "ios",
19
+ }
20
+
21
+ IGNORE_FILES: Set[str] = {
22
+ "package-lock.json",
23
+ "yarn.lock",
24
+ "pnpm-lock.yaml",
25
+ "cargo.lock",
26
+ "poetry.lock",
27
+ "Gemfile.lock",
28
+ "composer.lock",
29
+ "mix.lock",
30
+ }
31
+
32
+ IGNORE_EXTENSIONS: Set[str] = {
33
+ ".png",
34
+ ".jpg",
35
+ ".jpeg",
36
+ ".gif",
37
+ ".svg",
38
+ ".ico",
39
+ ".pdf",
40
+ ".zip",
41
+ ".tar",
42
+ ".gz",
43
+ ".map",
44
+ ".min.js",
45
+ ".min.css",
46
+ ".json",
47
+ ".xml",
48
+ ".txt",
49
+ ".ttf",
50
+ ".woff",
51
+ ".woff2",
52
+ ".eot",
53
+ ".otf",
54
+ ".mp4",
55
+ ".mp3",
56
+ ".wav",
57
+ ".webm",
58
+ ".ogg",
59
+ ".exe",
60
+ ".dll",
61
+ ".so",
62
+ ".dylib",
63
+ ".bin",
64
+ ".iso",
65
+ ".dmg",
66
+ ".pkg",
67
+ ".deb",
68
+ ".rpm",
69
+ ".csv",
70
+ ".lock",
71
+ }
@@ -0,0 +1,50 @@
1
+ from typing import List
2
+ from pydantic import BaseModel, Field
3
+
4
+ class Metrics(BaseModel):
5
+ loc: int = 0
6
+ # Cyclomatic complexity can be fractional (e.g. average complexity),
7
+ # so we store it as a float to match the analysis values.
8
+ complexity: float = 0.0
9
+ function_count: int = 0
10
+ last_modified: float = 0.0
11
+ gitignored_count: int = 0
12
+ file_size: int = 0
13
+ file_count: int = 0
14
+
15
+ # New metrics
16
+ comment_lines: int = 0
17
+ comment_density: float = 0.0
18
+ max_nesting_depth: int = 0
19
+ average_function_length: float = 0.0
20
+ parameter_count: int = 0
21
+ todo_count: int = 0
22
+ classes_count: int = 0
23
+
24
+ # TS/TSX Specific Metrics
25
+ tsx_nesting_depth: int = 0
26
+ tsx_render_branching_count: int = 0
27
+ tsx_react_use_effect_count: int = 0
28
+ tsx_anonymous_handler_count: int = 0
29
+ tsx_prop_count: int = 0
30
+ ts_any_usage_count: int = 0
31
+ ts_ignore_count: int = 0
32
+ ts_import_coupling_count: int = 0
33
+ tsx_hardcoded_string_volume: int = 0
34
+ tsx_duplicated_string_count: int = 0
35
+ ts_type_interface_count: int = 0
36
+ ts_export_count: int = 0
37
+
38
+ class Node(BaseModel):
39
+ name: str
40
+ type: str # "folder", "file", "function", "code_fragment"
41
+ path: str
42
+ metrics: Metrics
43
+ # Use default_factory to avoid sharing the same list across instances
44
+ children: List["Node"] = Field(default_factory=list)
45
+ start_line: int = 0
46
+ end_line: int = 0
47
+
48
+ model_config = {
49
+ "populate_by_name": True
50
+ }
@@ -44,20 +44,18 @@ async def refresh_analysis():
44
44
  return tree
45
45
 
46
46
 
47
- @router.get("/context")
48
- async def get_analysis_context():
47
+ def _estimate_counts(root: Path) -> tuple[int, int]:
49
48
  """
50
- Return basic information about the current analysis root directory.
49
+ Return an estimated (file_count, folder_count) for a given root directory.
51
50
 
52
- This is used by the client to offer a one-click "analyze current folder"
53
- option on first load.
51
+ This applies the same ignore rules used during a full analysis to keep the
52
+ estimate reasonably fast while still informative.
54
53
  """
55
- root = ROOT_PATH
56
54
  file_count = 0
57
55
  folder_count = 0
58
56
 
59
57
  try:
60
- for dirpath, dirnames, filenames in os.walk(root):
58
+ for _, dirnames, filenames in os.walk(root):
61
59
  # Apply ignore rules for directories to keep the estimate reasonable
62
60
  dirnames[:] = [
63
61
  d for d in dirnames
@@ -70,8 +68,32 @@ async def get_analysis_context():
70
68
  file_count = 0
71
69
  folder_count = 0
72
70
 
71
+ return file_count, folder_count
72
+
73
+
74
+ @router.get("/context")
75
+ async def get_analysis_context():
76
+ """
77
+ Return basic information about the current analysis root directory.
78
+
79
+ This is used by the client to offer one-click analysis options on first
80
+ load, for both the current working directory and the repository root.
81
+ """
82
+ current_root = ROOT_PATH
83
+ repo_root = current_root.parent
84
+
85
+ current_file_count, current_folder_count = _estimate_counts(current_root)
86
+
87
+ repo_file_count = 0
88
+ repo_folder_count = 0
89
+ if repo_root.exists():
90
+ repo_file_count, repo_folder_count = _estimate_counts(repo_root)
91
+
73
92
  return {
74
- "root_path": str(root),
75
- "file_count": file_count,
76
- "folder_count": folder_count,
93
+ "root_path": str(current_root),
94
+ "file_count": current_file_count,
95
+ "folder_count": current_folder_count,
96
+ "repo_root_path": str(repo_root),
97
+ "repo_file_count": repo_file_count,
98
+ "repo_folder_count": repo_folder_count,
77
99
  }
@@ -26,11 +26,34 @@ def _open_browser_later(url: str, delay: float = 1.0) -> None:
26
26
  thread.start()
27
27
 
28
28
 
29
+ def _find_repo_root(start_path: str) -> str:
30
+ """
31
+ Walk upwards from ``start_path`` to find a Git repository root.
32
+
33
+ Returns the first directory that contains a `.git` directory or file.
34
+ If none is found, the original ``start_path`` is returned.
35
+ """
36
+ current = os.path.abspath(start_path)
37
+
38
+ while True:
39
+ git_path = os.path.join(current, ".git")
40
+ if os.path.exists(git_path):
41
+ return current
42
+
43
+ parent = os.path.dirname(current)
44
+ if parent == current:
45
+ # Reached filesystem root; fall back to the original start path.
46
+ return os.path.abspath(start_path)
47
+
48
+ current = parent
49
+
50
+
29
51
  def main(argv: list[str] | None = None) -> None:
30
52
  """
31
53
  Entry point for the CLI.
32
54
 
33
- - Uses the current working directory (or a provided path) as the codebase root.
55
+ - With no path argument, uses the enclosing Git repo root as the codebase root.
56
+ - If a path is provided (including "."), uses that as the codebase root.
34
57
  - Starts the FastAPI server.
35
58
  - Opens the default browser to the app URL.
36
59
  """
@@ -38,14 +61,17 @@ def main(argv: list[str] | None = None) -> None:
38
61
  prog="srcly",
39
62
  description=(
40
63
  "Interactive codebase treemap and metrics viewer. "
41
- "By default, analyzes the current working directory."
64
+ "By default, analyzes the enclosing Git repository root."
42
65
  ),
43
66
  )
44
67
  parser.add_argument(
45
68
  "path",
46
69
  nargs="?",
47
- default=".",
48
- help="Path to the codebase to analyze (default: current directory).",
70
+ help=(
71
+ "Path to the codebase to analyze. "
72
+ "If omitted, the enclosing Git repo root is used. "
73
+ 'Use "." explicitly to analyze the current directory.'
74
+ ),
49
75
  )
50
76
  parser.add_argument(
51
77
  "--host",
@@ -61,7 +87,11 @@ def main(argv: list[str] | None = None) -> None:
61
87
 
62
88
  args = parser.parse_args(argv)
63
89
 
64
- target_path = os.path.abspath(args.path)
90
+ if args.path is None:
91
+ target_path = _find_repo_root(os.getcwd())
92
+ else:
93
+ target_path = os.path.abspath(args.path)
94
+
65
95
  if not os.path.exists(target_path):
66
96
  raise SystemExit(f"Path does not exist: {target_path}")
67
97
 
@@ -0,0 +1,406 @@
1
+ import os
2
+ import lizard
3
+ import concurrent.futures
4
+ from pathlib import Path
5
+
6
+ from app.models import Node, Metrics
7
+ from app.config import IGNORE_DIRS, IGNORE_FILES, IGNORE_EXTENSIONS
8
+ from app.services.tree_sitter_analysis import TreeSitterAnalyzer
9
+ from pathspec import PathSpec
10
+
11
+ _ts_analyzer = None
12
+
13
+ def get_ts_analyzer():
14
+ global _ts_analyzer
15
+ if _ts_analyzer is None:
16
+ _ts_analyzer = TreeSitterAnalyzer()
17
+ return _ts_analyzer
18
+
19
+ def find_repo_root(start_path: Path) -> Path:
20
+ current = start_path.resolve()
21
+ for parent in [current, *current.parents]:
22
+ if (parent / ".git").exists(): return parent
23
+ return current
24
+
25
+ def create_node(name: str, node_type: str, path: str) -> Node:
26
+ return Node(
27
+ name=name,
28
+ type=node_type,
29
+ path=path,
30
+ metrics=Metrics(),
31
+ children=[]
32
+ )
33
+
34
+ def attach_file_metrics(node: Node, file_info) -> None:
35
+ total_loc = file_info.nloc
36
+ node.metrics.loc = total_loc
37
+ node.metrics.complexity = file_info.average_cyclomatic_complexity
38
+ node.metrics.function_count = len(file_info.function_list)
39
+ node.metrics.function_count = len(file_info.function_list)
40
+ node.metrics.file_count = 1
41
+ # TS/TSX-specific metrics are only available for TypeScript/TSX files analyzed
42
+ # by the TreeSitterAnalyzer. Plain lizard FileInformation objects (e.g. for
43
+ # Python or other languages) won't have these attributes, so guard them.
44
+ if hasattr(file_info, "tsx_nesting_depth"):
45
+ node.metrics.tsx_nesting_depth = file_info.tsx_nesting_depth
46
+ node.metrics.tsx_render_branching_count = file_info.tsx_render_branching_count
47
+ node.metrics.tsx_react_use_effect_count = file_info.tsx_react_use_effect_count
48
+ node.metrics.tsx_anonymous_handler_count = file_info.tsx_anonymous_handler_count
49
+ node.metrics.tsx_prop_count = file_info.tsx_prop_count
50
+ node.metrics.ts_any_usage_count = file_info.ts_any_usage_count
51
+ node.metrics.ts_ignore_count = file_info.ts_ignore_count
52
+ node.metrics.ts_import_coupling_count = file_info.ts_import_coupling_count
53
+ node.metrics.tsx_hardcoded_string_volume = file_info.tsx_hardcoded_string_volume
54
+ node.metrics.tsx_duplicated_string_count = file_info.tsx_duplicated_string_count
55
+ node.metrics.ts_type_interface_count = file_info.ts_type_interface_count
56
+ node.metrics.ts_export_count = file_info.ts_export_count
57
+
58
+ # New metrics
59
+ if hasattr(file_info, 'comment_lines'):
60
+ node.metrics.comment_lines = file_info.comment_lines
61
+ node.metrics.comment_density = file_info.comment_density
62
+ node.metrics.max_nesting_depth = file_info.max_nesting_depth
63
+ node.metrics.average_function_length = file_info.average_function_length
64
+ node.metrics.parameter_count = file_info.parameter_count
65
+ node.metrics.todo_count = file_info.todo_count
66
+ node.metrics.classes_count = file_info.classes_count
67
+
68
+ # Set start/end line for the file (approximate, 1 to total lines)
69
+ # We don't strictly have this from lizard always, but we can infer or leave 0.
70
+ # For now, let's leave file start/end as 0 unless we want to read the file.
71
+
72
+ # Calculate sum of function LOCs
73
+ func_sum_loc = 0
74
+
75
+ def convert_function(func, parent_path: str) -> Node:
76
+ func_node = create_node(func.name, "function", f"{parent_path}::{func.name}")
77
+ func_node.metrics.loc = func.nloc
78
+ func_node.metrics.loc = func.nloc
79
+ func_node.metrics.complexity = func.cyclomatic_complexity
80
+
81
+ # Safely get new metrics (Lizard functions won't have these)
82
+ func_node.metrics.parameter_count = getattr(func, 'parameter_count', 0)
83
+ func_node.metrics.max_nesting_depth = getattr(func, 'max_nesting_depth', 0)
84
+ func_node.metrics.comment_lines = getattr(func, 'comment_lines', 0)
85
+ func_node.metrics.todo_count = getattr(func, 'todo_count', 0)
86
+ func_node.metrics.ts_type_interface_count = getattr(func, 'ts_type_interface_count', 0)
87
+
88
+ # Density for function
89
+ comment_lines = getattr(func, 'comment_lines', 0)
90
+ func_node.metrics.comment_density = comment_lines / func.nloc if func.nloc > 0 else 0.0
91
+
92
+ if hasattr(func, 'start_line'):
93
+ func_node.start_line = func.start_line
94
+ if hasattr(func, 'end_line'):
95
+ func_node.end_line = func.end_line
96
+
97
+ # Process children if they exist (for TS/TSX)
98
+ if hasattr(func, 'children') and func.children:
99
+ for child in func.children:
100
+ child_node = convert_function(child, func_node.path)
101
+ func_node.children.append(child_node)
102
+
103
+ return func_node
104
+
105
+ for func in file_info.function_list:
106
+ func_node = convert_function(func, node.path)
107
+ node.children.append(func_node)
108
+ func_sum_loc += func.nloc
109
+
110
+ # CRITICAL FIX: Add a virtual node for the "Glue Code" (Imports, Exports, Global Vars)
111
+ remainder = total_loc - func_sum_loc
112
+ if remainder > 0:
113
+ misc_node = create_node("(misc/imports)", "code_fragment", f"{node.path}::__misc__")
114
+ misc_node.metrics.loc = remainder
115
+ misc_node.metrics.complexity = 0 # Glue code is usually simple
116
+ node.children.append(misc_node)
117
+
118
+
119
+ def _translate_gitignore_pattern(raw_line: str, base_rel: str) -> str | None:
120
+ """
121
+ Translate a single .gitignore pattern that lives in a directory `base_rel`
122
+ (relative to the repo root) into a repo-root-relative gitwildmatch pattern.
123
+
124
+ This approximates Git's semantics including:
125
+ - patterns starting with '!' (negation)
126
+ - patterns starting with '/' (anchored to the .gitignore directory)
127
+ - patterns without '/' applying within the directory subtree
128
+ """
129
+ line = raw_line.rstrip("\n")
130
+ if not line or line.lstrip().startswith("#"):
131
+ return None
132
+
133
+ negated = line.startswith("!")
134
+ body = line[1:] if negated else line
135
+
136
+ # Strip leading slash: anchored to the directory containing the .gitignore
137
+ if body.startswith("/"):
138
+ body = body[1:]
139
+
140
+ # Compute prefix for this .gitignore directory
141
+ prefix = f"{base_rel}/" if base_rel else ""
142
+
143
+ # If the pattern contains a slash, it's relative to the directory root.
144
+ # Otherwise, it should match that name anywhere under the directory.
145
+ if "/" in body:
146
+ pat = prefix + body
147
+ else:
148
+ if base_rel:
149
+ pat = f"{base_rel}/**/{body}"
150
+ else:
151
+ pat = f"**/{body}"
152
+
153
+ return f"!{pat}" if negated else pat
154
+
155
+
156
+ def _load_gitignore_spec(root_path: Path) -> tuple[Path, PathSpec | None]:
157
+ """
158
+ Load a PathSpec representing .gitignore rules visible from the given
159
+ root path, honoring nested .gitignore files similarly to Git.
160
+
161
+ We treat the *repository root* (where .git lives) as the base for all
162
+ ignore patterns, so that scanning a subdirectory still respects repo-level
163
+ .gitignore files and nested ones.
164
+ """
165
+ repo_root = find_repo_root(root_path)
166
+
167
+ all_patterns: list[str] = []
168
+
169
+ for dirpath, dirnames, filenames in os.walk(repo_root):
170
+ # Never look inside the .git directory for ignore rules
171
+ if ".git" in dirnames:
172
+ dirnames.remove(".git")
173
+
174
+ if ".gitignore" not in filenames:
175
+ continue
176
+
177
+ gitignore_file = Path(dirpath) / ".gitignore"
178
+ base_rel = (
179
+ str(Path(dirpath).relative_to(repo_root).as_posix())
180
+ if Path(dirpath) != repo_root
181
+ else ""
182
+ )
183
+
184
+ with open(gitignore_file, "r") as f:
185
+ for raw in f:
186
+ translated = _translate_gitignore_pattern(raw, base_rel)
187
+ if translated is not None:
188
+ all_patterns.append(translated)
189
+
190
+ if not all_patterns:
191
+ return repo_root, None
192
+
193
+ spec = PathSpec.from_lines("gitwildmatch", all_patterns)
194
+ return repo_root, spec
195
+
196
+
197
+ def _is_gitignored(path: Path, ignore_root: Path, spec: PathSpec | None) -> bool:
198
+ """
199
+ Return True if the given path should be ignored according to the
200
+ provided PathSpec and root_path.
201
+ """
202
+ if spec is None:
203
+ return False
204
+
205
+ try:
206
+ rel = path.relative_to(ignore_root)
207
+ except ValueError:
208
+ rel = path
209
+
210
+ rel_str = rel.as_posix()
211
+ return spec.match_file(rel_str)
212
+
213
+ def aggregate_metrics(node: Node) -> Metrics:
214
+ if not node.children: return node.metrics
215
+
216
+ total_loc = 0
217
+ max_complexity = 0
218
+ total_funcs = 0
219
+
220
+ # New metrics aggregation
221
+ total_comment_lines = 0
222
+ max_nesting_depth = 0
223
+ total_parameter_count = 0
224
+ total_todo_count = 0
225
+ total_classes_count = 0
226
+ total_type_interface_count = 0
227
+ total_export_count = 0
228
+
229
+ # For average function length, we need total function loc and total functions (already have total_funcs)
230
+ # But we need to sum function locs from children.
231
+ # Let's track total function loc separately if we want to be precise,
232
+ # OR we can just use the child's average * child's function count.
233
+ total_function_loc = 0
234
+
235
+ for child in node.children:
236
+ child_metrics = aggregate_metrics(child)
237
+ total_loc += child_metrics.loc
238
+ max_complexity = max(max_complexity, child_metrics.complexity)
239
+ total_funcs += child_metrics.function_count
240
+
241
+ total_comment_lines += child_metrics.comment_lines
242
+ max_nesting_depth = max(max_nesting_depth, child_metrics.max_nesting_depth)
243
+ total_parameter_count += child_metrics.parameter_count
244
+ total_todo_count += child_metrics.todo_count
245
+ total_classes_count += child_metrics.classes_count
246
+ total_type_interface_count += child_metrics.ts_type_interface_count
247
+ total_export_count += child_metrics.ts_export_count
248
+
249
+ # Reconstruct total function loc from average * count
250
+ total_function_loc += (child_metrics.average_function_length * child_metrics.function_count)
251
+
252
+ # For Folders: Sum of children
253
+ # For Files: We trust the attach_file_metrics logic (which includes __misc__)
254
+ if node.type == "folder":
255
+ node.metrics.loc = total_loc
256
+ node.metrics.complexity = max_complexity
257
+ node.metrics.function_count = total_funcs
258
+
259
+ # Aggregate last_modified (max of children) and gitignored_count (sum of children)
260
+ node.metrics.last_modified = max((child.metrics.last_modified for child in node.children), default=0.0)
261
+ node.metrics.gitignored_count = sum(child.metrics.gitignored_count for child in node.children)
262
+ node.metrics.file_size = sum(child.metrics.file_size for child in node.children)
263
+ node.metrics.file_count = sum(child.metrics.file_count for child in node.children)
264
+
265
+ node.metrics.comment_lines = total_comment_lines
266
+ node.metrics.comment_density = total_comment_lines / total_loc if total_loc > 0 else 0.0
267
+ node.metrics.max_nesting_depth = max_nesting_depth
268
+ node.metrics.parameter_count = total_parameter_count
269
+ node.metrics.todo_count = total_todo_count
270
+ node.metrics.classes_count = total_classes_count
271
+ node.metrics.average_function_length = total_function_loc / total_funcs if total_funcs > 0 else 0.0
272
+ node.metrics.ts_type_interface_count = total_type_interface_count
273
+ node.metrics.ts_export_count = total_export_count
274
+
275
+ return node.metrics
276
+
277
+ def analyze_single_file(file_path: str):
278
+ """
279
+ Wrapper to analyze a single file safely.
280
+ Must be top-level for multiprocessing pickling.
281
+ """
282
+ try:
283
+ if file_path.endswith('.ts') or file_path.endswith('.tsx'):
284
+ analyzer = get_ts_analyzer()
285
+ return analyzer.analyze_file(file_path)
286
+ else:
287
+ return lizard.analyze_file(file_path)
288
+ except Exception as e:
289
+ # Return error info instead of crashing
290
+ return {"error": str(e), "filename": file_path}
291
+
292
+ def scan_codebase(root_path: Path) -> Node:
293
+ print(f"🔍 Scanning: {root_path}", flush=True)
294
+
295
+ # Load .gitignore spec (repo-wide, with nested .gitignore support)
296
+ ignore_root, gitignore_spec = _load_gitignore_spec(root_path)
297
+
298
+ files_to_scan: list[str] = []
299
+ ignored_counts: dict[str, int] = {}
300
+
301
+ for root_dir, dirs, files in os.walk(root_path):
302
+ root_dir_path = Path(root_dir)
303
+
304
+ # Apply ignore dirs from config and .gitignore
305
+ # We must modify dirs in-place to prune traversal
306
+ pruned_dirs: list[str] = []
307
+ for d in dirs:
308
+ if d in IGNORE_DIRS:
309
+ continue
310
+ dir_path = root_dir_path / d
311
+ if _is_gitignored(dir_path, ignore_root, gitignore_spec):
312
+ # Entire directory is ignored; we skip traversing into it.
313
+ continue
314
+ pruned_dirs.append(d)
315
+ dirs[:] = pruned_dirs
316
+
317
+ current_ignored_count = 0
318
+ for file in files:
319
+ if file in IGNORE_FILES:
320
+ continue
321
+ if Path(file).suffix in IGNORE_EXTENSIONS:
322
+ continue
323
+
324
+ file_path = root_dir_path / file
325
+
326
+ if _is_gitignored(file_path, ignore_root, gitignore_spec):
327
+ current_ignored_count += 1
328
+ continue
329
+
330
+ files_to_scan.append(str(file_path))
331
+
332
+ if current_ignored_count > 0:
333
+ ignored_counts[str(root_dir_path)] = current_ignored_count
334
+
335
+ print(f"📂 Analyzing {len(files_to_scan)} source files...", flush=True)
336
+
337
+ analysis_results = []
338
+
339
+ # Use ProcessPoolExecutor for better control and per-file error handling
340
+ # lizard.analyze_files was opaque and crashing the whole pool on one error
341
+ with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
342
+ future_to_file = {executor.submit(analyze_single_file, f): f for f in files_to_scan}
343
+
344
+ completed_count = 0
345
+ total_count = len(files_to_scan)
346
+
347
+ for future in concurrent.futures.as_completed(future_to_file):
348
+ file = future_to_file[future]
349
+ completed_count += 1
350
+ try:
351
+ # Add a timeout to prevent hanging on single files
352
+ result = future.result(timeout=5)
353
+ if isinstance(result, dict) and "error" in result:
354
+ print(f"❌ [{completed_count}/{total_count}] Error analyzing {file}: {result['error']}", flush=True)
355
+ else:
356
+ # Success
357
+ print(f"✅ [{completed_count}/{total_count}] Analyzed {file}", flush=True)
358
+ analysis_results.append(result)
359
+ except concurrent.futures.TimeoutError:
360
+ print(f"❌ [{completed_count}/{total_count}] Timeout analyzing {file} (skipped)", flush=True)
361
+ except Exception as exc:
362
+ print(f"❌ [{completed_count}/{total_count}] Exception analyzing {file}: {exc}", flush=True)
363
+
364
+ tree_root = create_node("root", "folder", str(root_path))
365
+ node_map = {str(root_path): tree_root}
366
+
367
+ for file_info in analysis_results:
368
+ path_obj = Path(file_info.filename)
369
+ try: rel_path = path_obj.relative_to(root_path)
370
+ except ValueError: continue
371
+
372
+ parts = rel_path.parts
373
+ current_node = tree_root
374
+ current_path = root_path
375
+
376
+ # Build Folder Tree
377
+ for part in parts[:-1]:
378
+ next_path = current_path / part
379
+ next_path_str = str(next_path)
380
+ if next_path_str not in node_map:
381
+ new_folder = create_node(part, "folder", next_path_str)
382
+ # Set gitignored count if we have it for this folder
383
+ if next_path_str in ignored_counts:
384
+ new_folder.metrics.gitignored_count = ignored_counts[next_path_str]
385
+ current_node.children.append(new_folder)
386
+ node_map[next_path_str] = new_folder
387
+ current_node = node_map[next_path_str]
388
+ current_path = next_path
389
+
390
+ # Add File
391
+ file_node = create_node(parts[-1], "file", str(path_obj))
392
+ file_node = create_node(parts[-1], "file", str(path_obj))
393
+ attach_file_metrics(file_node, file_info)
394
+ # Set last_modified
395
+ try:
396
+ stat = os.stat(path_obj)
397
+ file_node.metrics.last_modified = stat.st_mtime
398
+ file_node.metrics.file_size = stat.st_size
399
+ except OSError:
400
+ file_node.metrics.last_modified = 0.0
401
+ file_node.metrics.file_size = 0
402
+
403
+ current_node.children.append(file_node)
404
+
405
+ aggregate_metrics(tree_root)
406
+ return tree_root