Skip to content

Releases: finnmglas/desprawl

First Full Release

Choose a tag to compare

@finnmglas finnmglas released this 23 Aug 21:35

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 using lands 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, and Microsoft.PowerToys stops reading as an outside package on 604 files
  • markup is read too: x:Class links a view to the file behind it, clr-namespace: and WinUI's using: to the controls it places, and a merged dictionary through a pack:// or ms-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.config and the nuget store give versions, licences and advisories, dotnet test counts 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%
  • @/x is 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__.py and 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 every pubspec.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 test and dotnet test alike, 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=settings and #dialog=export are 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 lower took every x.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/sources read 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.

https://www.npmjs.com/package/desprawl

Prerelease Six - api graph and unification

Choose a tag to compare

@finnmglas finnmglas released this 22 Aug 21:14

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 .proto read as endpoints, Bruno, Postman and .http collections 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 api prints 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 --json is an envelope, { desprawl, kind, repo, made, data }, and --limit and 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
  • --anon did 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.10 shipped as 1.1, invalid TOML for backslashes and newlines, tabs shifting every column after them, and no guard on a cell a spreadsheet would run

https://www.npmjs.com/package/desprawl

Fifth Prerelease - Visual Graph, AI Tasks, Multilanguage

Choose a tag to compare

@finnmglas finnmglas released this 13 Aug 22:45

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 sprawl names repeated literals, copied blocks and files that are mostly comment
  • every view takes --json with exact numbers, so CI reads the same thing the tab shows
  • desprawl knowledge writes 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 captured react-dom
  • go read return "hello" as an import, and python 'strings' leaked their contents as code
  • [dependencies.serde] never reached the table, and requests[socks]>=2 broke on its own brackets
  • advisories for uninstalled packages were fetched, then dropped on the way to the row
  • a missing pnpm killed the whole server, and desprawl view printed the terminal report instead of opening one
  • one person committing seven days of a week read as seven devs

https://www.npmjs.com/package/desprawl

Fourth Prerelease - View, Export, Deps, UX

Choose a tag to compare

@finnmglas finnmglas released this 11 Aug 03:16

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

https://www.npmjs.com/package/desprawl

Third Prerelease - Import-Graphs and Modules

Choose a tag to compare

@finnmglas finnmglas released this 09 Aug 20:59

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

https://www.npmjs.com/package/desprawl

Cross-Platform UX Patch - it works on my machine!

Choose a tag to compare

@finnmglas finnmglas released this 08 Aug 18:51

✨ Opus 5 Summary

Default command switched

  • desprawl now opens the explorer, which is the thing worth seeing first
  • desprawl cli prints the terminal report, --json still implies it
  • desprawl view kept 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

  • view no longer dies on Windows: start is a shell builtin, not a binary, and the
    failed spawn was an uncaught exception
  • preferences go to %APPDATA% on Windows, $XDG_CONFIG_HOME elsewhere

https://www.npmjs.com/package/desprawl

Second Prerelease - Analyze large ts codebase identity visually

Choose a tag to compare

@finnmglas finnmglas released this 08 Aug 17:14

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-by and Generated with trailers, 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 view now 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.

https://www.npmjs.com/package/desprawl

Initial Release - Analyze loc and contributors

Choose a tag to compare

@finnmglas finnmglas released this 08 Aug 09:29

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

  • desprawl prints the report, --depth, --top, --digits, --raw
  • desprawl --json emits the whole thing, version stamped, tree and series included
  • desprawl view writes 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.

https://www.npmjs.com/package/desprawl