@mrciphersmith/keryx 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/cli.js +872 -316
  2. package/docs/README.md +2 -0
  3. package/package.json +1 -1
  4. package/src/gdskills/bundled/install-manifest.json +319 -76
  5. package/src/gdskills/bundled/stacks/django/agent-refs.json +3 -0
  6. package/src/gdskills/bundled/stacks/django/governance/eval.json +1763 -0
  7. package/src/gdskills/bundled/stacks/django/governance/scout.json +40 -0
  8. package/src/gdskills/bundled/stacks/django/pack.json +43 -0
  9. package/src/gdskills/bundled/stacks/django/rules/coding-style.mdc +80 -0
  10. package/src/gdskills/bundled/stacks/django/rules/patterns.mdc +92 -0
  11. package/src/gdskills/bundled/stacks/django/rules/security.mdc +92 -0
  12. package/src/gdskills/bundled/stacks/django/rules/testing.mdc +89 -0
  13. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/SKILL.md +149 -0
  14. package/src/gdskills/bundled/stacks/django/skills/django-build-fix/evals.json +49 -0
  15. package/src/gdskills/bundled/stacks/django/skills/django-code-review/SKILL.md +137 -0
  16. package/src/gdskills/bundled/stacks/django/skills/django-code-review/evals.json +48 -0
  17. package/src/gdskills/bundled/stacks/django/skills/django-implementation/SKILL.md +147 -0
  18. package/src/gdskills/bundled/stacks/django/skills/django-implementation/evals.json +75 -0
  19. package/src/gdskills/bundled/stacks/django/skills/django-migrate/SKILL.md +166 -0
  20. package/src/gdskills/bundled/stacks/django/skills/django-migrate/evals.json +49 -0
  21. package/src/gdskills/bundled/stacks/django/skills/django-testing/SKILL.md +130 -0
  22. package/src/gdskills/bundled/stacks/django/skills/django-testing/evals.json +48 -0
  23. package/src/gdskills/bundled/stacks/fastapi/agent-refs.json +3 -0
  24. package/src/gdskills/bundled/stacks/fastapi/governance/eval.json +1777 -0
  25. package/src/gdskills/bundled/stacks/fastapi/governance/scout.json +34 -0
  26. package/src/gdskills/bundled/stacks/fastapi/pack.json +43 -0
  27. package/src/gdskills/bundled/stacks/fastapi/rules/coding-style.mdc +68 -0
  28. package/src/gdskills/bundled/stacks/fastapi/rules/patterns.mdc +108 -0
  29. package/src/gdskills/bundled/stacks/fastapi/rules/security.mdc +99 -0
  30. package/src/gdskills/bundled/stacks/fastapi/rules/testing.mdc +85 -0
  31. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/SKILL.md +157 -0
  32. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/evals.json +76 -0
  33. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/SKILL.md +150 -0
  34. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/evals.json +74 -0
  35. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/SKILL.md +158 -0
  36. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/evals.json +75 -0
  37. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/SKILL.md +146 -0
  38. package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/evals.json +74 -0
  39. package/src/gdskills/bundled/stacks/java-kotlin-spring/agent-refs.json +3 -0
  40. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/eval.json +2194 -0
  41. package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/scout.json +39 -0
  42. package/src/gdskills/bundled/stacks/java-kotlin-spring/pack.json +40 -0
  43. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/coding-style.mdc +67 -0
  44. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/patterns.mdc +65 -0
  45. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/security.mdc +69 -0
  46. package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/testing.mdc +80 -0
  47. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/SKILL.md +144 -0
  48. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/evals.json +74 -0
  49. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/SKILL.md +129 -0
  50. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/evals.json +74 -0
  51. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/SKILL.md +147 -0
  52. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/evals.json +75 -0
  53. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/SKILL.md +139 -0
  54. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/evals.json +74 -0
  55. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/SKILL.md +128 -0
  56. package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/evals.json +73 -0
  57. package/src/gdskills/bundled/stacks/python/agent-refs.json +2 -1
  58. package/src/gdskills/bundled/stacks/python/pack.json +1 -1
  59. package/src/gdskills/bundled/stacks/rust/agent-refs.json +3 -0
  60. package/src/gdskills/bundled/stacks/rust/governance/eval.json +1823 -0
  61. package/src/gdskills/bundled/stacks/rust/governance/scout.json +32 -0
  62. package/src/gdskills/bundled/stacks/rust/pack.json +42 -0
  63. package/src/gdskills/bundled/stacks/rust/rules/coding-style.mdc +93 -0
  64. package/src/gdskills/bundled/stacks/rust/rules/patterns.mdc +85 -0
  65. package/src/gdskills/bundled/stacks/rust/rules/security.mdc +85 -0
  66. package/src/gdskills/bundled/stacks/rust/rules/testing.mdc +82 -0
  67. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/SKILL.md +141 -0
  68. package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/evals.json +78 -0
  69. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/SKILL.md +127 -0
  70. package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/evals.json +72 -0
  71. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/SKILL.md +133 -0
  72. package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/evals.json +79 -0
  73. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/SKILL.md +130 -0
  74. package/src/gdskills/bundled/stacks/rust/skills/rust-testing/evals.json +75 -0
  75. package/src/gdskills/bundled/agents/python-build-fixer.md +0 -52
  76. package/src/gdskills/bundled/agents/python-code-auditor.md +0 -49
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: django-code-review
3
+ description: "Use when reviewing Django changes for correctness and injection-class risks -- checks N+1 queries missing select_related/prefetch_related, mark_safe/|safe applied to user-traceable data, raw()/extra() built by string interpolation, @csrf_exempt on a session-authenticated view, missing/hand-edited migrations, and settings.py misconfiguration (DEBUG, SECRET_KEY, ALLOWED_HOSTS). Read-only: reports findings, does not edit code."
4
+ triggers:
5
+ - "review this django diff"
6
+ - "check this django change before merging"
7
+ - "review django code changes"
8
+ - "audit this django view for csrf or xss gaps"
9
+ - "check this django model change for N+1"
10
+ - "review this django migration"
11
+ metadata:
12
+ origin: authored
13
+ category: review
14
+ version: "1.0.0"
15
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
16
+ license: "MIT"
17
+ ---
18
+
19
+ # Django code review
20
+
21
+ Review a set of Django changes for correctness, query-performance, and
22
+ security risk specific to the framework. Read-only: this skill reports
23
+ findings, it never edits code. Scoped to Django-specific defects; for
24
+ generic Python defects (mutable defaults, bare `except`, resource leaks) use
25
+ `python-code-review`, for a language-agnostic security sweep use
26
+ `review-security-code`, for fixing what this skill finds use
27
+ `django-build-fix` (checker failures) or hand the report to the author.
28
+
29
+ ## Workflow
30
+
31
+ ### Step 1: Scope the review
32
+
33
+ 1. Identify the changed Django files (`git diff` against the review base,
34
+ or the files the requester names) — models, views, forms/serializers,
35
+ templates, migrations, settings.
36
+ 2. Read `settings.py` for `DEBUG`, `ALLOWED_HOSTS`, and whether DRF/
37
+ `pytest-django` are installed, so findings can be judged against the
38
+ project's actual configuration.
39
+ 3. Read enough of the surrounding view/template/serializer to judge whether
40
+ a flagged pattern is a real bug in context (e.g. does the template
41
+ actually dereference the relation this queryset didn't eager-load).
42
+
43
+ ### Step 2: Check each changed file against these categories
44
+
45
+ **Query performance (N+1)**
46
+ - A queryset iterated in a view, template, or serializer that dereferences
47
+ a `ForeignKey`/`OneToOneField` per row with no `select_related`, or a
48
+ many-valued relation per row with no `prefetch_related`.
49
+ - A loop calling `.save()`/`.delete()` per row where `bulk_create`/
50
+ `bulk_update`/`.update()`/`.delete()` on the queryset would collapse it to
51
+ one (or a few) queries.
52
+
53
+ **Cross-site scripting**
54
+ - `mark_safe()`/the `|safe` template filter/`format_html()`'s raw-string
55
+ form applied to a value that traces back to user input (a form field, a
56
+ query parameter, stored user-submitted content) — Django's own
57
+ auto-escaping is the correct default; only application-controlled markup
58
+ should ever bypass it.
59
+
60
+ **SQL injection**
61
+ - `.raw()`/`.extra()` built with an f-string/`%`/`.format()` instead of the
62
+ `params` argument.
63
+
64
+ **CSRF**
65
+ - `@csrf_exempt` added to a view that still relies on session/cookie
66
+ authentication — an exemption is only correct alongside an equivalent
67
+ protection (signed webhook signature, non-cookie token auth).
68
+
69
+ **Migrations**
70
+ - A model field/constraint change with no corresponding migration file in
71
+ the diff, or a migration file whose `operations` look hand-edited rather
72
+ than `makemigrations`-generated (no matching model diff for what the
73
+ operations do).
74
+ - A `RunPython` data migration with no reverse function (or explicit
75
+ `RunPython.noop`) when reversal is meaningful for the data being changed.
76
+
77
+ **Settings and secrets**
78
+ - `SECRET_KEY` hard-coded or the `django-insecure-` placeholder left in a
79
+ settings module used outside local dev.
80
+ - `DEBUG = True` or an empty/`["*"]` `ALLOWED_HOSTS` in a settings module
81
+ reachable in production.
82
+
83
+ **Forms/serializers**
84
+ - `fields = "__all__"` on a `ModelForm`/`ModelSerializer` that accepts
85
+ user-submitted input.
86
+
87
+ ### Step 3: Report
88
+
89
+ For each finding: file:line, category, what's wrong, and the safe
90
+ alternative (cite the exact API, e.g. "add `.select_related(\"author\")` to
91
+ this queryset").
92
+
93
+ ```
94
+ django-code-review: 3 findings
95
+ [n+1] views.py:22 — `Book.objects.all()` iterated with `book.author.name`
96
+ in the template; add `.select_related("author")`
97
+ [xss] profile.py:15 — `mark_safe(profile.bio)` on a user-submitted field;
98
+ remove mark_safe and let auto-escaping handle it
99
+ [csrf] webhooks.py:8 — `@csrf_exempt` on a view still using
100
+ `SessionAuthentication`; either drop the exemption or switch to a
101
+ signed-payload auth scheme
102
+ ```
103
+
104
+ ## Rules
105
+
106
+ - NEVER edit the files under review — report findings only.
107
+ - Cite the specific line and the specific safe alternative; a vague "this
108
+ could be an issue" finding is not actionable.
109
+ - Do not duplicate a finding the project's own configured `ruff`/`mypy`/
110
+ `manage.py check` already enforce and would catch on their own — focus on
111
+ what static tooling misses (N+1 patterns needing template/serializer
112
+ context, `mark_safe` on genuinely user-traceable data, migration/model
113
+ drift).
114
+ - Distinguish a real bug from a stylistic preference; a stylistic point
115
+ belongs in `rules/coding-style.mdc`/`rules/patterns.mdc`, not a review
116
+ finding blocking the change.
117
+
118
+ ## Red Flags
119
+
120
+ | Rationalization | Why it is wrong |
121
+ |---|---|
122
+ | "The `mark_safe` call is fine, it's inside an admin-only view" | An admin-only view still renders content a user submitted through some earlier form; check where the value actually originated, not just where it's rendered |
123
+ | "It's just a review, I'll add select_related myself since it's one line" | This skill is read-only; even a trivial fix belongs to the author or `django-implementation`, not a silent edit during review |
124
+ | "The N+1 loop only ever runs over 3 rows in the demo data" | Demo/test data size does not bound production data size; flag it regardless of current call sites |
125
+ | "The migration file looks fine, I don't need to check it matches the model diff" | A hand-edited or missing migration is invisible until deploy; always cross-check migration operations against the actual model change |
126
+
127
+ ## Verification
128
+
129
+ Do not report the review done until all of the following hold:
130
+
131
+ - Every changed Django file in scope was checked against all seven
132
+ categories in Step 2.
133
+ - No finding duplicates something the project's own configured linter/type
134
+ checker/`manage.py check` already flags and enforces.
135
+ - Every finding names a file:line, the specific problem, and a specific
136
+ fix — no vague findings.
137
+ - No file under review was modified.
@@ -0,0 +1,48 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "Can you look over the changes in this Django PR (#482) and flag any N+1 query risk before it ships?",
5
+ "Check this Django PR for CSRF or XSS issues",
6
+ "Audit this Django view for security problems around user input",
7
+ "Review this Django migration to make sure it matches the model change",
8
+ "Does this queryset in our Django Order model hit the DB once per row, or is select_related missing somewhere?",
9
+ "Review this Django REST Framework serializer for mass-assignment risk"
10
+ ],
11
+ "negative": [
12
+ "Implement this new feature in this Django view, don't just review it",
13
+ "Review this plain Python module for bare except clauses",
14
+ "Review this FastAPI endpoint for missing auth or CORS misconfiguration",
15
+ "Review this Node.js Express route for injection vulnerabilities",
16
+ "Fix the failing Django migration, don't just report what's wrong",
17
+ "Write pytest tests covering this Django view's permission checks",
18
+ "Review this React component for unnecessary re-renders"
19
+ ]
20
+ },
21
+ "scenarios": [
22
+ {
23
+ "id": "flag-raw-query-string-interpolation",
24
+ "prompt": "Review this Django code for security issues:\n\n```python\ndef search_orders(request):\n status = request.GET.get(\"status\", \"\")\n orders = Order.objects.raw(\n f\"SELECT * FROM orders_order WHERE status = '{status}'\"\n )\n return render(request, \"orders/search.html\", {\"orders\": orders})\n```",
25
+ "strictness": "high",
26
+ "expected_behavior": [
27
+ {
28
+ "grader": "judge",
29
+ "rubric": "A correct review flags that the `status` query parameter is interpolated directly into the raw SQL string with an f-string, which is a SQL injection vulnerability, and recommends passing it through `raw()`'s `params` argument instead.",
30
+ "pass_criteria": [
31
+ "identifies the f-string interpolation of `status` into the raw SQL string as a SQL injection vulnerability, citing the specific line/code",
32
+ "recommends the fix concretely: pass `status` via the `params` argument (e.g. `Order.objects.raw(\"SELECT * FROM orders_order WHERE status = %s\", [status])`), not just \"sanitize the input\" in the abstract"
33
+ ],
34
+ "fail_criteria": [
35
+ "reviews the code without flagging the f-string SQL interpolation as a vulnerability, or suggests a fix that still builds the query string by interpolating `status` (e.g. escaping quotes manually) instead of using `params`. Mentioning that raw() with string interpolation is dangerous only as background context, while still flagging this exact line, is not this failure."
36
+ ]
37
+ }
38
+ ],
39
+ "calibration": {
40
+ "known_right": "```\n[security] views.py:4 — `Order.objects.raw(f\"...status = '{status}'\")` builds the raw SQL by\n interpolating the `status` GET parameter directly into the string with an\n f-string. This is a SQL injection vulnerability: a value like\n `x' OR '1'='1` breaks out of the intended WHERE clause. Fix: pass status\n through raw()'s params argument instead --\n `Order.objects.raw(\"SELECT * FROM orders_order WHERE status = %s\", [status])`\n -- so the database driver parameterizes it rather than the string being\n built by hand.\n```\nThis names the exact line, explains the injection mechanism (breaking out of the string literal), and gives the concrete `params`-based fix rather than a general warning.",
41
+ "known_wrong": "```\nThe search_orders view looks fine -- it reads the status filter from the\nquery string and returns matching orders. raw() is a normal way to run a\ncustom query when the ORM's query builder doesn't fit, and status here is\njust a simple filter value.\n```\nThis approves the code without flagging that `status` is interpolated directly into the raw SQL string via an f-string, which is exactly the SQL injection pattern Django's own raw() documentation warns against.",
42
+ "vague": "Watch out for SQL injection risk anywhere user input reaches a raw query in this codebase.",
43
+ "subtle_wrong": "```\n[note] views.py:4 — the status filter is built with an f-string into the raw\n SQL. Since `request.GET.get(\"status\", \"\")` only returns simple query-string\n values (not file uploads or JSON bodies), and the orders_order table has no\n sensitive columns beyond what's already shown in the search results, this is\n low risk in practice. Worth cleaning up eventually but not blocking.\n```\nThis correctly locates the f-string interpolation but downgrades it to a non-blocking style note by reasoning that GET-parameter input is inherently lower risk and the table has no sensitive columns -- a plausible-sounding risk assessment that still recommends merging code with a live SQL injection vector, since any GET parameter is exactly the kind of value an attacker controls directly through the URL."
44
+ },
45
+ "anti_patterns": ["f-string"]
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: django-implementation
3
+ description: "Use when implementing or extending a feature in a Django (5.x) project -- covers models and migrations, class-based/function-based views, forms and serializers, querysets with select_related/prefetch_related to avoid N+1, the framework's CSRF/escaping defaults, and the code-level changes for a Django major-version upgrade (e.g. 4.x to 5.x; pair with django-migrate for any accompanying schema migration discipline). Not for plain Python with no Django import (see python-implementation)."
4
+ triggers:
5
+ - "add a django model for..."
6
+ - "write a django rest view that..."
7
+ - "implement this django feature"
8
+ - "add a new django app for..."
9
+ - "write a class-based view for..."
10
+ - "add a form/serializer that validates..."
11
+ - "implement a django queryset that..."
12
+ - "add a manager method to this django model"
13
+ metadata:
14
+ origin: authored
15
+ category: implement
16
+ version: "1.0.0"
17
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
18
+ license: "MIT"
19
+ ---
20
+
21
+ # Django implementation
22
+
23
+ Implement or extend a feature in a Django (5.x) project: models and
24
+ migrations, views (class-based or function-based), forms/serializers, and
25
+ query patterns that avoid the framework's most common performance and
26
+ security pitfalls. Scoped to writing production code in a Django project —
27
+ for plain Python with no Django import use `python-implementation`, for
28
+ writing/fixing tests use `django-testing`, for reviewing a diff without
29
+ editing it use `django-code-review`, for fixing a broken build/check use
30
+ `django-build-fix`, for a Django-version-major upgrade use `django-migrate`.
31
+
32
+ ## Workflow
33
+
34
+ ### Step 1: Discover the project's own conventions
35
+
36
+ 1. Read `settings.py`/`settings/` to find: installed apps, the configured
37
+ database backend, `AUTH_USER_MODEL`, whether Django REST Framework is
38
+ installed, and whether `pytest-django` is configured.
39
+ 2. Identify the target app (or whether a new app is warranted) — one app per
40
+ cohesive domain concern, per `rules/coding-style.mdc`.
41
+ 3. Read 1-2 neighboring `models.py`/`views.py`/`forms.py` in the same app
42
+ for: CBV vs FBV convention, `related_name` style, how validation is
43
+ split between model/form/serializer, and response-shape conventions
44
+ (plain Django views + templates vs. a DRF API).
45
+ 4. Check `requires-python`/CI config and the pinned Django version before
46
+ using a feature specific to a recent Django release.
47
+
48
+ ### Step 2: Design the change
49
+
50
+ - Model layer: choose fields, `related_name`, `Meta.constraints`
51
+ (`UniqueConstraint`, `CheckConstraint`) for invariants the database itself
52
+ should enforce, not only a form's `clean()`. See `rules/patterns.mdc`.
53
+ - Decide the view shape: a generic class-based view (`ListView`,
54
+ `DetailView`, `CreateView`, or a DRF `APIView`/`ViewSet`) for standard
55
+ CRUD, a function-based view or a CBV method override when the logic
56
+ doesn't map onto a generic view's hooks.
57
+ - Plan the query path before writing the view body: which related objects
58
+ will be accessed per row, and whether `select_related`/`prefetch_related`
59
+ is needed to avoid N+1 (see `rules/patterns.mdc`).
60
+ - Decide validation ownership: a per-field/cross-field rule that must hold
61
+ for every write path goes on the model (`Meta.constraints`,
62
+ `clean()`/`full_clean()`); a rule specific to one submission path goes on
63
+ that `Form`/`Serializer`.
64
+
65
+ ### Step 3: Implement
66
+
67
+ 1. Add/update the model, then generate the migration:
68
+ ```bash
69
+ python manage.py makemigrations
70
+ ```
71
+ Review the generated migration file; never hand-write `operations` to
72
+ match a model edit by guesswork.
73
+ 2. Write the view, applying `select_related`/`prefetch_related` on the
74
+ queryset for every relation the view or its template/serializer accesses
75
+ per row.
76
+ 3. Write the form/serializer with an explicit `fields = [...]` list (never
77
+ `"__all__"` on user-submitted input) and a `clean_<field>`/`validate_<field>`
78
+ method for interdependent validation.
79
+ 4. Wire the URL with `path()`, namespaced under the app, reversed with
80
+ `reverse()`/`{% url %}` rather than a hand-built path string.
81
+ 5. Follow `rules/coding-style.mdc` for naming/layout, `rules/patterns.mdc`
82
+ for query/migration idiom; check `rules/security.mdc` before touching
83
+ `mark_safe`/`|safe`, `.raw()`/`.extra()`, CSRF decorators, or settings.
84
+
85
+ ### Step 4: Verify
86
+
87
+ ```bash
88
+ python manage.py check
89
+ python manage.py makemigrations --check --dry-run
90
+ ruff check .
91
+ ruff format --check .
92
+ mypy . # with django-stubs, if configured
93
+ python manage.py test # or: pytest, if pytest-django is configured
94
+ ```
95
+
96
+ Prefix each command with the project's own run prefix (`uv run`, `poetry
97
+ run`) when it uses one — discovered in Step 1.
98
+
99
+ ### Step 5: Report
100
+
101
+ ```
102
+ Implemented: billing/models.py, billing/views.py, billing/migrations/0007_add_invoice_status.py
103
+ - added `Invoice.status` field + `Status` choices, migration generated and reviewed
104
+ - `InvoiceListView` uses select_related("customer") to avoid N+1
105
+ - manage.py check / makemigrations --check / ruff / mypy / test: all green
106
+ ```
107
+
108
+ ## Rules
109
+
110
+ - ALWAYS discover and match the project's own conventions (Step 1) before
111
+ assuming DRF, `pytest-django`, or a specific Django version's features.
112
+ - ALWAYS run `makemigrations` to generate a migration from a model change;
113
+ never hand-write migration `operations` to match an edit by guesswork.
114
+ - NEVER apply `mark_safe()`/`|safe` to a value that traces back to user
115
+ input, and never add `@csrf_exempt` to make a failing request succeed
116
+ without confirming an equivalent protection exists first (`rules/security.mdc`).
117
+ - NEVER build a `.raw()`/`.extra()` query by interpolating a value into the
118
+ SQL string; pass it through the `params` argument.
119
+ - NEVER leave `fields = "__all__"` on a `ModelForm`/`ModelSerializer` that
120
+ accepts user-submitted data.
121
+
122
+ ## Red Flags
123
+
124
+ | Rationalization | Why it is wrong |
125
+ |---|---|
126
+ | "I'll just loop and call `.save()` per row, it's simpler than `bulk_create`" | An N+1 on the write side; use `bulk_create`/`bulk_update` for a batch write |
127
+ | "This template value is safe, I'll mark it `\|safe` to stop the escaping" | If the value ever traces back to user input, this reintroduces XSS; escape it properly or sanitize before rendering instead |
128
+ | "`@csrf_exempt` makes the POST succeed, I'll leave it" | A passing request after exempting CSRF proves the exemption works, not that it's safe on a session-authenticated endpoint |
129
+ | "I'll hand-edit the migration file, `makemigrations` produced something odd" | The generated file reflects the real model diff; fix the model or the migration's logic, don't paper over a mismatch by hand |
130
+
131
+ ## Verification
132
+
133
+ Do not report the work done until all of the following hold:
134
+
135
+ - Every model change has a corresponding migration generated by
136
+ `makemigrations` and reviewed, not hand-written.
137
+ - A queryset the view/serializer iterates and dereferences per row uses
138
+ `select_related`/`prefetch_related` for the relations it accesses.
139
+ - `python manage.py check`, `makemigrations --check --dry-run`, `ruff
140
+ check .`, `ruff format --check .`, `mypy .` (or the project's configured
141
+ equivalents) all exit 0.
142
+ - `python manage.py test`/`pytest` (whichever the project configures)
143
+ passes; if the feature needs new tests, hand off to `django-testing`
144
+ rather than writing test files as part of this skill's own change set.
145
+ - No `mark_safe`/`|safe` on user-traceable data, no `.raw()`/`.extra()`
146
+ built by string interpolation, and no `@csrf_exempt` added, introduced by
147
+ this change.
@@ -0,0 +1,75 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "Add a Django model for Invoice with a status field and generate the migration",
5
+ "Write a Django REST Framework view that lists a customer's orders with their line items",
6
+ "Implement a class-based view in Django that shows a book's detail page along with its author",
7
+ "We need a standalone app to own the subscription-billing logic -- can you scaffold a Django app with models, admin, and urls set up?",
8
+ "Write a Django form that validates a signup submission with cross-field checks",
9
+ "Implement a queryset method on this Django model that returns only pending orders",
10
+ "Add a manager method to the Order model that avoids N+1 when listing items"
11
+ ],
12
+ "negative": [
13
+ "Implement this in plain Python with no framework, just a script that parses a CSV",
14
+ "Implement this FastAPI path operation that returns a list of users, not a Django view",
15
+ "Write a pytest test for this FastAPI endpoint that returns a list of users",
16
+ "Review this Node.js Express route for security issues",
17
+ "Add type hints to this untyped Python function in our shared utils package",
18
+ "Implement a React component that renders a list of the same orders",
19
+ "Fix this ModuleNotFoundError when importing mypkg.util in our plain Python service"
20
+ ]
21
+ },
22
+ "scenarios": [
23
+ {
24
+ "id": "select-related-n1",
25
+ "prompt": "Write a Django view function `book_list` that renders every Book in a template, where the template also displays each book's author name.",
26
+ "strictness": "high",
27
+ "expected_behavior": [
28
+ {
29
+ "grader": "judge",
30
+ "rubric": "A correct answer builds the queryset with `select_related` on the author foreign key so the author's name is fetched in the same query as the books, rather than triggering one extra query per book when the template accesses `book.author.name`.",
31
+ "pass_criteria": [
32
+ "the queryset passed to the template is built with `Book.objects.select_related(\"author\")` (or an equivalent named relation) shown in actual code, not merely described as an intention",
33
+ "the view returns/renders that queryset so the template's per-book access to `book.author.name` does not trigger an additional query per row"
34
+ ],
35
+ "fail_criteria": [
36
+ "iterates or renders `Book.objects.all()` (or any queryset with no `select_related`/`prefetch_related` for the author relation) while the template accesses `book.author` per row, causing one query per book. Mentioning select_related only to describe the problem is not a failure."
37
+ ]
38
+ }
39
+ ],
40
+ "calibration": {
41
+ "known_right": "```python\nfrom django.shortcuts import render\n\ndef book_list(request):\n books = Book.objects.select_related(\"author\").all()\n return render(request, \"books/list.html\", {\"books\": books})\n```\n`select_related(\"author\")` performs a SQL join and pulls each book's author row into the same query as the books themselves, so the template's `{{ book.author.name }}` on every row hits data already loaded in Python instead of issuing a fresh query per book. This is the right choice specifically because `author` is a `ForeignKey` (single-valued) -- `select_related` only works for foreign-key/one-to-one relations; a many-valued relation would need `prefetch_related` instead.",
42
+ "known_wrong": "```python\nfrom django.shortcuts import render\n\ndef book_list(request):\n books = Book.objects.all()\n return render(request, \"books/list.html\", {\"books\": books})\n```\nThis just fetches all the books and hands them to the template -- simple and it works, since the template can access `book.author.name` on each one via Django's normal relation traversal, and `Book.objects.all()` is the standard way to list every row in a table.",
43
+ "vague": "Make sure the author lookup doesn't cause a separate query for every book in the list -- optimize the queryset before it reaches the template.",
44
+ "subtle_wrong": "```python\nfrom django.shortcuts import render\n\ndef book_list(request):\n books = Book.objects.select_related(\"publisher\")\n return render(request, \"books/list.html\", {\"books\": books})\n```\nAdding `select_related(\"publisher\")` eager-loads the book-publisher join in the same query, which is exactly the kind of optimization this queryset needed -- it turns what would otherwise be a per-row lookup into a single upfront join, so the view no longer pays a query-per-book cost when the template walks the list."
45
+ }
46
+ },
47
+ {
48
+ "id": "no-mark-safe-on-user-bio",
49
+ "prompt": "Implement a Django view that renders a user's bio -- submitted earlier through a profile-edit form -- on their public profile page template.",
50
+ "strictness": "high",
51
+ "expected_behavior": [
52
+ {
53
+ "grader": "judge",
54
+ "rubric": "A correct answer renders the user-submitted bio through Django's normal template auto-escaping and does not call `mark_safe()` or apply the `|safe` filter to it, since the bio is user-controlled data and marking it safe would reintroduce stored XSS.",
55
+ "pass_criteria": [
56
+ "passes the bio to the template as an ordinary context variable rendered with normal auto-escaping (no `mark_safe()` call and no `|safe` filter applied to the bio anywhere in the view or template code shown)",
57
+ "shows the actual view/template code implementing this, not just a description of the approach"
58
+ ],
59
+ "fail_criteria": [
60
+ "wraps the bio in `mark_safe()` or renders it with the `|safe` template filter so it bypasses Django's auto-escaping. Mentioning `mark_safe`/`|safe` only to warn that they must not be used on user input is not a failure."
61
+ ]
62
+ }
63
+ ],
64
+ "calibration": {
65
+ "known_right": "```python\n# views.py\ndef profile_detail(request, username):\n profile = get_object_or_404(Profile, user__username=username)\n return render(request, \"profiles/detail.html\", {\"profile\": profile})\n```\n```html+django\n<!-- profiles/detail.html -->\n<p>{{ profile.bio }}</p>\n```\nThe bio is passed straight through as a normal context variable, so Django's template auto-escaper HTML-escapes it before rendering -- any `<script>` a user typed into their bio renders as inert text, not executable markup. No `mark_safe()`/`|safe` is used anywhere, because the bio is user-submitted data and marking it safe would let a user's own script tag execute in every visitor's browser (stored XSS).",
66
+ "known_wrong": "```python\n# views.py\nfrom django.utils.safestring import mark_safe\n\ndef profile_detail(request, username):\n profile = get_object_or_404(Profile, user__username=username)\n bio_html = mark_safe(profile.bio)\n return render(request, \"profiles/detail.html\", {\"bio_html\": bio_html})\n```\nWrapping the bio in `mark_safe()` before passing it to the template lets it render exactly as the user typed it, including any basic formatting they added -- `mark_safe` is the documented way to tell Django a string is already safe HTML, so this is the standard tool for showing user content as-is.",
67
+ "vague": "Be careful with how the bio gets rendered so a user can't inject a script into their own profile page that runs for other visitors.",
68
+ "subtle_wrong": "```python\n# views.py\ndef profile_detail(request, username):\n profile = get_object_or_404(Profile, user__username=username)\n return render(request, \"profiles/detail.html\", {\"profile\": profile})\n```\n```html+django\n<!-- profiles/detail.html -->\n<p>{{ profile.bio|safe }}</p>\n```\nThe view itself stays simple and passes the profile through untouched -- the `|safe` filter is applied only in the template, right where the bio is displayed, which keeps the escaping decision colocated with the markup instead of scattered across the view layer."
69
+ },
70
+ "anti_patterns": [
71
+ "mark_safe"
72
+ ]
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: django-migrate
3
+ description: "Use when generating, reviewing, or safely applying Django schema/data migrations -- makemigrations for a model change, RunPython data migrations with forward/reverse functions, merge migrations for divergent branches, and staged rollout for a migration that must ship alongside a multi-phase deploy (e.g. a column removal). Not for a Django major-version framework upgrade (there is no separate django-upgrade skill in this pack; the code-level upgrade work is owned by django-implementation) or for fixing a broken/conflicting migration that's blocking the build (see django-build-fix)."
4
+ triggers:
5
+ - "generate a django migration for this model change"
6
+ - "write a data migration to backfill this field"
7
+ - "make this django migration reversible"
8
+ - "how do I safely drop this column in django"
9
+ - "squash these django migrations"
10
+ - "write a RunPython migration"
11
+ - "stage this django schema change across two deploys"
12
+ metadata:
13
+ origin: authored
14
+ category: migrate
15
+ version: "1.0.0"
16
+ compatible_harnesses: "claude,codex,cursor,zed,opencode"
17
+ license: "MIT"
18
+ ---
19
+
20
+ # Django migrate
21
+
22
+ Generate, review, and safely apply Django schema and data migrations:
23
+ `makemigrations` for a model change, `RunPython` for data transforms with
24
+ forward/reverse functions, and staged rollout for a migration whose schema
25
+ change would break code still running the previous release. Scoped to the
26
+ migration itself — for the model/view/form code change that motivates the
27
+ migration use `django-implementation`; for resolving a migration that is
28
+ already conflicting or blocking the build use `django-build-fix`.
29
+
30
+ ## Workflow
31
+
32
+ ### Step 1: Establish what's changing and why
33
+
34
+ 1. Diff the model change driving the migration (new field, changed
35
+ constraint, removed field, renamed field/table) against the current
36
+ migration graph's state for that app.
37
+ 2. Decide whether this is schema-only (Django can express it entirely as
38
+ `AddField`/`AlterField`/etc.) or needs a data transform (backfilling a
39
+ new non-nullable field, splitting one column into two, converting a
40
+ value's representation) — a data transform needs a `RunPython` step, not
41
+ just the schema operation.
42
+ 3. Check whether the change is destructive for code still running the
43
+ previous release (a column removal, a rename, a type narrowing) — if the
44
+ project deploys code and migrations as separate steps, this needs a
45
+ staged rollout (Step 4), not a single migration.
46
+
47
+ ### Step 2: Generate the migration
48
+
49
+ ```bash
50
+ python manage.py makemigrations <app_label>
51
+ ```
52
+
53
+ Let Django diff the models against the existing migration graph; never
54
+ hand-write `operations` to match a model edit by guesswork. Review the
55
+ generated file: correct operation types, correct `dependencies`, and no
56
+ unexpected side operation (an accidental `AlterField` on an untouched field
57
+ usually means the model's `Meta` or a field kwarg drifted from what the
58
+ last migration recorded).
59
+
60
+ ### Step 3: Add data-transform logic when needed
61
+
62
+ - Add a `migrations.RunPython(forward, reverse)` operation for any
63
+ transform that touches existing row data. Write both directions:
64
+ `forward` performs the transform, `reverse` undoes it (or use
65
+ `migrations.RunPython.noop` for `reverse` only when undoing is genuinely
66
+ not meaningful, e.g. an irreversible data merge — state that explicitly
67
+ rather than omitting the argument).
68
+ - Fetch models inside `RunPython` via `apps.get_model("app", "Model")` (the
69
+ historical, frozen-at-this-migration model), never by importing the
70
+ live model class — the live model can have fields this migration's
71
+ historical state doesn't, which breaks replaying migration history from
72
+ scratch.
73
+ - For a backfill on a large table, batch the update (`.iterator()` +
74
+ chunked `.bulk_update()`, or `Model.objects.filter(...).update(...)` when
75
+ the value doesn't depend on per-row computation) rather than a single
76
+ `.save()`-per-row loop.
77
+
78
+ ### Step 4: Stage a destructive/breaking change
79
+
80
+ For a schema change that would break code from the previous release still
81
+ running during a rolling deploy (removing a column code still reads, a
82
+ rename, `NOT NULL` added without a default):
83
+
84
+ 1. **Release N:** ship the code change that stops reading/writing the old
85
+ shape, while the schema still has it (e.g. stop reading the old column,
86
+ start reading/writing the new one; add the new nullable column alongside
87
+ the old one).
88
+ 2. **Release N+1:** once release N is fully rolled out, ship the migration
89
+ that actually drops/alters the old schema — nothing still running reads
90
+ it by then.
91
+ 3. Never combine "stop using the old column in code" and "drop the old
92
+ column in the same migration" in one release when the project's deploy
93
+ process can run old code against the new schema for any window (rolling
94
+ deploy, canary, multiple app servers) — that window is exactly when a
95
+ still-running old-code instance would break.
96
+
97
+ ### Step 5: Merge conflicting migrations
98
+
99
+ When two branches added divergent migrations from the same parent:
100
+
101
+ ```bash
102
+ python manage.py makemigrations --merge
103
+ ```
104
+
105
+ Review the generated merge migration; never hand-edit either migration's
106
+ `dependencies` to force a resolution instead (see `django-build-fix` if this
107
+ is blocking a currently-broken build).
108
+
109
+ ### Step 6: Verify and report
110
+
111
+ ```bash
112
+ python manage.py makemigrations --check --dry-run
113
+ python manage.py migrate --plan
114
+ python manage.py test # or: pytest, if pytest-django is configured
115
+ ```
116
+
117
+ ```
118
+ Migrated: accounts app
119
+ - 0012_backfill_display_name.py: RunPython forward fills display_name from
120
+ first_name+last_name for existing rows; reverse clears it back to ""
121
+ - staged: this release only adds the nullable display_name column and
122
+ backfills it; a follow-up release will make it NOT NULL and stop
123
+ accepting the old two-field form
124
+ - makemigrations --check, migrate --plan, test: all green
125
+ ```
126
+
127
+ ## Rules
128
+
129
+ - ALWAYS generate migrations with `makemigrations`; never hand-write
130
+ `operations` to match a model edit by guesswork.
131
+ - ALWAYS write both `forward` and `reverse` for a `RunPython` data
132
+ migration, or explicitly use `RunPython.noop` with a stated reason when
133
+ reversal isn't meaningful.
134
+ - ALWAYS fetch models inside a `RunPython` function via
135
+ `apps.get_model(...)`, never the live imported model class.
136
+ - NEVER combine dropping/breaking a schema shape with removing the last
137
+ code that reads it in the same release when the deploy process can run
138
+ old code against the new schema for any window — stage it across two
139
+ releases instead.
140
+ - NEVER hand-edit a migration's `dependencies`/`operations` to resolve a
141
+ conflict — generate a merge migration and review it.
142
+
143
+ ## Red Flags
144
+
145
+ | Rationalization | Why it is wrong |
146
+ |---|---|
147
+ | "I'll drop the old column and update the code in the same migration/release" | Breaks any old-code instance still running against the new schema during a rolling deploy; stage it across two releases |
148
+ | "I'll import the live model in this RunPython function, it's simpler" | The live model can drift from what this migration's historical state expects, breaking a from-scratch replay of migration history; use `apps.get_model(...)` |
149
+ | "This data migration doesn't need a reverse, I'll just leave it out" | Leaves `migrate <app> <previous>` broken for this migration; use `RunPython.noop` explicitly if reversal truly isn't meaningful |
150
+ | "I'll hand-edit the migration's `dependencies` to resolve this merge conflict" | Desyncs the migration graph from what other environments already recorded as applied; use `makemigrations --merge` |
151
+
152
+ ## Verification
153
+
154
+ Do not report the work done until all of the following hold:
155
+
156
+ - The migration was generated by `makemigrations` (or `--merge`), not
157
+ hand-written from scratch.
158
+ - Any data transform uses `RunPython` with both `forward` and `reverse` (or
159
+ an explicit, justified `RunPython.noop`), fetching models via
160
+ `apps.get_model(...)`.
161
+ - A destructive/breaking schema change is staged across two releases when
162
+ the project's deploy process can run old and new code concurrently.
163
+ - `makemigrations --check --dry-run` and `migrate --plan` both succeed with
164
+ no unexpected pending operation, and the test suite passes.
165
+ - No migration file's `dependencies`/`operations` was hand-edited outside
166
+ what `makemigrations`/`--merge` generated.
@@ -0,0 +1,49 @@
1
+ {
2
+ "triggers": {
3
+ "positive": [
4
+ "Generate a Django migration for this new required field on the Order model",
5
+ "The display_name column needs to be populated from existing first_name/last_name values on all existing rows -- what's the right way to do that as a Django migration?",
6
+ "This Django migration only defines a forward RunPython operation -- how do I add the reverse function so it can be rolled back cleanly with `migrate`?",
7
+ "We're removing the `legacy_phone` column from a Django model via a rolling deploy -- what's the safe sequencing so instances running the old code don't break mid-rollout?",
8
+ "The accounts app in our Django project has accumulated 40+ migration files going back two years -- can you collapse them into a smaller set?",
9
+ "Write a RunPython migration that normalizes existing phone numbers",
10
+ "We want to rename a column but can't do it in one shot with our rolling deploy setup -- how should I split this Django schema change into multiple releases?"
11
+ ],
12
+ "negative": [
13
+ "Fix this Django migration conflict that's currently blocking makemigrations from running",
14
+ "Implement the actual view logic that will use this new field, not the migration",
15
+ "Write a database migration for our Node.js service using Knex",
16
+ "Write an Alembic migration for our FastAPI service's SQLAlchemy models",
17
+ "Add a pytest-django test for this model's new field",
18
+ "Run a Prisma migration for our TypeScript backend",
19
+ "Review this Django migration for correctness, don't write a new one"
20
+ ]
21
+ },
22
+ "scenarios": [
23
+ {
24
+ "id": "stage-destructive-column-drop",
25
+ "prompt": "We deploy Django app code and run migrations as separate steps, with a rolling deploy across several app servers. The `User` model has an old `full_name` column that's now replaced by separate `first_name`/`last_name` columns; all reads/writes of `full_name` in the codebase have already been removed in the code we're about to ship. How should the migration to actually drop the `full_name` column be sequenced?",
26
+ "strictness": "high",
27
+ "expected_behavior": [
28
+ {
29
+ "grader": "judge",
30
+ "rubric": "A correct answer ships the code that stops reading/writing full_name in one release, and only drops the full_name column in a later, separate release/migration once that release's rollout has been confirmed complete -- not merely scheduled after the deploy step in a pipeline. Placing a migration step after a deploy step in the same pipeline run does not, by itself, prove every instance has finished rolling onto the new code (a stalled or partially-failed rollout can leave old-code instances running well after the deploy step reports success), and keeping the drop in the same release as the code change is unsafe regardless of step ordering or added verification within that release -- the column drop must be its own, later release, gated on confirming the prior release's rollout is fully complete.",
31
+ "pass_criteria": [
32
+ "states that dropping the `full_name` column must happen in a separate, later migration/release from the code change that stops using it, and that this later step should only run once the prior release's rollout is confirmed complete (not merely scheduled after it in a pipeline)",
33
+ "explains why: during a rolling deploy, some app server instances still run the old code (which still reads/writes `full_name`) while others already run the new code, so dropping the column before every instance has actually finished rolling onto the new code would break the old instances"
34
+ ],
35
+ "fail_criteria": [
36
+ "recommends dropping the `full_name` column in the same release/migration as the code change that removes its usage. Noting that combining them would be 'faster' only to then reject it as unsafe is not this failure.",
37
+ "treats scheduling the migration step to run after the deploy step within the same release's pipeline as sufficient on its own to guarantee every old-code instance is gone, with no verification (e.g. checking rollout/replica status or a health check) that the rollout actually completed"
38
+ ]
39
+ }
40
+ ],
41
+ "calibration": {
42
+ "known_right": "Ship the code change (already done here, since the codebase no longer reads/writes `full_name`) as its own release first, and confirm it has fully rolled out to every app server -- e.g. checking that the deployment shows 100% of instances on the new revision, not just that the deploy step exited successfully. Only after that confirmation -- in a separate, later release -- generate and apply the migration that actually drops the `full_name` column:\n```bash\npython manage.py makemigrations accounts # in the follow-up release\n```\nThis produces a `RemoveField` operation for `full_name`. The reason for the two-release split plus the explicit confirmation: during a rolling deploy some servers are still running the previous release's code, which still references `full_name`, while others already run the new code, and a deploy step reporting success doesn't guarantee every instance has actually finished cycling onto the new code (a stuck pod, a slow canary, or a failed health check can leave old instances running past that point). Splitting into a later release and confirming rollout completion before touching the schema removes that window entirely.",
43
+ "known_wrong": "Since the code no longer reads or writes `full_name` at all, it's safe to include the column drop in this same release -- generate the migration now:\n```bash\npython manage.py makemigrations accounts\n```\nand ship it alongside the code change. There's no reason to wait for a second release since the application layer is already fully decoupled from that column.",
44
+ "vague": "Don't drop the column and remove the code that uses it at the same time -- give it some separation so nothing breaks during the rollout.",
45
+ "subtle_wrong": "You can ship the column drop in this same release as long as the migration step is placed after the code-deploy step in the pipeline definition, rather than before or during it -- since the pipeline runs steps in order, by the time the migration step executes every server is guaranteed to already be on the new code. That keeps it to one release instead of splitting into two: the ordering of steps within the pipeline is what actually matters, not which release they belong to, and a deploy step that has exited successfully means the rollout is done."
46
+ }
47
+ }
48
+ ]
49
+ }