Releases: syrian963/django-chainsaw-mcp
Release list
v0.1.5
[0.1.5] - 2026-09-10
A delivery release. Both changes were already on main; neither had reached
anybody, because the places people actually read are the published package and
the PyPI page, and a merge updates neither.
Changed
-
The PyPI page was still showing the 615-line README. The front page was
rewritten down to 270 lines — the narrative moved todocs/why.md, the
pull-request workflow came up out of line 411, and a sentence saying there
is no model in the loop was added because its absence had measurably
confused a reader. All of that landed onmainafter 0.1.4 was tagged, so
the long description on PyPI was the old one.That matters more than it sounds: the repository went public five days ago
and is not indexed yet, while the PyPI page is. For now it is the only page
a search engine can find, and it was showing the version of the text this
release exists to replace.
Fixed
-
A version attribution nobody had checked.
defeated_prefetchesshipped
saying.count()and.exists()had been answered from the prefetch cache
"since Django 4.1". The behaviour is real and was measured; the version was
not. Django's 4.1 release notes do not mention it, and the claim went into a
module docstring, a documentation page and a changelog entry on the strength
of a recollection.Measured instead: identical results on 4.2, the oldest version this supports,
and on 6.1. That is the claim the pages make now. Every method the check does
report re-queries on both, so no finding depended on the wrong sentence — but
a repository that asks other people to distrust unmeasured claims does not
get to ship one. The corrected docstring only reaches anyone who installs the
package with this release; 0.1.4 on PyPI still carries the wrong sentence.
v0.1.4 — a prefetch paid for and then thrown away
[0.1.4] - 2026-09-09
Added
-
defeated_prefetches: a relation that was prefetched and then re-queried
anyway.prefetch_relatedfills a cache on each parent object, and the
related manager hands it back only to the accessors that can read it. Ask it
anything else and it goes to the database once per parent, with the prefetch
query already paid for on top - so the loop costs more than it would have
with no prefetch at all, while reading like an optimisation.Which accessors, measured rather than assumed: ten parents with three
children each, queries counted withCaptureQueriesContexton Django 6.1.
.filter(),.exclude(),.order_by(),.first(),.last(),.only(),
.defer(),.values(),.values_list(),.distinct(),
.select_related(),.annotate()and.reverse()each cost 12 queries
where the cache costs 2..count(),.exists(),.all()and a slice cost
2 - the related manager has answeredcountandexistsfrom the prefetch
since Django 4.1, which is not obvious, and reporting them would be a
finding whose fix changes nothing. Only the first group is reported, and
every method in it re-queries on earlier Django versions too.Reported only where the prefetch and the accessor are provably the same
object: a name bound in the same scope, or the loop variable iterating one.
That boundary was measured too. Matching on the relation name anywhere in
the same file found 79 sites across nine large projects; five were read by
hand and four of the five were coincidence - the same relation name,
unrelated objects. The scope-local rule finds 7 in the same nine projects,
and all 7 were read and confirmed: Wagtail, Saleor (four, one of them under
a comment that says "get cached variant with related fields"), pretix and
DefectDojo. A prefetch in a view and the accessor in a template tag is the
same defect and is not reported, because the fix is to delete or rewrite a
line and a guess is not a good enough reason to suggest that.nplusonefinds the neighbouring problem - eagerly loaded and never touched- at runtime, and
unused_eager_loadinghere answers that one statically
for DRF viewsets. Neither answers this one, where the prefetch is used and
the accessor cannot read it.
- at runtime, and
Fixed
-
The check found the same relation name in a different function and called
it one object. The first version walked the module body into the functions
it contained, so a name bound in one function matched an accessor in
another. Saleor has exactly that pair a hundred lines apart, and it was
reported as a finding. Each function is now scanned as a scope of its own.
The cost is a name a nested function closes over, which is the right trade:
a missed finding costs less than a confident wrong one. -
Three of its own tests asserted on line numbers that were off by one or
two, so they passed without ever reaching the code they were written to
guard. Found by reverting each guard in turn and watching which tests failed- two did not. The tests now key on the enclosing function name instead, and
all five go red when their guard is removed.
- two did not. The tests now key on the enclosing function name instead, and
-
The server introduced itself as 0.1.0 for three releases. The version was
written out by hand in three places and only one of them was ever updated, so
a client asking the server who it was got an answer that had been wrong since
the first tag. There is now one__version__, read from the installed
package metadata, and a documentation gate that fails when a version is
spelled out anywhere it can be derived. -
The local runner did not lint, so CI failed on an unused import.
Seventeen suites green here, three jobs red on GitHub nine seconds in, over
one leftoverfrom pathlib import Path. CI runsruff check src/ tests/as
its first step andrun_tests.shnever did. A local runner that passes what
the pipeline fails is worse than no local runner, because it is trusted; it
now runs the same invocation, first.
Changed
- The README stopped promising an invocation that cannot work. It offered
uvx django-chainsaw-mcp, anduvxbuilds an isolated environment - which
is exactly what a tool that has to import your project cannot use. It starts
and then fails every check. The install section now says plainly that it must
run in an interpreter that can import the project, gives the two ways to do
that, and a documentation gate rejects a bareuvxinvocation in a code
fence.
v0.1.3
[0.1.3] - 2026-09-09
Changed
- The release publishes itself to the MCP registry. Three releases meant
three GitHub device flows, and the registry token expired during two of them- in the window between the PyPI upload and the registry call, which nobody
can shorten by hand. The workflow now usesmcp-publisher login github-oidc, the same mechanism PyPI trusted publishing already uses
here: the runner proves who it is, and there is no token to expire, leak
or rotate. It runs after the PyPI job, because the registry checks that
the version the manifest names really exists, and it refuses to run if
the tag andserver.jsondisagree.
- in the window between the PyPI upload and the registry call, which nobody
v0.1.2
[0.1.2] - 2026-09-09
Found by installing 0.1.1 from PyPI into a clean environment and running it
against a project it had never seen. Every test before this one used the
source tree, which is not what anybody else gets.
Fixed
-
A third of the tenancy suggestions proposed no change at all.
re.sub
returns the subject unchanged when the pattern does not match, and the
pattern knows.all(),.filter(,.get(and.exclude(. A queryset that
starts at a custom manager method -Profile.objects.for_user(request.user)- matches none of them, so the rewrite came back identical to the original
and was printed as a diff whose-and+lines are the same text.
On healthchecks, 13 of 37. Nothing was ever written, because
--writeonly
touches mechanical fixes and these are advisory, so the damage was to the
report rather than to anybody's source - which does not make it a smaller
lie. The finding stays, because the queryset really is unscoped; what goes
is the pretence that there is a mechanical rewrite, replaced by a sentence
saying where the scoping actually belongs. Found by running the published
package against a project it had never seen. - matches none of them, so the rewrite came back identical to the original
v0.1.1
[0.1.1] - 2026-09-09
Registry publication only. No behaviour changed.
The MCP registry will not list a server unless the PyPI package can be shown
to belong to the same person, and the proof it looks for is a line in the
README as published on PyPI. That cannot be added by editing a file: it
has to ship inside a release. Hence a version whose entire content is one
line of text.
mcp-name: io.github.syrian963/django-chainsaw-mcpin the README, which is
what the registry reads.
v0.1.0
django-chainsaw-mcp 0.1.0
An MCP server and CLI that analyses a Django project rather than describing
it: what a delete takes with it, where the N+1 queries are, which pending
migration stops writes or breaks the code still running during a rolling
deploy, which queries read tenant-scoped rows without scoping them.
21 checks, 36 MCP tools with five prompts, 35 CLI subcommands. 420 tests
across 17 suites at 87% coverage, and a documentation page per check that
states what that check cannot see.
Run against eighteen public Django projects, from channels at 55 files to
Saleor at 4332. They found twelve defects in this tool that neither its own
fixtures nor a private codebase had shown. docs/tested-against.md lists the
runs, the defects, the two checks that have never fired on real code with the
coverage that makes those zeroes readable, and the projects that could not be
analysed with a reason for each.
MIT licensed.