@armanckeser/downstream 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/SKILL.md +150 -0
- package/bin/downstream.mjs +19 -0
- package/dist/cli/downstream.js +543 -0
- package/dist/domain/agent.js +3 -0
- package/dist/domain/approval.js +190 -0
- package/dist/domain/delivery.js +21 -0
- package/dist/domain/home.js +1 -0
- package/dist/domain/inbox.js +245 -0
- package/dist/domain/model.js +180 -0
- package/dist/domain/next-move.js +34 -0
- package/dist/domain/pr-detail.js +45 -0
- package/dist/server/actions.js +564 -0
- package/dist/server/agent-host.js +230 -0
- package/dist/server/analyze/git.js +161 -0
- package/dist/server/analyze/graph.js +403 -0
- package/dist/server/analyze/py-graph.js +141 -0
- package/dist/server/analyze/py.js +346 -0
- package/dist/server/analyze/ts-decls.js +299 -0
- package/dist/server/analyze/ts-project.js +212 -0
- package/dist/server/gh.js +51 -0
- package/dist/server/github.js +264 -0
- package/dist/server/home-store.js +127 -0
- package/dist/server/home.js +325 -0
- package/dist/server/install.js +12 -0
- package/dist/server/main.js +228 -0
- package/dist/server/prompts.js +32 -0
- package/dist/server/publish.js +136 -0
- package/dist/server/repos.js +164 -0
- package/dist/server/stale.js +50 -0
- package/dist/server/store.js +269 -0
- package/dist/server/workspace.js +251 -0
- package/package.json +63 -4
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-iITFzleb.js +1 -0
- package/web/dist/assets/angular-ts-qNGaWUZr.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-DAcxbtpZ.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-CxPQu5s8.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-DJrnOqL4.js +1 -0
- package/web/dist/assets/bsl-_VjlB9mv.js +1 -0
- package/web/dist/assets/c-DJLzn8qa.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DLTphjLi.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-BHYX99F3.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-C_LDam8n.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-CcuifkxZ.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/cpp-E7p80IG9.js +1 -0
- package/web/dist/assets/crystal-iYHODIir.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-Bk6njqO4.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-Ct7_SkpN.js +1 -0
- package/web/dist/assets/elixir-YTzSQL2h.js +1 -0
- package/web/dist/assets/elm-bNkwlHr8.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/erb-uFBspDdG.js +1 -0
- package/web/dist/assets/erlang-Cphh6RMH.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/fortran-fixed-form-DEKoE2YW.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-D13ZGOAj.js +1 -0
- package/web/dist/assets/gdresource-xMC7jr7x.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-BSykSTBG.js +1 -0
- package/web/dist/assets/git-rebase-CSzaMu4k.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-WrpQIag3.js +1 -0
- package/web/dist/assets/glimmer-ts-BkR7s81M.js +1 -0
- package/web/dist/assets/glsl-r6ahr8UN.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-vcFzEvUM.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-DV1TStQR.js +1 -0
- package/web/dist/assets/haml-BzEpEFnM.js +1 -0
- package/web/dist/assets/handlebars-Bw2ai2tn.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CVWJgEI4.js +1 -0
- package/web/dist/assets/html-derivative-kTFEcoss.js +1 -0
- package/web/dist/assets/http-DXC6PGgt.js +1 -0
- package/web/dist/assets/hurl-Bpd8P_ef.js +1 -0
- package/web/dist/assets/hxml-Dcm88srW.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DpYMsPxu.js +1673 -0
- package/web/dist/assets/index-Zh-Bd2u6.css +2 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-Bsmh9nXE.js +1 -0
- package/web/dist/assets/javascript-CeyaFN_P.js +1 -0
- package/web/dist/assets/jinja-BuqTDofy.js +1 -0
- package/web/dist/assets/jison-BYfmmaBZ.js +1 -0
- package/web/dist/assets/json-Bhl6cDM_.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-BSUHjwDX.js +1 -0
- package/web/dist/assets/julia-CWrNF0wf.js +1 -0
- package/web/dist/assets/just-wLw7gWsD.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-DyWaeH0P.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-DeCc5Rg2.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-Ba-NltBp.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-DGb2HMVj.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-CL7bhRQ-.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-CSgcYZcx.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-CbyuJibN.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-C7LemgDR.js +1 -0
- package/web/dist/assets/nix-IvuFDN5E.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-wrbLazmP.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-DarHXyEu.js +1 -0
- package/web/dist/assets/php--54M0-pj.js +1 -0
- package/web/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/web/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/web/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/web/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/web/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/web/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/web/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/web/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/web/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/web/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-CFrC1rDC.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-Go4B46qX.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-Bpu17F3f.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-CeZTEIIg.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-D4K6CiTG.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-Dy2Bl1Fg.js +1 -0
- package/web/dist/assets/ruby-CNqr2Wuz.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-B6MsvXFb.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CHuFWGi8.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-TOUzSsQk.js +1 -0
- package/web/dist/assets/shellscript-EEdMjEbs.js +1 -0
- package/web/dist/assets/shellsession-U_I5Q2cJ.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-BnCBZkG3.js +1 -0
- package/web/dist/assets/sparql-D_iOobhT.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-5dStcFSw.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-B5iSQdtS.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-D4K5msa4.js +1 -0
- package/web/dist/assets/svelte-Dn-_7BgE.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-BTe8yLW8.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-BBG77enc.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-BzJXXnMD.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-Cj2inrAP.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-BH3HU2q4.js +1 -0
- package/web/dist/assets/typescript-DpDozqSz.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-GHzvrUad.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CqkncxvB.js +1 -0
- package/web/dist/assets/vue-html-DrD64fqb.js +1 -0
- package/web/dist/assets/vue-vine-BJy37wv2.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-CtXDvb3g.js +1 -0
- package/web/dist/assets/xsl-9J2XPEqv.js +1 -0
- package/web/dist/assets/yaml-CKsrpaXo.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/index.html +15 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// Turns a diff into a flow graph: the changed symbols, what they call, who
|
|
2
|
+
// calls them, and which of those relationships are new. The agent edits the
|
|
3
|
+
// result; this only has to be a good first draft.
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { countInRange, inRanges } from "./git.js";
|
|
7
|
+
import { calleeNames, enclosing, extractDecls, isTsLike, parse } from "./ts-decls.js";
|
|
8
|
+
import { TsProjects } from "./ts-project.js";
|
|
9
|
+
import { isPy, PyIndex } from "./py.js";
|
|
10
|
+
import { pyEdges, pySymbols } from "./py-graph.js";
|
|
11
|
+
const MAX_CALLERS = 12;
|
|
12
|
+
const symbolId = (file, key) => `${file}#${key}`;
|
|
13
|
+
const TEST_FILE = /(^|\/)(tests?|__tests__|spec)\/|(^|\/)test_[^/]*\.py$|_test\.(py|go)$|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)conftest\.py$/;
|
|
14
|
+
export const isTestFile = (file) => TEST_FILE.test(file);
|
|
15
|
+
function toSymbol(file, d, status, changedLines = 0) {
|
|
16
|
+
return {
|
|
17
|
+
test: isTestFile(file),
|
|
18
|
+
id: symbolId(file, d.key),
|
|
19
|
+
name: d.name,
|
|
20
|
+
container: d.container,
|
|
21
|
+
kind: d.kind,
|
|
22
|
+
file,
|
|
23
|
+
range: status === "removed" ? null : d.range,
|
|
24
|
+
oldFile: null,
|
|
25
|
+
oldRange: null,
|
|
26
|
+
status,
|
|
27
|
+
signature: d.signature,
|
|
28
|
+
exported: d.exported,
|
|
29
|
+
entry: false,
|
|
30
|
+
summary: null,
|
|
31
|
+
changedLines,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Lines changed in a declaration that do not belong to one of its members. */
|
|
35
|
+
function ownChanges(d, ranges) {
|
|
36
|
+
let n = 0;
|
|
37
|
+
for (let line = d.range.start; line <= d.range.end; line++) {
|
|
38
|
+
if (inRanges(line, ranges) && !inRanges(line, d.memberRanges))
|
|
39
|
+
n++;
|
|
40
|
+
}
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
export function buildGraph(input) {
|
|
44
|
+
const lap = timer();
|
|
45
|
+
const symbols = new Map();
|
|
46
|
+
const edges = new Map();
|
|
47
|
+
const projects = input.projects ?? new TsProjects(input.root, input.newSrc, input.files.filter((f) => f.status !== "deleted").map((f) => f.path));
|
|
48
|
+
const addEdge = (from, to, kind, line, change) => {
|
|
49
|
+
if (from === to)
|
|
50
|
+
return;
|
|
51
|
+
const id = `${from}->${to}:${kind}`;
|
|
52
|
+
const prev = edges.get(id);
|
|
53
|
+
if (prev) {
|
|
54
|
+
if (change === "added" && prev.change === "same")
|
|
55
|
+
prev.change = "added";
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
edges.set(id, { id, from, to, kind, change, line, label: null });
|
|
59
|
+
};
|
|
60
|
+
// 1. Symbols per file.
|
|
61
|
+
const changedDecls = [];
|
|
62
|
+
const pyChanged = [];
|
|
63
|
+
for (const f of input.files) {
|
|
64
|
+
const hunks = input.hunks.get(f.path) ?? { added: [], deleted: [] };
|
|
65
|
+
if (isPy(f.path)) {
|
|
66
|
+
pyChanged.push(...pySymbols(input, f, hunks, toSymbol, symbols));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!isTsLike(f.path)) {
|
|
70
|
+
symbols.set(...moduleSymbol(f, hunks, input));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const newText = f.status === "deleted" ? null : input.newSrc.read(f.path);
|
|
74
|
+
const oldText = f.oldPath && input.oldSrc ? input.oldSrc.read(f.oldPath) : null;
|
|
75
|
+
const newDecls = newText !== null ? extractDecls(parse(f.path, newText)) : [];
|
|
76
|
+
const oldDecls = oldText !== null ? extractDecls(parse(f.oldPath, oldText)) : [];
|
|
77
|
+
const oldByKey = new Map(oldDecls.map((d) => [d.key, d]));
|
|
78
|
+
const newKeys = new Set(newDecls.map((d) => d.key));
|
|
79
|
+
for (const d of newDecls) {
|
|
80
|
+
const od = oldByKey.get(d.key);
|
|
81
|
+
if (input.mode === "teach") {
|
|
82
|
+
if (d.kind === "const" && !d.exported)
|
|
83
|
+
continue;
|
|
84
|
+
const s = toSymbol(f.path, d, "context");
|
|
85
|
+
symbols.set(s.id, s);
|
|
86
|
+
changedDecls.push({ file: f.path, decl: d, symbol: s });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const hasMembers = d.memberRanges.length > 0;
|
|
90
|
+
const added = hasMembers ? ownChanges(d, hunks.added) : countInRange(d.range, hunks.added);
|
|
91
|
+
const deleted = od ? (hasMembers ? ownChanges(od, hunks.deleted) : countInRange(od.range, hunks.deleted)) : 0;
|
|
92
|
+
const status = !od && (added > 0 || f.status === "added") ? "added" : added + deleted > 0 ? "modified" : null;
|
|
93
|
+
if (!status)
|
|
94
|
+
continue;
|
|
95
|
+
const s = toSymbol(f.path, d, status, added + deleted);
|
|
96
|
+
if (od) {
|
|
97
|
+
s.oldFile = f.oldPath;
|
|
98
|
+
s.oldRange = od.range;
|
|
99
|
+
}
|
|
100
|
+
symbols.set(s.id, s);
|
|
101
|
+
changedDecls.push({ file: f.path, decl: d, symbol: s });
|
|
102
|
+
}
|
|
103
|
+
if (input.mode === "diff") {
|
|
104
|
+
for (const od of oldDecls) {
|
|
105
|
+
if (newKeys.has(od.key))
|
|
106
|
+
continue;
|
|
107
|
+
const s = toSymbol(f.path, od, "removed", od.range.end - od.range.start + 1);
|
|
108
|
+
s.oldFile = f.oldPath;
|
|
109
|
+
s.oldRange = od.range;
|
|
110
|
+
symbols.set(s.id, s);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
lap("symbols");
|
|
115
|
+
// 2. Outgoing edges through the checker; context symbols for what changed code calls.
|
|
116
|
+
const ensureContext = (project, rel, decl) => {
|
|
117
|
+
const id = symbolId(rel, decl.key);
|
|
118
|
+
if (!symbols.has(id))
|
|
119
|
+
symbols.set(id, toSymbol(rel, decl, "context"));
|
|
120
|
+
return id;
|
|
121
|
+
};
|
|
122
|
+
const resolve = (project, node) => {
|
|
123
|
+
const checker = project.program().getTypeChecker();
|
|
124
|
+
let sym = checker.getSymbolAtLocation(node);
|
|
125
|
+
if (sym && sym.flags & ts.SymbolFlags.Alias)
|
|
126
|
+
sym = checker.getAliasedSymbol(sym);
|
|
127
|
+
let target = sym?.valueDeclaration ?? sym?.declarations?.[0];
|
|
128
|
+
// Services return `{ createGrant, patchGrant }`; follow the shorthand to the function itself.
|
|
129
|
+
if (target && ts.isShorthandPropertyAssignment(target)) {
|
|
130
|
+
target = checker.getShorthandAssignmentValueSymbol(target)?.valueDeclaration ?? target;
|
|
131
|
+
}
|
|
132
|
+
else if (target && ts.isPropertyAssignment(target) && ts.isIdentifier(target.initializer)) {
|
|
133
|
+
target = checker.getSymbolAtLocation(target.initializer)?.valueDeclaration ?? target;
|
|
134
|
+
}
|
|
135
|
+
if (!target)
|
|
136
|
+
return null;
|
|
137
|
+
const sf = target.getSourceFile();
|
|
138
|
+
const rel = project.rel(sf.fileName);
|
|
139
|
+
if (rel === null || project.program().isSourceFileDefaultLibrary(sf))
|
|
140
|
+
return null;
|
|
141
|
+
const entry = project.declsFor(rel);
|
|
142
|
+
if (!entry)
|
|
143
|
+
return null;
|
|
144
|
+
const decl = enclosing(entry.decls, entry.sf, target.getStart(entry.sf));
|
|
145
|
+
return decl ? { rel, decl } : null;
|
|
146
|
+
};
|
|
147
|
+
for (const { file, decl, symbol } of changedDecls) {
|
|
148
|
+
const project = projects.for(file);
|
|
149
|
+
const entry = project?.declsFor(file);
|
|
150
|
+
if (!project || !entry)
|
|
151
|
+
continue;
|
|
152
|
+
const live = entry.decls.find((d) => d.key === decl.key);
|
|
153
|
+
if (!live)
|
|
154
|
+
continue;
|
|
155
|
+
const addedLines = input.hunks.get(file)?.added ?? [];
|
|
156
|
+
const lineOf = (n) => entry.sf.getLineAndCharacterOfPosition(n.getStart(entry.sf)).line + 1;
|
|
157
|
+
// An unchanged call into unchanged code is noise in a diff; hover still explains it.
|
|
158
|
+
const relevant = (targetId, line, kind) => {
|
|
159
|
+
if (input.mode === "teach" || kind === "extends" || kind === "implements")
|
|
160
|
+
return true;
|
|
161
|
+
const known = symbols.get(targetId);
|
|
162
|
+
if (known && known.status !== "context")
|
|
163
|
+
return true;
|
|
164
|
+
return inRanges(line, addedLines) || inRanges(line - 1, addedLines) || inRanges(line + 1, addedLines);
|
|
165
|
+
};
|
|
166
|
+
const visit = (n) => {
|
|
167
|
+
// Members are their own symbols; don't attribute their calls to the class.
|
|
168
|
+
if (n !== live.node && live.memberRanges.length && (ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n) || ts.isPropertyDeclaration(n)))
|
|
169
|
+
return;
|
|
170
|
+
let target = null;
|
|
171
|
+
let kind = "calls";
|
|
172
|
+
if (ts.isCallExpression(n) || ts.isNewExpression(n)) {
|
|
173
|
+
target = ts.isPropertyAccessExpression(n.expression) ? n.expression.name : n.expression;
|
|
174
|
+
for (const arg of n.arguments ?? []) {
|
|
175
|
+
if (ts.isIdentifier(arg) || ts.isPropertyAccessExpression(arg)) {
|
|
176
|
+
const hit = resolve(project, ts.isPropertyAccessExpression(arg) ? arg.name : arg);
|
|
177
|
+
if (hit && relevant(symbolId(hit.rel, hit.decl.key), lineOf(arg), "calls") && hit.decl.kind !== "const" && hit.decl.kind !== "type" && hit.decl.kind !== "interface") {
|
|
178
|
+
addEdge(symbol.id, ensureContext(project, hit.rel, hit.decl), "calls", lineOf(arg), input.mode === "diff" && inRanges(lineOf(arg), addedLines) ? "added" : "same");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (ts.isJsxOpeningElement(n) || ts.isJsxSelfClosingElement(n)) {
|
|
184
|
+
target = n.tagName;
|
|
185
|
+
kind = "renders";
|
|
186
|
+
}
|
|
187
|
+
else if (ts.isTypeReferenceNode(n)) {
|
|
188
|
+
target = n.typeName;
|
|
189
|
+
kind = "uses";
|
|
190
|
+
}
|
|
191
|
+
else if (ts.isExpressionWithTypeArguments(n) && ts.isHeritageClause(n.parent)) {
|
|
192
|
+
target = n.expression;
|
|
193
|
+
kind = n.parent.token === ts.SyntaxKind.ExtendsKeyword ? "extends" : "implements";
|
|
194
|
+
}
|
|
195
|
+
if (target) {
|
|
196
|
+
const hit = resolve(project, target);
|
|
197
|
+
if (hit) {
|
|
198
|
+
const targetId = symbolId(hit.rel, hit.decl.key);
|
|
199
|
+
// Type uses only matter when the type is part of the change.
|
|
200
|
+
const known = symbols.get(targetId);
|
|
201
|
+
const interesting = kind !== "uses" || (known !== undefined && known.status !== "context");
|
|
202
|
+
const line = lineOf(n);
|
|
203
|
+
if (interesting && targetId !== symbol.id && relevant(targetId, line, kind)) {
|
|
204
|
+
addEdge(symbol.id, ensureContext(project, hit.rel, hit.decl), kind, line, input.mode === "diff" && inRanges(line, addedLines) ? "added" : "same");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
ts.forEachChild(n, visit);
|
|
209
|
+
};
|
|
210
|
+
ts.forEachChild(live.node, visit);
|
|
211
|
+
}
|
|
212
|
+
lap("outgoing");
|
|
213
|
+
// 3. Callers: who reaches the changed code from outside it.
|
|
214
|
+
if (input.mode === "diff") {
|
|
215
|
+
for (const { file, decl, symbol } of changedDecls) {
|
|
216
|
+
if (symbol.kind === "route" || symbol.kind === "class")
|
|
217
|
+
continue;
|
|
218
|
+
const project = projects.for(file);
|
|
219
|
+
const entry = project?.declsFor(file);
|
|
220
|
+
const live = entry?.decls.find((d) => d.key === decl.key);
|
|
221
|
+
if (!project || !entry || !live?.nameNode)
|
|
222
|
+
continue;
|
|
223
|
+
// Where to ask for references: the declaration, plus (for a lifted service function) the
|
|
224
|
+
// `return { patchGrant }` shorthand that callers actually reach through `store.patchGrant(...)`.
|
|
225
|
+
const positions = [live.nameNode.getStart(entry.sf)];
|
|
226
|
+
if (live.container) {
|
|
227
|
+
const holder = entry.decls.find((d) => d.key === live.container);
|
|
228
|
+
const visit = (n) => {
|
|
229
|
+
if (ts.isShorthandPropertyAssignment(n) && n.name.text === live.name)
|
|
230
|
+
positions.push(n.name.getStart(entry.sf));
|
|
231
|
+
ts.forEachChild(n, visit);
|
|
232
|
+
};
|
|
233
|
+
if (holder)
|
|
234
|
+
visit(holder.node);
|
|
235
|
+
}
|
|
236
|
+
const refs = [];
|
|
237
|
+
for (const at of positions) {
|
|
238
|
+
try {
|
|
239
|
+
refs.push(...(project.service.findReferences(project.abs(file), at) ?? []));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
/* a reference search that throws just finds nothing */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let count = 0;
|
|
246
|
+
for (const group of refs) {
|
|
247
|
+
for (const ref of group.references) {
|
|
248
|
+
if (ref.isDefinition || count >= MAX_CALLERS)
|
|
249
|
+
continue;
|
|
250
|
+
const rel = project.rel(ref.fileName);
|
|
251
|
+
if (rel === null)
|
|
252
|
+
continue;
|
|
253
|
+
const callerEntry = project.declsFor(rel);
|
|
254
|
+
if (!callerEntry)
|
|
255
|
+
continue;
|
|
256
|
+
const pos = ref.textSpan.start;
|
|
257
|
+
const caller = enclosing(callerEntry.decls, callerEntry.sf, pos);
|
|
258
|
+
if (!caller)
|
|
259
|
+
continue;
|
|
260
|
+
const callerId = symbolId(rel, caller.key);
|
|
261
|
+
if (callerId === symbol.id)
|
|
262
|
+
continue;
|
|
263
|
+
const kind = refKind(callerEntry.sf, pos);
|
|
264
|
+
if (!kind)
|
|
265
|
+
continue;
|
|
266
|
+
if (kind === "uses" && symbol.kind !== "interface" && symbol.kind !== "type")
|
|
267
|
+
continue;
|
|
268
|
+
const line = callerEntry.sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
269
|
+
const callerAdded = input.hunks.get(rel)?.added ?? [];
|
|
270
|
+
if (!symbols.has(callerId))
|
|
271
|
+
symbols.set(callerId, toSymbol(rel, caller, "context"));
|
|
272
|
+
addEdge(callerId, symbol.id, kind, line, inRanges(line, callerAdded) ? "added" : "same");
|
|
273
|
+
count++;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// 4. Calls that disappeared: compare call names across sides.
|
|
278
|
+
for (const s of symbols.values()) {
|
|
279
|
+
if ((s.status !== "modified" && s.status !== "removed") || !s.oldRange || !input.oldSrc || !isTsLike(s.file))
|
|
280
|
+
continue;
|
|
281
|
+
const oldText = input.oldSrc.read(s.oldFile ?? s.file);
|
|
282
|
+
if (!oldText)
|
|
283
|
+
continue;
|
|
284
|
+
const oldSf = parse(s.oldFile ?? s.file, oldText);
|
|
285
|
+
const oldDecl = extractDecls(oldSf).find((d) => symbolId(s.file, d.key) === s.id);
|
|
286
|
+
if (!oldDecl)
|
|
287
|
+
continue;
|
|
288
|
+
const before = calleeNames(oldSf, oldDecl.node);
|
|
289
|
+
let after = new Map();
|
|
290
|
+
if (s.status === "modified" && s.range) {
|
|
291
|
+
const newText = input.newSrc.read(s.file);
|
|
292
|
+
const newSf = newText ? parse(s.file, newText) : null;
|
|
293
|
+
const newDecl = newSf ? extractDecls(newSf).find((d) => symbolId(s.file, d.key) === s.id) : null;
|
|
294
|
+
if (newSf && newDecl)
|
|
295
|
+
after = calleeNames(newSf, newDecl.node);
|
|
296
|
+
}
|
|
297
|
+
for (const [name, line] of before) {
|
|
298
|
+
if (after.has(name))
|
|
299
|
+
continue;
|
|
300
|
+
const target = [...symbols.values()].find((t) => t.name === name && t.id !== s.id && (t.file === s.file || t.status !== "context"));
|
|
301
|
+
if (target)
|
|
302
|
+
addEdge(s.id, target.id, "calls", line, "removed");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
lap("callers+removed");
|
|
307
|
+
if (pyChanged.length) {
|
|
308
|
+
const index = input.pyIndex ?? new PyIndex(input.newSrc.files(), (f) => input.newSrc.read(f));
|
|
309
|
+
pyEdges(input, index, pyChanged, symbols, toSymbol, addEdge);
|
|
310
|
+
lap("python");
|
|
311
|
+
}
|
|
312
|
+
// 5. Doors: changed symbols nothing else in the change calls.
|
|
313
|
+
const all = [...symbols.values()];
|
|
314
|
+
const changed = new Set(all.filter((s) => s.status !== "context").map((s) => s.id));
|
|
315
|
+
for (const s of all) {
|
|
316
|
+
if (s.test || s.status === "removed" || s.kind === "type" || s.kind === "interface" || s.kind === "module" || s.kind === "const")
|
|
317
|
+
continue;
|
|
318
|
+
const scope = input.mode === "teach" ? true : changed.has(s.id);
|
|
319
|
+
if (!scope)
|
|
320
|
+
continue;
|
|
321
|
+
const incoming = [...edges.values()].filter((e) => e.to === s.id && e.kind !== "uses" && e.change !== "removed" && !symbols.get(e.from)?.test && (input.mode === "teach" || changed.has(e.from)));
|
|
322
|
+
s.entry = incoming.length === 0 && s.name !== "constructor" && (input.mode === "diff" || s.exported || s.kind === "route");
|
|
323
|
+
}
|
|
324
|
+
return { symbols: all, edges: [...edges.values()] };
|
|
325
|
+
}
|
|
326
|
+
function refKind(sf, pos) {
|
|
327
|
+
let node = findToken(sf, pos);
|
|
328
|
+
if (!node)
|
|
329
|
+
return null;
|
|
330
|
+
// Walk up through property access so obj.method() counts as a call of method.
|
|
331
|
+
while (node.parent && ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)
|
|
332
|
+
node = node.parent;
|
|
333
|
+
const p = node.parent;
|
|
334
|
+
if (!p)
|
|
335
|
+
return null;
|
|
336
|
+
if (ts.isImportSpecifier(p) || ts.isExportSpecifier(p) || ts.isImportClause(p) || ts.isNamespaceImport(p))
|
|
337
|
+
return null;
|
|
338
|
+
if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.expression === node)
|
|
339
|
+
return "calls";
|
|
340
|
+
if ((ts.isCallExpression(p) || ts.isNewExpression(p)) && p.arguments?.includes(node))
|
|
341
|
+
return "calls";
|
|
342
|
+
if (ts.isJsxOpeningElement(p) || ts.isJsxSelfClosingElement(p) || ts.isJsxClosingElement(p))
|
|
343
|
+
return ts.isJsxClosingElement(p) ? null : "renders";
|
|
344
|
+
if (ts.isTypeReferenceNode(p) || ts.isExpressionWithTypeArguments(p))
|
|
345
|
+
return "uses";
|
|
346
|
+
if (ts.isTaggedTemplateExpression(p))
|
|
347
|
+
return "calls";
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
function findToken(sf, pos) {
|
|
351
|
+
let found = null;
|
|
352
|
+
const visit = (n) => {
|
|
353
|
+
if (pos < n.getStart(sf) || pos >= n.getEnd())
|
|
354
|
+
return;
|
|
355
|
+
found = n;
|
|
356
|
+
ts.forEachChild(n, visit);
|
|
357
|
+
};
|
|
358
|
+
visit(sf);
|
|
359
|
+
return found;
|
|
360
|
+
}
|
|
361
|
+
/** Non-TS files get one symbol covering their changed region; the agent can split it. */
|
|
362
|
+
function moduleSymbol(f, hunks, input) {
|
|
363
|
+
const text = f.status === "deleted" ? null : input.newSrc.read(f.path);
|
|
364
|
+
const total = text?.split("\n").length ?? 0;
|
|
365
|
+
const span = (ranges, max) => {
|
|
366
|
+
if (!ranges.length)
|
|
367
|
+
return null;
|
|
368
|
+
const start = Math.max(1, Math.min(...ranges.map((r) => r.start)) - 3);
|
|
369
|
+
const end = Math.min(max || Infinity, Math.max(...ranges.map((r) => r.end)) + 3);
|
|
370
|
+
return { start, end: Math.max(start, end) };
|
|
371
|
+
};
|
|
372
|
+
const status = f.status === "added" ? "added" : f.status === "deleted" ? "removed" : input.mode === "teach" ? "context" : "modified";
|
|
373
|
+
const range = status === "removed" ? null : input.mode === "teach" ? { start: 1, end: Math.max(1, total) } : span(hunks.added, total) ?? { start: 1, end: Math.min(Math.max(total, 1), 40) };
|
|
374
|
+
const id = symbolId(f.path, "module");
|
|
375
|
+
return [
|
|
376
|
+
id,
|
|
377
|
+
{
|
|
378
|
+
id,
|
|
379
|
+
name: path.posix.basename(f.path),
|
|
380
|
+
container: null,
|
|
381
|
+
kind: "module",
|
|
382
|
+
file: f.path,
|
|
383
|
+
range,
|
|
384
|
+
oldFile: f.oldPath,
|
|
385
|
+
oldRange: span(hunks.deleted, Infinity),
|
|
386
|
+
status,
|
|
387
|
+
signature: null,
|
|
388
|
+
exported: false,
|
|
389
|
+
entry: false,
|
|
390
|
+
summary: null,
|
|
391
|
+
changedLines: f.additions + f.deletions,
|
|
392
|
+
test: isTestFile(f.path),
|
|
393
|
+
},
|
|
394
|
+
];
|
|
395
|
+
}
|
|
396
|
+
function timer() {
|
|
397
|
+
let t = performance.now();
|
|
398
|
+
return (label) => {
|
|
399
|
+
if (process.env.DS_DEBUG)
|
|
400
|
+
console.error(`[graph] ${label}: ${Math.round(performance.now() - t)}ms`);
|
|
401
|
+
t = performance.now();
|
|
402
|
+
};
|
|
403
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { countInRange, inRanges } from "./git.js";
|
|
2
|
+
import { enclosingPy, parsePy, PyIndex } from "./py.js";
|
|
3
|
+
const MAX_CALLERS = 12;
|
|
4
|
+
const id = (file, key) => `${file}#${key}`;
|
|
5
|
+
function ownChanges(d, ranges) {
|
|
6
|
+
let n = 0;
|
|
7
|
+
for (let line = d.range.start; line <= d.range.end; line++)
|
|
8
|
+
if (inRanges(line, ranges) && !inRanges(line, d.memberRanges))
|
|
9
|
+
n++;
|
|
10
|
+
return n;
|
|
11
|
+
}
|
|
12
|
+
/** Statuses for one Python file's declarations, both sides. */
|
|
13
|
+
export function pySymbols(input, f, hunks, toSymbol, symbols) {
|
|
14
|
+
const newText = f.status === "deleted" ? null : input.newSrc.read(f.path);
|
|
15
|
+
const oldText = f.oldPath && input.oldSrc ? input.oldSrc.read(f.oldPath) : null;
|
|
16
|
+
const newDecls = newText !== null ? parsePy(f.path, newText).decls : [];
|
|
17
|
+
const oldDecls = oldText !== null ? parsePy(f.oldPath, oldText).decls : [];
|
|
18
|
+
const oldByKey = new Map(oldDecls.map((d) => [d.key, d]));
|
|
19
|
+
const newKeys = new Set(newDecls.map((d) => d.key));
|
|
20
|
+
const changed = [];
|
|
21
|
+
for (const d of newDecls) {
|
|
22
|
+
if (input.mode === "teach") {
|
|
23
|
+
if (!d.exported && d.kind === "function" && !d.container)
|
|
24
|
+
continue;
|
|
25
|
+
const s = toSymbol(f.path, d, "context");
|
|
26
|
+
symbols.set(s.id, s);
|
|
27
|
+
changed.push({ file: f.path, decl: d, symbol: s });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const od = oldByKey.get(d.key);
|
|
31
|
+
const own = d.memberRanges.length > 0;
|
|
32
|
+
const added = own ? ownChanges(d, hunks.added) : countInRange(d.range, hunks.added);
|
|
33
|
+
const deleted = od ? (od.memberRanges.length ? ownChanges(od, hunks.deleted) : countInRange(od.range, hunks.deleted)) : 0;
|
|
34
|
+
const status = !od && (added > 0 || f.status === "added") ? "added" : added + deleted > 0 ? "modified" : null;
|
|
35
|
+
if (!status)
|
|
36
|
+
continue;
|
|
37
|
+
const s = toSymbol(f.path, d, status, added + deleted);
|
|
38
|
+
if (od) {
|
|
39
|
+
s.oldFile = f.oldPath;
|
|
40
|
+
s.oldRange = od.range;
|
|
41
|
+
}
|
|
42
|
+
symbols.set(s.id, s);
|
|
43
|
+
changed.push({ file: f.path, decl: d, symbol: s });
|
|
44
|
+
}
|
|
45
|
+
if (input.mode === "diff") {
|
|
46
|
+
for (const od of oldDecls) {
|
|
47
|
+
if (newKeys.has(od.key))
|
|
48
|
+
continue;
|
|
49
|
+
const s = toSymbol(f.path, od, "removed", od.range.end - od.range.start + 1);
|
|
50
|
+
s.oldFile = f.oldPath;
|
|
51
|
+
s.oldRange = od.range;
|
|
52
|
+
symbols.set(s.id, s);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return changed;
|
|
56
|
+
}
|
|
57
|
+
/** Outgoing calls, callers, and calls that disappeared, for the changed Python symbols. */
|
|
58
|
+
export function pyEdges(input, index, changed, symbols, toSymbol, addEdge) {
|
|
59
|
+
const ensure = (file, d) => {
|
|
60
|
+
const sid = id(file, d.key);
|
|
61
|
+
if (!symbols.has(sid))
|
|
62
|
+
symbols.set(sid, toSymbol(file, d, "context"));
|
|
63
|
+
return sid;
|
|
64
|
+
};
|
|
65
|
+
// What changed code calls.
|
|
66
|
+
for (const { file, decl, symbol } of changed) {
|
|
67
|
+
const pf = index.get(file);
|
|
68
|
+
if (!pf)
|
|
69
|
+
continue;
|
|
70
|
+
const added = input.hunks.get(file)?.added ?? [];
|
|
71
|
+
for (const c of pf.calls) {
|
|
72
|
+
if (c.line < decl.range.start || c.line > decl.range.end || inRanges(c.line, decl.memberRanges))
|
|
73
|
+
continue;
|
|
74
|
+
const hit = index.resolve(file, c.chain, c.line);
|
|
75
|
+
if (!hit)
|
|
76
|
+
continue;
|
|
77
|
+
const targetId = id(hit.file, hit.decl.key);
|
|
78
|
+
if (targetId === symbol.id)
|
|
79
|
+
continue;
|
|
80
|
+
const known = symbols.get(targetId);
|
|
81
|
+
const near = inRanges(c.line, added) || inRanges(c.line - 1, added) || inRanges(c.line + 1, added);
|
|
82
|
+
if (input.mode === "diff" && !(known && known.status !== "context") && !near)
|
|
83
|
+
continue;
|
|
84
|
+
addEdge(symbol.id, ensure(hit.file, hit.decl), "calls", c.line, input.mode === "diff" && inRanges(c.line, added) ? "added" : "same");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (input.mode !== "diff")
|
|
88
|
+
return;
|
|
89
|
+
// Who calls the changed code, from anywhere in the repo.
|
|
90
|
+
for (const { file, decl, symbol } of changed) {
|
|
91
|
+
if (symbol.kind === "class")
|
|
92
|
+
continue;
|
|
93
|
+
const callName = decl.name === "__init__" && decl.container ? decl.container.split(".").at(-1) : decl.name;
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const other of index.all()) {
|
|
96
|
+
if (count >= MAX_CALLERS)
|
|
97
|
+
break;
|
|
98
|
+
const text = input.newSrc.read(other);
|
|
99
|
+
if (!text || !text.includes(callName))
|
|
100
|
+
continue;
|
|
101
|
+
const pf = index.get(other);
|
|
102
|
+
if (!pf)
|
|
103
|
+
continue;
|
|
104
|
+
for (const c of pf.calls) {
|
|
105
|
+
if (c.chain.split(".").at(-1) !== callName)
|
|
106
|
+
continue;
|
|
107
|
+
const hit = index.resolve(other, c.chain, c.line);
|
|
108
|
+
if (!hit || id(hit.file, hit.decl.key) !== symbol.id)
|
|
109
|
+
continue;
|
|
110
|
+
const caller = enclosingPy(pf.decls, c.line);
|
|
111
|
+
if (!caller)
|
|
112
|
+
continue;
|
|
113
|
+
const callerId = id(other, caller.key);
|
|
114
|
+
if (callerId === symbol.id)
|
|
115
|
+
continue;
|
|
116
|
+
const added = input.hunks.get(other)?.added ?? [];
|
|
117
|
+
addEdge(ensure(other, caller), symbol.id, "calls", c.line, inRanges(c.line, added) ? "added" : "same");
|
|
118
|
+
if (++count >= MAX_CALLERS)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Calls that disappeared.
|
|
124
|
+
for (const s of symbols.values()) {
|
|
125
|
+
if ((s.status !== "modified" && s.status !== "removed") || !s.oldRange || !input.oldSrc || !(s.file.endsWith(".py") || s.file.endsWith(".pyi")))
|
|
126
|
+
continue;
|
|
127
|
+
const oldText = input.oldSrc.read(s.oldFile ?? s.file);
|
|
128
|
+
if (!oldText)
|
|
129
|
+
continue;
|
|
130
|
+
const before = PyIndex.calleeNames(parsePy(s.file, oldText), s.oldRange);
|
|
131
|
+
const pf = s.range ? index.get(s.file) : null;
|
|
132
|
+
const after = pf && s.range ? PyIndex.calleeNames(pf, s.range) : new Map();
|
|
133
|
+
for (const [name, line] of before) {
|
|
134
|
+
if (after.has(name))
|
|
135
|
+
continue;
|
|
136
|
+
const target = [...symbols.values()].find((t) => t.name === name && t.id !== s.id && (t.file === s.file || t.status !== "context"));
|
|
137
|
+
if (target)
|
|
138
|
+
addEdge(s.id, target.id, "calls", line, "removed");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|