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