|
1 | | -# PR-Reviewer |
| 1 | +# Loadpath |
| 2 | + |
| 3 | +Review as **load-path inspection** on a Django + React architecture graph. Not another hunk-comment bot. |
| 4 | + |
| 5 | +A change is a force. Loadpath traces where that force travels until it hits a sink — HTTP response, UI, Celery/Dramatiq job, migration, permission — then scores whether you have enough evidence to merge. |
| 6 | + |
| 7 | +``` |
| 8 | +Loadpath: MEDIUM — Invoice.total field change |
| 9 | +Sinks: GET/POST /api/invoices/{id}; Celery send_invoice_email, apply_credit; Dramatiq rebuild_ledger; React InvoicePage + InvoiceForm |
| 10 | +Tests: pytest hits serializer and view; no RTL test on InvoiceForm |
| 11 | +Architecture: stays inside billing |
| 12 | +Residual: total also formatted in a Signal update_ledger — no test |
| 13 | +Suggested reviewers: billing-team |
| 14 | +``` |
| 15 | + |
| 16 | +On the demo monorepo that path is: |
| 17 | + |
| 18 | +`Invoice.total → InvoiceSerializer → InvoiceViewSet → /api/invoices/{id} → OpenAPI → fetch → useInvoice → InvoicePage → InvoiceForm / Zod` |
| 19 | + |
| 20 | +plus the jobs the view enqueues (`send_invoice_email.delay`, `rebuild_ledger.send`). |
| 21 | + |
| 22 | +## App |
| 23 | + |
| 24 | +`loadpath serve --port 7345` opens a local desktop-style UI. Tokens stay on the machine in `~/.loadpath/settings.json`. AI is used **only** for residual uncertainty the graph cannot close. |
| 25 | + |
| 26 | +### Review |
| 27 | + |
| 28 | +Confidence brief, read-order, clusters, architecture findings on the impact path, residual list, and the subgraph for the git range. Review walks the **indexed** graph (incremental refresh by default). |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | +### Architecture |
| 33 | + |
| 34 | +Index a repo first. The architecture tab is the full typed graph plus `loadpath.yml` contexts and rules — not a PR diff. Findings here are repo-wide; review then scopes them to the change. |
| 35 | + |
| 36 | + |
| 37 | + |
| 38 | +### Impact graph |
| 39 | + |
| 40 | +Toggle **This review** (impact subgraph) vs **Indexed architecture** (the repo map). Dashed edges are inferred (URL/Zod overlap); solid edges are extracted or generated-client stitches. |
| 41 | + |
| 42 | + |
| 43 | + |
| 44 | +### Pull requests |
| 45 | + |
| 46 | +GitHub and Bitbucket via API tokens from Settings. Pick a PR and jump to a branch-range review. |
| 47 | + |
| 48 | + |
| 49 | + |
| 50 | +### Settings |
| 51 | + |
| 52 | +GitHub / Bitbucket tokens; AI providers (Anthropic, OpenAI, Grok/xAI, DeepSeek, Cursor-compatible, Ollama). Residual analysis only — Loadpath does not comment every hunk. |
| 53 | + |
| 54 | + |
| 55 | + |
| 56 | +## Install |
| 57 | + |
| 58 | +Python 3.12+ and Node 22+ (UI). |
| 59 | + |
| 60 | +```bash |
| 61 | +pip install -e ".[dev]" |
| 62 | +python -m playwright install chromium # optional, for UI screenshot tests |
| 63 | +cd ui && npm install && npm run build && cd .. |
| 64 | +loadpath --help |
| 65 | +``` |
| 66 | + |
| 67 | +## CLI |
| 68 | + |
| 69 | +```bash |
| 70 | +# Index a monorepo (SQLite graph at .loadpath/graph.sqlite3, incremental on file hashes) |
| 71 | +loadpath index /path/to/repo |
| 72 | + |
| 73 | +# Inspect bounded contexts, rules, and type counts from that index |
| 74 | +loadpath architecture /path/to/repo |
| 75 | + |
| 76 | +# Review a git range against the index (incremental refresh; --no-reindex to reuse as-is) |
| 77 | +loadpath review /path/to/repo --base origin/main --head HEAD |
| 78 | +loadpath review /path/to/repo --base origin/main --no-reindex |
| 79 | + |
| 80 | +# Cross-platform app (API + visual graph + PR list) |
| 81 | +loadpath serve --port 7345 |
| 82 | +``` |
| 83 | + |
| 84 | +**Flow:** `index` builds the architecture graph → `architecture` shows contexts and rule hits on the whole repo → `review` walks that same graph for a git range. The app mirrors this: Index registers a workspace, Architecture inspects it, Review traces a change through it. |
| 85 | + |
| 86 | +Put `loadpath.yml` at the repo root (see [`loadpath.yml.example`](loadpath.yml.example) and [`fixtures/demo_monorepo/loadpath.yml`](fixtures/demo_monorepo/loadpath.yml)). The tool is opinionated about *your* architecture, not a generic module graph. |
| 87 | + |
| 88 | +## Django support |
| 89 | + |
| 90 | +AST is the default extractor. It is a **framework overlay**, not an import graph. |
| 91 | + |
| 92 | +| Surface | What Loadpath extracts | |
| 93 | +| --- | --- | |
| 94 | +| Models | Fields, FK / M2M / O2O, `on_delete`, string refs (`ForeignKey("accounts.User")`) as residuals | |
| 95 | +| Serializers | `Meta.fields` / `exclude`, declared fields, `serializes` edges, queryset-in-serializer flag | |
| 96 | +| Views | DRF ViewSets / APIViews, `serializer_class`, `get_serializer_class` (residual), `permission_classes`, `get_queryset`, `filterset_class`, `authentication_classes`, `pagination_class` | |
| 97 | +| Function views | `@api_view`, `@login_required`, `@csrf_exempt`, … | |
| 98 | +| Django Ninja | `@router.get/post/…` routes and views | |
| 99 | +| URLs | `path` / `re_path`, DRF `router.register`, `include()` mount composition (`/api` + `invoices/<id>/` → `/api/invoices/{id}`) | |
| 100 | +| Signals | `@receiver`, `signal.connect()` residual, `AppConfig.ready()` residual | |
| 101 | +| Management commands | `BaseCommand` + `handle()`, including `.delay(` / `.send(` enqueue edges | |
| 102 | +| Migrations | `CreateModel` / `AddField` / `RemoveField` / `DeleteModel` / `RunPython` as `destructive_migration` | |
| 103 | +| Tests | `test_*` in `tests.py` / `tests/` as `tested_by` | |
| 104 | + |
| 105 | +### Celery |
| 106 | + |
| 107 | +- `@shared_task`, `@app.task`, `@periodic_task` |
| 108 | +- `celery.Task` subclasses (`run(self, invoice_id)`) |
| 109 | +- Enqueue: `.delay(`, `.apply_async(` |
| 110 | +- Signatures / canvas: `.s(`, `.si(`, `chain` / `group` / `chord` (canvas is a residual; inner signatures are `enqueues` edges) |
| 111 | +- `current_app.send_task("billing.tasks.send_invoice_email")` — residual + inferred enqueue |
| 112 | +- `transaction.on_commit(lambda: task.apply_async(...))` — residual, nested enqueue walked |
| 113 | +- `CELERY_BEAT_SCHEDULE` / `beat_schedule` in settings |
| 114 | + |
| 115 | +### Dramatiq |
| 116 | + |
| 117 | +- `@dramatiq.actor` |
| 118 | +- `dramatiq.GenericActor` subclasses (`perform(self, invoice_id)`) |
| 119 | +- Enqueue: `.send(`, `.send_with_options(` (heuristic: dramatiq import or `actors` / `tasks` module) |
| 120 | + |
| 121 | +Call-site placeholders (`rebuild_ledger.send` in a view) do **not** overwrite the actor definition’s file. The graph keeps `actors.py` / `tasks.py` as the node home. |
| 122 | + |
| 123 | +### Idempotency rule |
| 124 | + |
| 125 | +`celery_tasks_must_be_idempotent_on_model_pk` (alias `async_tasks_must_be_idempotent_on_model_pk`) warns when a Celery **or** Dramatiq task takes a full object payload instead of `pk` / `*_id`. Message names the broker. |
| 126 | + |
| 127 | +### Optional `django.setup()` overlay |
| 128 | + |
| 129 | +AST is enough for review. If you need live `_meta` (db_table, resolved relations), set `boot_django: true` in `loadpath.yml`. Loadpath then imports Django, calls `django.setup()`, and merges model/field nodes. Failures become residuals; the AST graph still stands. Leave it `false` in CI unless the fixture is a bootable project. |
| 130 | + |
| 131 | +## React + stitch |
| 132 | + |
| 133 | +**React:** react-router tables, composition, TanStack Query `queryKey` + fetch/axios URL templates, Zod schemas, feature-folder imports, RTL `render(<Page/>)` as `tested_by`. |
| 134 | + |
| 135 | +**Stitch (the moat):** OpenAPI from Spectacular/schema files first; generated clients (`generated/`, orval, openapi-typescript) as high-confidence `consumed_by_client`; fallback URL-template matching and serializer/Zod field overlap marked **inferred**. |
| 136 | + |
| 137 | +## Architecture rules (`loadpath.yml`) |
| 138 | + |
| 139 | +| Rule | Meaning | |
| 140 | +| --- | --- | |
| 141 | +| `views_cannot_import_other_context_models` | A billing view must not import `accounts.UserProfile` | |
| 142 | +| `react_feature_may_only_call_own_or_shared_api` | Billing UI may not `fetch('/api/me')` | |
| 143 | +| `serializers_are_the_only_published_contract` | Zod/form fields must not drift from the serializer | |
| 144 | +| `no_queryset_in_serializer` | Serializers must not run querysets | |
| 145 | +| `celery_tasks_must_be_idempotent_on_model_pk` | Celery and Dramatiq tasks take a model pk | |
| 146 | + |
| 147 | +Waivers live under `waivers:` in the same file. Reviewers are the `owners` of the bounded contexts in the impact subgraph. |
| 148 | + |
| 149 | +Impact walk skips permission/app/context hubs, does not climb `renders` into the App shell, and does not follow cross-context `relates_to` (an Invoice FK to UserProfile does not pull identity into a billing review). |
| 150 | + |
| 151 | +## How confidence is scored |
| 152 | + |
| 153 | +Line coverage on changed files is the wrong metric. Loadpath scores the **impact subgraph**: |
| 154 | + |
| 155 | +| Signal | High | Low | |
| 156 | +| --- | --- | --- | |
| 157 | +| Tests | Sinks in the radius are hit by tests that still reach the changed symbol | Serializer changed, tests only on the view happy path | |
| 158 | +| Contract | OpenAPI/client types track the serializer | React path/Zod field still old | |
| 159 | +| Architecture | No new cross-context edges | `crosses_context` with no waiver | |
| 160 | +| Graph | Resolved edges | Many inferred/dynamic edges | |
| 161 | + |
| 162 | +`high` / `medium` / `low` plus three reasons. Isolated leaf UI with green tests and no rule hits is labeled `loadpath:low-risk`. |
| 163 | + |
| 164 | +## Tests |
| 165 | + |
| 166 | +```bash |
| 167 | +pytest |
| 168 | +cd ui && npm test |
| 169 | +``` |
| 170 | + |
| 171 | +| Suite | What it covers | |
| 172 | +| --- | --- | |
| 173 | +| `tests/unit/` | Django/React extractors, architecture rules, stitch, SCM/AI providers | |
| 174 | +| `tests/integration/test_review_vertical_slice.py` | Serializer field change reaches InvoicePage/Zod, not MePage; reviewers `billing-team` | |
| 175 | +| `tests/e2e/test_cli_review.py` | `loadpath index` / `architecture` / `review` markdown, JSON, HTML | |
| 176 | +| `tests/e2e/test_api_flow.py` | health, index, architecture, review-from-index, graph, settings, GitHub + Bitbucket PR list | |
| 177 | +| `tests/e2e/test_index_architecture_flow.py` | index snapshot, review without index, review walking an existing graph | |
| 178 | +| `tests/e2e/test_brokers_and_django.py` | Celery + Dramatiq sinks, actor-only PR, non-idempotent Dramatiq warning, destructive migration, cross-context blocker, boot overlay, management commands, beat/canvas | |
| 179 | +| `tests/e2e/test_ui_screenshots.py` | Playwright: Architecture, Review, Impact graph, Pull requests, Settings → `docs/screenshots/` | |
| 180 | + |
| 181 | +CI installs Chromium and runs the full suite. |
| 182 | + |
| 183 | +## Demo fixture |
| 184 | + |
| 185 | +[`fixtures/demo_monorepo`](fixtures/demo_monorepo) is a billing/identity split: DRF ViewSet, FBV, Ninja ledger route, Celery tasks + beat + canvas, Dramatiq actor + GenericActor, management command, signal, FK string ref, React InvoicePage/Zod. |
| 186 | + |
| 187 | +## What this is not |
| 188 | + |
| 189 | +Not CodeRabbit (comments without a closed impact set). Not CodeScene (historical coupling). Not django-orm-lens (models only). Not a generic SCIP call graph. The product is review as load-path inspection. |
0 commit comments