Releases: finnmglas/desprawl
Release list
First Full Release
Everything polished, and ready to be used by you.
✨ Opus 5 Summary
Many major fixes and polishes. C#, .NET and the single file components, every number saying which way it went, and a picture you can arrange by hand
C#, .NET and XAML
- a
usinglands on the files that declare that namespace, since several folders fill one and no path rule can say which: on PowerToys that is 10.5k internal edges to 13.4k, andMicrosoft.PowerToysstops reading as an outside package on 604 files - markup is read too:
x:Classlinks a view to the file behind it,clr-namespace:and WinUI'susing:to the controls it places, and a merged dictionary through apack://orms-appx:///uri - a member is told from the statement written the same way, so a constructor, an expression body and an interface member declare while
return Compute(1);does not, and a class whose brace opens on the line below spans its own body .csproj, central package management,packages.configand the nuget store give versions, licences and advisories,dotnet testcounts its cases by the attribute each runner declares them with, and an ASP.NET route reads[controller]as the class it names
Vue, Svelte, and one map per language
- a single file component is its script, so a template is never code and a line still says where it is: 15 files read to 35 on a vue app, and 51% call coverage to 92%
@/xis not a package, since nothing installs under a bare scope, so it resolves to src the way every bundler that offers it means- a name a factory hands back is a declaration other files can call, which is 33k more call edges on vscode
- python packages found by their
__init__.pyand rust crates at the root their manifest declares: on ripgrep, 425 import edges to 801, and 46 "fix this import" tasks on code that compiles gone with them - dart and flutter resolve through
package:names off everypubspec.yaml
Numbers that say which way they went
- an arrow on every kpi, over a window you pick: none, 24h, 7d, a month, a rolling year or the year so far
- lines and commits come off the log, and the other twenty off a throwaway checkout of the commit that window opens on
- tests are counted in whatever language they were written in, pytest,
go test,cargo testanddotnet testalike, with the command that would run them - a python dependency knows its version, its licence and its size, off the venv and the lockfiles
A picture you can arrange, and settings a link can name
- drag a dot and it moves inside the module holding it, drag a module and its dots go with it, drag a repo and everything in it keeps its arrangement and travels: panning is what the background does
- panels only move when asked, so dragging, reordering and hiding stay off until the settings turn them on
#dialog=settingsand#dialog=exportare linkable and survive a reload, and the ellipsis menu is three buttons in the header- the api graph is one more file the export panel hands you, and the timeline reads its shares back as lines on a second axis
.desprawlignore, one exclude a reader can write down for the folder no heuristic will ever know about
Fixes worth naming
- a helper collected every call that shared its name, so one
def lowertook everyx.lower()in the repo - somebody else's code was counted as this project's: a repo committing its node_modules read as 437k lines for a fifteen file project
- dead code that was provably alive: a declaration where a value goes runs where it sits, a caller through a barrel is a caller, and a test's own declarations are never delete tasks
- a route decorator was read twice, once as the endpoint it is and once as the request it is not
- a document describing another repo's api stood as its server: code answers, a document describes
- call sites with no readable path were dropped in silence, and are counted and reported now
- every stage read and scrubbed the same file again: one bounded cache, 11% and 18% off the api view on two repos, byte identical output
- smaller: a pdf read as a language, a flutter repo read as Kotlin,
src/sourcesread as "Source sources", and svg path data outranked real copy in the repeated-text panel
Try it without installing anything: https://finnmglas.github.io/desprawl/ reads itself, vscode, and a backend with two frontends calling it.
Prerelease Six - api graph and unification
We match every http call to the endpoint it hits now, draw the graph at module grain!
✨ Opus 5 Summary
The api graph, a picture you can read at module grain, and a folder of repos read as one
The api graph
- every endpoint a repo serves matched to every http call site that reaches it, drawn in red as the one edge that crosses a repo, so a folder of repos shows its frontend reaching into its backend
- endpoints come off routers, their prefixes and file based routes across sixteen frameworks; call sites off fetch, axios, requests, httpx, retrofit, okhttp, reqwest and the repo's own wrappers
- OpenAPI, Swagger and
.protoread as endpoints, Bruno, Postman and.httpcollections as call sites - a spec also says which hosts the fleet answers on, which is how an internal call is told from a call to OpenAI
desprawl apiprints both sides
A picture you can read
- module grain draws modules as boxes, each carrying its name, size and shape, and it is what the graph opens on
- a line is something you can open: hovering names both ends and what it carries, clicking lists the imports, calls and requests on it and every path along it. Reachable on 22% of the picture rather than 2%
- laid out to be read, not to be tall: each repo gets a lane sized from its own content, the lanes pack into rows, and nothing that calls out is drawn above or right of something that answers
- three tabs: Overview, Graph, Tasks. History, Files, Modules and Execution became panels, and old links still land where they meant to
- panels drag, reorder and hide per tab, saved between runs
A folder of repos, read as a set
- the picker in the header ticks which repos the page is about, and every panel follows it
- picking filters what was already read rather than reading again: unticking one of five went from twelve seconds to three
- a folder claims no name, version or licence of its own
Things you can build on
import { analyze, build, calls, cycles, fold, knowledge } from "desprawl", with types. One entry point, everything else internal- every
--jsonis an envelope,{ desprawl, kind, repo, made, data }, and--limitand the error shape are the same across all views - panel search finds any panel on any tab by what it is called and what it holds
- tables window past 60 rows, with frozen column widths and the total pinned to the floor of the scroll
- exports moved into one dialog, and controls sit on the panel they change
- the repo is laid out in folders now, every declaration over 300 lines and every file over 700 split behind it, so desprawl reports no size, shape or cycle work on itself
Fixes worth naming
- python bodies ended at their first line, so two thirds of the call graph was attributed to a file's top level: on a django backend that took call coverage from 72% to 92%
- C and C++ saw only definitions in the first column, missing every method in a class and every prototype in a header: an arduino firmware went from 22% to 68%
- a repo past thirty thousand files ran out of heap building its call graph. The linux kernel now reads as 1.4m declarations and 3.9m calls, and a windowed table's first paint on 150k declarations went from 30 seconds to under one
- every tailwind class list read as repeated text: 238 of 287 findings on one frontend were className attributes
--anondid not anonymise. It left the per-identity list beside the blanked one, and only ran when a page was being written- a folder of repos answered with the first repo's numbers for the timeline, commits, tests and size, and returned a 500 for dependencies and history
- an
export { a, b }swallowed the next import, a jvm wildcard import named nothing, and PHP single quotes read as character literals - a crate was called
npm:tokio, and a small repo in a folder collapsed to one module - the exporters lied about what a table held:
1.10shipped as1.1, invalid TOML for backslashes and newlines, tabs shifting every column after them, and no guard on a cell a spreadsheet would run
Fifth Prerelease - Visual Graph, AI Tasks, Multilanguage
We can draw pretty graphs now and detect it for more languages - and send agents to do tasks from the task panel!
✨ Fable 5 Summary
Ten languages in one graph, the tabs in the terminal, agent runs watched live
Graphs in ten languages
- TypeScript, Rust, Python, Java/Kotlin/Scala, Go, Swift, C#, Ruby, PHP and C/C++ in one import graph, each readable alone
- every language resolved its own way:
super::names the folder in rust, a dotted path walks packages in python and the jvm, an angled include is the toolchain's unless the repo holds it - strings and comments scrubbed per language first, so a quoted path is not an import and a commented one is nothing
- the execution tab follows calls across all of them, and a mixed repo is one picture
The tabs, in the terminal
- nine named views: tasks, architecture, modules, execution, deps, stack, sprawl, history, knowledge
desprawl sprawlnames repeated literals, copied blocks and files that are mostly comment- every view takes
--jsonwith exact numbers, so CI reads the same thing the tab shows desprawl knowledgewrites the repo as things and links, at module, file or function grain
Agent runs, watched from the panel
- a run streams into the panel as it happens: what it did, what failed, what it cost
- every agent binary on PATH is its own entry with its own config, so two accounts never mix
- closing the tab leaves a running agent alone, and desprawl stays up until it finishes
- fix buttons on tasks hand the finding straight to the agent, repo in hand
Fixes worth naming (an adversarial sweep: 35 files, +301/−270)
- bot-author detection lost every commit after the first to a stray newline, so AI-signed counts read near zero
- a tsconfig path without a star matched by prefix:
"react"quietly capturedreact-dom - go read
return "hello"as an import, and python'strings'leaked their contents as code [dependencies.serde]never reached the table, andrequests[socks]>=2broke on its own brackets- advisories for uninstalled packages were fetched, then dropped on the way to the row
- a missing
pnpmkilled the whole server, anddesprawl viewprinted the terminal report instead of opening one - one person committing seven days of a week read as seven devs
Fourth Prerelease - View, Export, Deps, UX
Enhanced the UI lots, and its insights and features (+actions)
✨ Opus 5 Summary
Cycles, read off the files
- a folder loop and a real import cycle are different things, and only one of them runs
- 9 of 12 real cycles across our test repos were invisible to every grouping, hidden inside a single folder
- vscode: 142 rings over 1,096 files, every one of them invisible at top-folder grain
- ghloc-web reported a 5-group "loop at runtime" while having zero file cycles
- the grid crosses out what genuinely runs, amber is now "not isolated" rather than "loop"
Dependencies, licences and CVEs
- every package on disk, not only the ones the manifest names: 185 → 1,513 on example
- licences read from each installed package, grouped by what they ask of the code around them
- advisories from osv.dev against the exact installed version, not the package name
- example's only AGPL package is transitive, so a direct-only check reported zero copyleft
- last release date per named package, so a quiet two years is visible
Tests and repo actions
- test files and cases counted by reading, coverage parsed from a report already on disk
- ▶ buttons run the suite, or synthesise a coverage command per runner when none exists
- git and project scripts run from the interface; servers start, stream and stop like Ctrl+C
- a stop signals the process group and then every descendant, so nothing keeps the port
Getting things out
- every panel as CSV, TSV, JSON, TOML, Markdown or Excel; every drawing as PNG, JPEG, WebP or SVG
- the whole report as a PDF your own browser prints, so the text stays text
- --anon drops every commit address, and the avatars with it, since both come from one
- --out FILE writes where told, so CI never parses a path out of stdout
- published from the repo itself on every push: https://finnmglas.github.io/desprawl/
Fixes worth naming
- desprawl could not see its own src/graph.ts: the bundle detector matched the regex that defines it
- import { type X } counted as a runtime edge, so type-only imports read as real ones
- a partial clone made --numstat refetch the history over the network, forever
- the test suite left a temp repo per call: 18,772 of them, and /tmp out of inodes
Third Prerelease - Import-Graphs and Modules
Now we create import-graphs and analyze them into modules and resolvable module loops. Quite cool!
✨ Opus 5 Summary
A new Modules tab
- every import in the repo, resolved to the file it names, then folded into groups you can read
- auto picks the folders by weight, so no group holds a tenth of the repo and none is a single file
- fixed depths are still there: top folder, folder, subfolder, file
- a group's level is how far its own dependencies reach, so folder nesting cannot fake it
- inside against outside per group: what never leaves it, what does, what arrives, what it installs
- each group reads as Module, Sprawl or Entrypoint from that balance alone
Loops, and what opens them
- groups that import each other, with the imports whose removal provably leaves nothing looping
- each one says whether it is type only, which the compiler erases, or real work
- and whether it breaks the loop on its own, checked against the graph rather than claimed
Every dependency at once
- one grid instead of a hairball, ordered bottom of the stack first, sortable and searchable
- drawn on a canvas, so it stays quick where a thousand table cells would not
The graph itself
- path aliases, extends, workspace packages, index files and the esm .js to .ts rewrite
- bundled output and declaration files left out, comments and strings cannot fake an import
- 100% of imports resolved on every repo tested, and whatever cannot be is reported with a reason
- downloadable as json, next to a new one for the git log and its metadata
Smaller things
- files are named 20260809--, and the browser tab carries the repo name
- a focus display mode keeps logos and language colours without the shouting
- tab bars scroll on a phone instead of overflowing
- the files panel splits code, comments and blank lines, and shading follows whichever you pick
Cross-Platform UX Patch - it works on my machine!
✨ Opus 5 Summary
Default command switched
desprawlnow opens the explorer, which is the thing worth seeing firstdesprawl cliprints the terminal report,--jsonstill implies itdesprawl viewkept as an alias
Says what is missing
- an old node or a missing git is named at startup, with the command that installs it,
picked from the package manager actually present (apt, dnf, pacman, zypper, apk,
brew, winget, xcode-select) - "not a git repository" and "no commits yet" read as sentences, not as git's stderr
Windows and macOS
viewno longer dies on Windows:startis a shell builtin, not a binary, and the
failed spawn was an uncaught exception- preferences go to
%APPDATA%on Windows,$XDG_CONFIG_HOMEelsewhere
Second Prerelease - Analyze large ts codebase identity visually
Run npm i desprawl, then desprawl view in any git repo. Analyses git-tracked files.
✨ Opus 5 Summary
Project metadata
- what the repo is, read off manifests and marker files: language and strictness,
package manager, pinning, frameworks, state, ui, what it connects to, auth,
telemetry, ports, build, testing, lint, format, ci, containers, api collections - every package.json in the tree, so a monorepo is read whole
- identity above it: name, version, licence, and what it holds (frontend, backend,
cli, monorepo, mobile, desktop, infra)
Assistance
- which ai coding tools left a trace, from checked in rules (CLAUDE.md, AGENTS.md,
.cursor, .aider, .windsurfrules and twenty more) and from commit signatures Co-authored-by,Assisted-byandGenerated withtrailers, plus bot author
addresses. Prose is never matched, so a commit about an assistant is not one by one- how much of the read history each tool signed
Explorer
desprawl viewnow serves live and reanalyses on request, so refresh picks up new
commits without a restart. One fixed origin, so display settings persist between runs- drag across the time series to zoom it, double click or reset to go back
- click a commit in the history graph for its files, added and removed
- time series with selectable granularity, a log scale, and per-series toggles
- every column explains itself on hover, every table copies as tsv or csv
Scale
- runs on the linux kernel: 87k files, 1.4m commits. The log reads a capped window
and says so, while commits, contributors and repo size over all time come from
cheaper passes
No runtime dependencies.
Initial Release - Analyze loc and contributors
First release. Run npm i desprawl, then desprawl in any git repo. Analyses git-tracked files.
✨ Opus 5 Summary
Scope
- loc, comment and blank split per language and per folder
- chars and estimated tokens, at four characters per token
- nest, the mean indentation depth of code lines
History
- commits, contributors merged by mailmap email, with renames followed
- commits, churn and last-touched per file and folder
- daily commit and churn time series in the JSON
Surfaces
desprawlprints the report,--depth,--top,--digits,--rawdesprawl --jsonemits the whole thing, version stamped, tree and series includeddesprawl viewwrites one self contained html file and opens it, with an overview,
a folder explorer you can click into with the language split per folder, and the
commit history drawn as a branch graph
No runtime dependencies. One git log and one git ls-files per run, roughly three
seconds on 400k lines.