Metadata-Version: 2.4
Name: django-architect
Version: 0.2.1
Summary: Static architecture analyzer for large Django codebases.
Project-URL: Homepage, https://github.com/Muhammadumar1671/django-architect
Project-URL: Repository, https://github.com/Muhammadumar1671/django-architect
Project-URL: Issues, https://github.com/Muhammadumar1671/django-architect/issues
Project-URL: Changelog, https://github.com/Muhammadumar1671/django-architect/releases
Author: Django Architect contributors
License: MIT
Keywords: architecture,ast,dependency-graph,django,static-analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.11
Requires-Dist: networkx>=3.2
Requires-Dist: pluggy>=1.4
Requires-Dist: pydantic>=2.6
Requires-Dist: pyyaml>=6.0
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# Django Architect

Static architecture analysis for large Django codebases.

You have just joined a company with a 500,000-line Django project. Run one
command and understand it:

```bash
pip install django-architect
django-architect analyze .
```

```
╭──────────────────────────────────────────────────────────────────────────╮
│  Architecture Score  74/100  (C)                                         │
│                                                                          │
│  cycle freedom       ████████████········   62  ×0.25  3 app-level       │
│  layer conformance   ██████████████······   71  ×0.25  18 errors         │
│  coupling health     ████████████████····   82  ×0.20  p95 fan-out 19    │
│  complexity health   █████████████████···   86  ×0.15  7% of SLOC MI<50  │
│  size health         ██████████████████··   90  ×0.10  4 god classes     │
│  dead code           ███████████████████·   95  ×0.05  6 of 421 modules  │
╰──────────────────────────────────────────────────────────────────────────╯
apps 23      modules 421     symbols 3,908    SLOC 187,442
edges 9,104  max depth 11    references resolved 96.4%    parse errors 0
```

Then explore it:

```bash
django-architect serve
```

## What makes it different

**It reads the wiring Django hides.** Most of a Django project's architecture
is not expressed as imports. `ForeignKey("orders.Order")` is a string.
`path("", views.index)` is a string. `@receiver(post_save, sender=Order)` is a
decorator argument. `task.delay()` never imports the worker. An import-graph
tool shows you almost none of it. Django Architect resolves all of it, which is
exactly the part a newcomer cannot discover by reading the code.

**It tells you how much to trust it.** Every edge carries a confidence level,
and the headline number is paired with a *resolution rate*: the share of
references that resolved to a concrete symbol. A score of 92 at 55% resolution
means much less than 82 at 96%, and the report says so instead of hiding it.

**It never runs your code.** `ast.parse()` only — no `exec`, no `import`, no
Django bootstrap. Settings are read by statically interpreting a safe subset of
Python; anything unresolvable becomes an explicit unknown rather than a guess.

**It is adoptable on a codebase that already has 4,000 problems.** Baselines
record today's reality so CI gates only on what is *new*. Suppressions require
a reason and support an expiry date — an expired suppression fails the build.

## Commands

| | |
|---|---|
| `analyze .` | Full pipeline; prints the summary |
| `serve` | Build and open the interactive report |
| `report --top 20` | Coupling, cycles, largest services/models, hotspots, dead code |
| `violations` | Check architecture rules (exit 1 when they fail) |
| `metrics --node OrderService` | One node: purpose, metrics with percentiles, dependencies, dependents |
| `graph --view model --format mermaid` | Export a diagram |
| `export --format sqlite` | A queryable artifact |
| `init` | Draft an `architecture.yml` from the layers you already have |
| `diff base.json head.json` | Compare two commits |

Exit codes: `0` clean, `1` violations at or above `--fail-on`, `2` the analyzer
itself failed. CI needs to tell the last two apart.

## Architecture rules

`django-architect init` writes a starting config from what your project already
contains. Tighten it from there:

```yaml
version: 1

layers:
  api:          { roles: [view_cbv, view_func, viewset] }
  serializers:  { roles: [serializer, form] }
  services:     { match: ["*/services.py", "*/services/*.py"], roles: [service] }
  repositories: { match: ["*/repositories.py", "*/selectors.py"] }
  models:       { roles: [model, manager, queryset] }

rules:
  - id: layered-architecture
    type: layer-order
    order: [api, serializers, services, repositories, models]
    allow_skip: false          # api -> models is a violation too
    severity: error

  - id: no-orm-in-views
    type: forbid-edge
    from: { layers: [api] }
    to:   { roles: [model, manager, queryset] }
    edge_attr: orm
    severity: error
    message: "views must not query the ORM directly -- go through a service"

  - id: no-app-cycles
    type: acyclic
    scope: app
    severity: error

  - id: service-fan-out
    type: threshold
    selector: { roles: [service] }
    metric: fan_out
    max: 15
    severity: warning

ignore:
  - paths: ["legacy/**"]
    rules: ["layered-architecture"]
    reason: "Tracked in ARCH-142"
    expires: "2026-12-31"       # an expired suppression fails the build
```

Every violation names the exact `file:line` responsible and, where it can, how
to fix it:

```
apps/orders/views.py:7  [no-orm-in-views]
    order_list -> Order: views must not query the ORM directly

apps/billing/services.py:1  [no-app-cycles]
    billing -> orders: app dependency cycle
    fix: break by removing: apps.billing.services -> apps.orders.tasks
```

Cycles always come with a minimal feedback edge set. "Delete these two imports"
is actionable; "these fourteen modules are tangled" is not.

### Adopting on an existing project

```bash
django-architect init
django-architect violations --update-baseline   # records today's reality
git add architecture.yml .django-architect/baseline.json
```

CI now fails only on violations added after that point.

## Continuous integration

```yaml
- uses: Muhammadumar1671/django-architect@v1
  with:
    baseline: .django-architect/baseline.json
    fail-on: error
    comment: true          # sticky PR comment, edited in place
    upload-report: true
```

The action analyzes the base ref, diffs, and comments:

```
## Django Architect
**Architecture regression**

| | Before | After | Change |
|---|---:|---:|---:|
| Architecture score | 82 | 79 | -3.0 |
| Resolution rate | 96.2% | 96.1% | |

### New violations (1)
- **error** `no-orm-in-views` — `billing/views.py:44` views must not query the ORM directly

### New cycles (1)
- `orders -> billing` — break by removing: `orders/services.py:12`
```

## What it detects

Circular dependencies (with the edges to cut) · unused modules · dead code ·
orphan services · god classes · oversized modules · excessive imports · tight
coupling · high fan-in / fan-out · architecture hotspots.

Django's entrypoints are exempt from dead-code detection. `urls.py`,
migrations, management commands, signal receivers, Celery tasks, admin classes,
middleware, and every module the app registry auto-imports (`models.py`,
`admin.py`, `signals.py`, …) have no static in-edges and are emphatically not
dead. Getting that list wrong is the fastest way to make an unused-code report
worthless.

## Metrics

Per symbol: cyclomatic and cognitive complexity, SLOC, nesting depth,
parameters.

Per module: Halstead volume, Maintainability Index, comment ratio, imports.

Per module and app: afferent and efferent coupling, fan-in, fan-out,
instability `I = Ce/(Ca+Ce)`, abstractness `A`, distance from the main sequence
`D = |A + I − 1|`, dependency depth.

Every metric is reported with its **percentile within your codebase**. A
cyclomatic complexity of 14 means nothing until you know it is the 94th
percentile here.

`if TYPE_CHECKING:` imports are excluded from runtime coupling — otherwise
properly type-hinted code scores worse than untyped code.

### Architecture Score

Six weighted components, all displayed, formula published:

| Component | Weight |
|---|---:|
| Cycle freedom | 0.25 |
| Layer conformance | 0.25 |
| Coupling health | 0.20 |
| Complexity health | 0.15 |
| Size health | 0.10 |
| Dead code | 0.05 |

An opaque single number gets either gamed or dismissed.

## Plugins

Django support is itself a plugin. So are DRF, Celery, and the
services/repositories conventions — all built on the public hook API. If a
first-party plugin ever needed access the API could not give it, the API would
be wrong.

```python
from django_architect.plugins import BaseClassifier
from django_architect.ir import Role

class NinjaRouterClassifier(BaseClassifier):
    name = "ninja.routers"
    role = Role.VIEWSET
    base_classes = ("ninja.Router",)
```

```toml
[project.entry-points."django_architect"]
ninja = "my_package.plugin"
```

Hooks: `da_visit_module`, `da_classify_symbol`, `da_classify_module`,
`da_resolve_reference`, `da_contribute_edges`, `da_register_metrics`,
`da_register_rules`, `da_extend_report`.

A plugin that crashes degrades its own contribution and is reported; it never
takes down the run. The loaded plugin set is hashed into the parse cache key,
so installing a plugin cannot serve you results produced without it.

See [docs/plugins.md](docs/plugins.md).

## How it works

```
Discovery → Parse → Resolve → Semantics → Graph → Analysis
```

Six one-directional stages over a single canonical intermediate
representation. Parsing is the expensive stage and the only one that is both
parallel and content-hash cached — a warm incremental run reparses only what
changed.

The hard part is not parsing; it is **resolution**. `self.repo.get_order(...)`
is meaningless until you know what `self.repo` is. A shallow type binder infers
it from annotations, direct instantiation, `__init__` self-assignments, and
class-level assignments — which together recover most service→repository and
viewset→serializer edges. Everything it cannot resolve is counted and reported
rather than guessed.

There is one graph. The app, package, module, model, service, and URL views are
all projections of it, and folded edges keep pointers back to the concrete
references behind them — click an app-to-app arrow, get the 47 `file:line`
imports it stands for.

Output is deterministic: same commit, byte-identical artifact. That is what
makes CI diffing possible at all, and it is enforced by tests.

## Measured on real codebases

Cold runs, no cache, on an 8-core laptop. Zero parse errors across 6,428 files.

| Project | Modules | SLOC | Edges | Resolution | Score | Cold |
|---|---:|---:|---:|---:|---:|---:|
| [django-oscar](https://github.com/django-oscar/django-oscar) | 627 | 51,858 | 25,713 | 83.5% | 89 (B) | 5.0s |
| [wagtail](https://github.com/wagtail/wagtail) | 977 | 181,684 | 75,073 | 83.7% | 68 (D) | 19.4s |
| [saleor](https://github.com/saleor/saleor) | 2,855 | 637,636 | 152,505 | 54.7% | 48 (F) | 66.5s |
| saleor, app code only | 1,142 | 169,891 | — | **81.6%** | — | — |

Peak memory on saleor: 1.3 GB.

**Why saleor's resolution is low, and why it is not a defect.** Its test suite
injects pytest fixtures as unannotated parameters — `staff_api_client` alone
accounts for 4,535 unresolved references. Nothing can type a bare fixture
parameter. Exclude tests (`--exclude-tests`) and saleor's application code
resolves at 81.6%, in line with the others. Surfacing exactly this is what the
resolution rate is for.

The score spread above is driven by dependency cycles: 19% of django-oscar's
apps sit inside a strongly-connected component, against 83% for wagtail and
97% for saleor.

### Metric accuracy

The Maintainability Index matches [radon](https://github.com/rubik/radon), the
reference implementation for these metrics in Python — mean absolute difference
**0.70 points**, median 0.25, over 200 files sampled from the three projects
above. A regression test asserts it, because MI consumes `ln(halstead_volume)`
and any drift in the counting scheme moves the result by tens of points and
makes it incomparable with the tooling people already run.

Cyclomatic complexity deliberately diverges from radon in one place. Radon's
module total keeps each *method's* base complexity while discarding each
*function's*, so a test module of 99 branch-free methods scores 97. Counting
decision points instead reports 1, which is the answer to the question people
are actually asking. This is the source of the MI tail above.

Every score component measures a **proportion** — share of apps in a cycle,
share of SLOC at MI rank C, share of modules oversized — never a raw count. A
count-based score is a proxy for project size: it charges a 3,000-module
codebase more than a 50-module one for identical quality. A test asserts the
score is invariant to project size at fixed quality.

When no layer rules are configured, layer conformance is **excluded and the
remaining weights renormalised**, rather than scoring 100 for something never
checked. The report says so.

**Known limitation: warm runs are only about 2× faster, not 10×.** The design
assumed parsing dominates wall clock and is the only stage worth caching. At
scale that is wrong — on wagtail a warm run spends 4.8s rehydrating cached IR,
3.0s building the graph, and 1.0s on metrics. Resolution and graph construction
re-run every time and are not cached. Saleor warm is ~55s against ~66s cold.
Fixing it means caching resolved edges, not just parse output.

## Documentation

- [Architecture design](docs/specs/2026-07-25-django-architect-design.md)
- [Writing plugins](docs/plugins.md)
- [Configuration reference](docs/configuration.md)

## Status

Alpha. The engine, rules, reports, web UI, and CI integration all work. The AI
layer described in the design document is not implemented.

Verified on django-oscar, wagtail, and saleor (see above): no parse errors,
81–84% resolution on application code, and 637k SLOC analyzed in 66s within
1.3 GB. Warm-run performance falls short of its target — see the limitation
noted above.

## License

MIT
