Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Added `clawpatch review --export-tribunal-ledger` to emit review findings as JSONL for downstream ledger ingestion, thanks @dpdanpittman.
- Added `clawpatch review --prompt-file` to append extra reviewer guidance from a file or stdin, thanks @dpdanpittman.
- Added deterministic Express, Fastify, and Hono route mapping for Node projects, thanks @rohitjavvadi.
- Added conservative Django `urls.py` route mapping for `path`, `re_path`, and legacy `url` declarations, thanks @rohitjavvadi.
- Fixed provider commands with relative `--root` paths by canonicalizing explicit roots before invoking Codex or other providers.
- Added first-pass Elixir Mix/Phoenix mapping for project metadata, contexts, Phoenix web slices, runtime config, Ecto migrations, project scripts, ExUnit tests, and Mix validation defaults, thanks @tears-mysthrala.
- Improved `clawpatch fix` handoff context and patch-attempt changed-file auditing for dirty-worktree fixes.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ validation commands and records a patch attempt under `.clawpatch/`.
- C/C++ standalone `main()` files, CMake `add_executable` / `add_library`
targets, and autotools `bin_PROGRAMS` / `lib_LTLIBRARIES` targets
- Python project metadata, console scripts, bounded source groups, pytest suites,
and Flask/FastAPI routes
and Flask/FastAPI/Django routes
- SwiftPM `Sources/*` targets and `Tests/*` suites
- Laravel/PHP projects from `composer.json` and `artisan`, including routes,
controllers, form requests, Artisan commands, jobs, services, models,
Expand Down
16 changes: 9 additions & 7 deletions docs/feature-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Supported deterministic mappers today:
- Go `cmd/*/main.go`
- Go `internal/*` packages
- Python project metadata, console scripts, root app files, bounded source groups,
pytest suites, and Flask/FastAPI routes
pytest suites, and Flask/FastAPI/Django routes
- Java and Kotlin JVM semantic role groups, plus Kotlin Android semantic role
groups including Hilt, Dagger, Koin, and Metro
- Ruby project metadata, executables, source groups, RSpec/Minitest suites,
Expand Down Expand Up @@ -148,11 +148,14 @@ Python mapping covers `pyproject.toml`, `setup.cfg`, `setup.py`, and
`requirements.txt` metadata; `[project.scripts]`, `[tool.poetry.scripts]`,
`setup.cfg` `console_scripts`, and `setup.py` console script entry points; root
app files; source groups under common Python source roots including `web/`;
pytest files; Flask `@*.route(...)` handlers; and FastAPI `@*.get(...)` /
`@*.api_route(...)` handlers. Flask and FastAPI route methods are read from list,
tuple, or set literals. FastAPI paths can be positional strings or literal
`path=` keywords. Default Python command detection covers pytest, ruff, mypy,
pyright, and black.
pytest files; Flask `@*.route(...)` handlers; FastAPI `@*.get(...)` /
`@*.api_route(...)` handlers; and conservative Django `urls.py` `path(...)`,
`re_path(...)`, and legacy `url(...)` declarations. Flask and FastAPI route
methods are read from list, tuple, or set literals. FastAPI paths can be
positional strings or literal `path=` keywords. Django route paths are normalized
from literal route strings and simple named regex groups; includes are mapped as
their own URL groups without recursively expanding imported URL configs. Default
Python command detection covers pytest, ruff, mypy, pyright, and black.

Ruby mapping covers project metadata, executables, source groups, RSpec and
Minitest suites, and Rails app structure. Rails legacy `config/secrets.yml`,
Expand All @@ -164,7 +167,6 @@ Known gaps:
- Express/Fastify/Hono route mapping is conservative and does not infer
prefixes from cross-file router mounts such as `app.use("/api", router)`,
`fastify.register(..., { prefix })`, or `app.route("/api", subApp)`
- no Django route mapper yet
- Laravel route parsing is convention-based, does not execute Laravel route discovery,
and may omit prefixes applied by `Route::group(...)` wrappers
- C#/.NET mapping does not evaluate MSBuild conditions, imported props/targets,
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ stderr so pipes stay parseable.

## What clawpatch does

- **Semantic feature mapping.** Detects npm bins, Next.js routes, React Router routes, Python packages and Flask/FastAPI routes, Ruby/Rails slices, Laravel/PHP slices, Java/Kotlin Gradle modules, C#/.NET projects and ASP.NET endpoints, Go packages, Rust crates, C/C++ build targets, SwiftPM targets, and common config files as reviewable units.
- **Semantic feature mapping.** Detects npm bins, Next.js routes, React Router routes, Python packages and Flask/FastAPI/Django routes, Ruby/Rails slices, Laravel/PHP slices, Java/Kotlin Gradle modules, C#/.NET projects and ASP.NET endpoints, Go packages, Rust crates, C/C++ build targets, SwiftPM targets, and common config files as reviewable units.
- **Automated code review.** Reviews features with AI providers (Codex CLI today), persists findings with severity, category, and line locations.
- **Explicit fix workflow.** `clawpatch fix` runs validated patches for one finding at a time, never commits or pushes automatically.
- **Stable state model.** All features, findings, patches live in `.clawpatch/` as JSON, resumable across runs.
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ This discovers reviewable features:
- Next.js routes
- Go packages and commands
- Java/Kotlin Gradle modules
- Python packages, console scripts, Flask/FastAPI/Django routes, and pytest suites
- C#/.NET projects, ASP.NET endpoints, source groups, and test projects
- Python packages, console scripts, Flask/FastAPI routes, and pytest suites
- JVM semantic role groups
- Ruby packages, Rails apps, executables, and tests
- Rust crates and binaries
Expand Down
179 changes: 179 additions & 0 deletions src/mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11600,6 +11600,185 @@ add_executable(headerapp include/headers.hpp)
expect(result.features.some((feature) => feature.source === "python-flask-route")).toBe(false);
});

it("maps Django urls.py routes conservatively", async () => {
const root = await fixtureRoot("clawpatch-python-django-routes-");
await writeFixture(root, "requirements.txt", "django\npytest\n");
await writeFixture(root, "mysite/__init__.py", "");
await writeFixture(
root,
"mysite/urls.py",
[
"from django.conf.urls import url",
"from django.urls import include, path, re_path",
"from django.contrib import admin",
"from . import views",
"from .views import SignupView",
"",
'"""',
"urlpatterns = [",
" path('docs-only/', views.docs_only),",
"]",
'"""',
"",
'r"""',
"urlpatterns = [",
" path('raw-docs-only/', views.raw_docs_only),",
"]",
'"""',
"",
"def build_local_patterns():",
" '''",
" urlpatterns = [",
" path('indented-docs-only/', views.indented_docs_only),",
" ]",
" '''",
" urlpatterns = [",
" path('local-only/', views.local_only),",
" ]",
" return urlpatterns",
"",
"def helper_patterns():",
" return [",
" path('helper/', views.helper),",
" ]",
"",
"unused_patterns = [",
" path('unused/', views.unused),",
"]",
"",
"urlpatterns = [path('inline/', views.inline), re_path(r'^inline-regex/$', views.inline_regex),",
" path('', views.index, name='index'),",
" path('users/<int:pk>/', views.user_detail, name='user-detail'),",
" path('accounts/password/reset/', views.password_reset, name='password-reset'),",
" path('orders/', views.orders, name='orders'),",
" path(",
" 'reports/',",
" views.reports,",
" name='reports',",
" ),",
" path('signup/', SignupView.as_view(), name='signup'),",
" path('admin/', admin.site.urls),",
" path('api/', include('api.urls')),",
" path('tuple-api/', include(('tuple.urls', 'tuple'), namespace='tuple')),",
" re_path(r'^legacy/(?P<slug>[-\\w]+)/$', views.legacy, name='legacy'),",
" url(r'^old/(?P<pk>\\d+)/$', views.old_detail),",
" path(DYNAMIC_ROUTE, views.dynamic),",
" path(f'tenant/{slug}/', views.dynamic),",
" re_path(r'^(foo|bar)/$', views.complex_regex),",
" custom_path('custom/', views.custom),",
" # path('commented/', views.commented),",
" \"path('string/', views.string)\",",
"]",
"urlpatterns += [path('extra/', views.extra)]",
"",
].join("\n"),
);
await writeFixture(root, "fallback/__init__.py", "");
await writeFixture(
root,
"fallback/urls.py",
[
"from . import views",
"",
"urlpatterns = [",
" path('dependency-only/', views.dependency_only),",
"]",
"",
].join("\n"),
);
await writeFixture(root, "mysite/views.py", "class SignupView:\n pass\n");
await writeFixture(root, "fallback/views.py", "def dependency_only():\n pass\n");
await writeFixture(root, "mysite/test_urls.py", "def test_urls():\n pass\n");

const project = await detectProject(root);
const result = await mapFeatures(root, project, []);
const routes = result.features.filter((feature) => feature.source === "python-django-route");
const titles = routes.map((feature) => feature.title);
const byTitle = (title: string) => routes.find((feature) => feature.title === title);

expect(project.detected.frameworks).toContain("django");
expect(titles).toEqual(
expect.arrayContaining([
"Django route /",
"Django route /users/:pk/",
"Django route /accounts/password/reset/",
"Django route /orders/",
"Django route /reports/",
"Django route /signup/",
"Django route /admin/",
"Django route /api/",
"Django route /tuple-api/",
"Django route /dependency-only/",
"Django route /legacy/:slug/",
"Django route /old/:pk/",
"Django route /inline/",
"Django route /inline-regex/",
"Django route /extra/",
]),
);
expect(byTitle("Django route /")?.entrypoints[0]).toMatchObject({
path: "mysite/urls.py",
symbol: "views.index",
route: "/",
});
expect(byTitle("Django route /")?.tests).toEqual([
{ path: "mysite/test_urls.py", command: "pytest" },
]);
expect(byTitle("Django route /api/")?.entrypoints[0]?.symbol).toBe("api.urls");
expect(byTitle("Django route /tuple-api/")?.entrypoints[0]?.symbol).toBeNull();
expect(byTitle("Django route /signup/")?.entrypoints[0]?.symbol).toBe("SignupView.as_view");
expect(byTitle("Django route /admin/")?.entrypoints[0]?.symbol).toBe("admin.site.urls");
expect(byTitle("Django route /dependency-only/")?.entrypoints[0]).toMatchObject({
path: "fallback/urls.py",
symbol: "views.dependency_only",
route: "/dependency-only/",
});
expect(byTitle("Django route /accounts/password/reset/")?.trustBoundaries).toContain("auth");
expect(byTitle("Django route /signup/")?.trustBoundaries).toContain("auth");
expect(byTitle("Django route /users/:pk/")?.trustBoundaries).not.toContain("auth");
expect(byTitle("Django route /orders/")?.trustBoundaries).not.toContain("auth");
expect(titles).not.toContain("Django route /tenant/");
expect(titles).not.toContain("Django route /custom/");
expect(titles).not.toContain("Django route /commented/");
expect(titles).not.toContain("Django route /string/");
expect(titles).not.toContain("Django route /(foo|bar)/");
expect(titles).not.toContain("Django route /docs-only/");
expect(titles).not.toContain("Django route /raw-docs-only/");
expect(titles).not.toContain("Django route /indented-docs-only/");
expect(titles).not.toContain("Django route /local-only/");
expect(titles).not.toContain("Django route /helper/");
expect(titles).not.toContain("Django route /unused/");
});

it("does not map Django-shaped URLs without a Django signal", async () => {
const root = await fixtureRoot("clawpatch-python-django-url-false-positive-");
await writeFixture(root, "requirements.txt", "pytest\n");
await writeFixture(root, "web/__init__.py", "");
await writeFixture(
root,
"web/urls.py",
[
'r"""from django.urls import path"""',
"",
"def path(route, handler):",
" return (route, handler)",
"",
"urlpatterns = [",
" path('not-django/', handler),",
"]",
"def handler():",
" pass",
"",
].join("\n"),
);

const project = await detectProject(root);
const result = await mapFeatures(root, project, []);

expect(project.detected.frameworks).not.toContain("django");
expect(result.features.some((feature) => feature.source === "python-django-route")).toBe(false);
});

it("maps FastAPI routes in root and web source files", async () => {
const root = await fixtureRoot("clawpatch-python-fastapi-routes-");
await writeFixture(root, "requirements.txt", "fastapi\npytest\n");
Expand Down
Loading