Skip to content

Releases: syrian963/django-chainsaw-mcp

v0.1.5

Choose a tag to compare

@github-actions github-actions released this 10 Sep 08:06
v0.1.5
dec9d15

[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 to docs/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 on main after 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_prefetches shipped
    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

Choose a tag to compare

@syrian963 syrian963 released this 09 Sep 15:13
v0.1.4
892bb4c

[0.1.4] - 2026-09-09

Added

  • defeated_prefetches: a relation that was prefetched and then re-queried
    anyway. prefetch_related fills 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 with CaptureQueriesContext on 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 answered count and exists from 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.

    nplusone finds the neighbouring problem - eagerly loaded and never touched

    • at runtime, and unused_eager_loading here answers that one statically
      for DRF viewsets. Neither answers this one, where the prefetch is used and
      the accessor cannot read it.

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.
  • 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 leftover from pathlib import Path. CI runs ruff check src/ tests/ as
    its first step and run_tests.sh never 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, and uvx builds 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 bare uvx invocation in a code
    fence.

v0.1.3

Choose a tag to compare

@syrian963 syrian963 released this 09 Sep 15:13
v0.1.3
1b7f94e

[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 uses mcp-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 and server.json disagree.

v0.1.2

Choose a tag to compare

@syrian963 syrian963 released this 09 Sep 15:13
v0.1.2
377124f

[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 --write only
    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.

v0.1.1

Choose a tag to compare

@syrian963 syrian963 released this 09 Sep 15:13
v0.1.1
fe467f6

[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-mcp in the README, which is
    what the registry reads.

v0.1.0

Choose a tag to compare

@syrian963 syrian963 released this 09 Sep 09:48

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.