@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.
- package/dist/cli.js +872 -316
- package/docs/README.md +2 -0
- package/package.json +1 -1
- package/src/gdskills/bundled/install-manifest.json +319 -76
- package/src/gdskills/bundled/stacks/django/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/django/governance/eval.json +1763 -0
- package/src/gdskills/bundled/stacks/django/governance/scout.json +40 -0
- package/src/gdskills/bundled/stacks/django/pack.json +43 -0
- package/src/gdskills/bundled/stacks/django/rules/coding-style.mdc +80 -0
- package/src/gdskills/bundled/stacks/django/rules/patterns.mdc +92 -0
- package/src/gdskills/bundled/stacks/django/rules/security.mdc +92 -0
- package/src/gdskills/bundled/stacks/django/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/django/skills/django-build-fix/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/django/skills/django-build-fix/evals.json +49 -0
- package/src/gdskills/bundled/stacks/django/skills/django-code-review/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/django/skills/django-code-review/evals.json +48 -0
- package/src/gdskills/bundled/stacks/django/skills/django-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/django/skills/django-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/django/skills/django-migrate/SKILL.md +166 -0
- package/src/gdskills/bundled/stacks/django/skills/django-migrate/evals.json +49 -0
- package/src/gdskills/bundled/stacks/django/skills/django-testing/SKILL.md +130 -0
- package/src/gdskills/bundled/stacks/django/skills/django-testing/evals.json +48 -0
- package/src/gdskills/bundled/stacks/fastapi/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/fastapi/governance/eval.json +1777 -0
- package/src/gdskills/bundled/stacks/fastapi/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/fastapi/pack.json +43 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/coding-style.mdc +68 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/patterns.mdc +108 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/security.mdc +99 -0
- package/src/gdskills/bundled/stacks/fastapi/rules/testing.mdc +85 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/SKILL.md +157 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-build-fix/evals.json +76 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/SKILL.md +150 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/SKILL.md +158 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/SKILL.md +146 -0
- package/src/gdskills/bundled/stacks/fastapi/skills/fastapi-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/eval.json +2194 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/governance/scout.json +39 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/pack.json +40 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/coding-style.mdc +67 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/patterns.mdc +65 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/security.mdc +69 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/rules/testing.mdc +80 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/SKILL.md +144 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-migrate/evals.json +74 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/SKILL.md +128 -0
- package/src/gdskills/bundled/stacks/java-kotlin-spring/skills/java-kotlin-spring-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/python/agent-refs.json +2 -1
- package/src/gdskills/bundled/stacks/python/pack.json +1 -1
- package/src/gdskills/bundled/stacks/rust/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/rust/governance/eval.json +1823 -0
- package/src/gdskills/bundled/stacks/rust/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/rust/pack.json +42 -0
- package/src/gdskills/bundled/stacks/rust/rules/coding-style.mdc +93 -0
- package/src/gdskills/bundled/stacks/rust/rules/patterns.mdc +85 -0
- package/src/gdskills/bundled/stacks/rust/rules/security.mdc +85 -0
- package/src/gdskills/bundled/stacks/rust/rules/testing.mdc +82 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/SKILL.md +141 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-build-fix/evals.json +78 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-code-review/evals.json +72 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/SKILL.md +133 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-implementation/evals.json +79 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-testing/SKILL.md +130 -0
- package/src/gdskills/bundled/stacks/rust/skills/rust-testing/evals.json +75 -0
- package/src/gdskills/bundled/agents/python-build-fixer.md +0 -52
- package/src/gdskills/bundled/agents/python-code-auditor.md +0 -49
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Use when a FastAPI app itself fails to start, import, or register a router -- app-startup failures, dependency-injection wiring errors (Depends() on the wrong callable, a missing sub-dependency), Pydantic v1/v2 model/validation errors, and a checker flagging a path-operation signature or response_model specifically, with the smallest root-cause fix. For a generic Python ModuleNotFoundError/packaging/dependency-resolver failure not specific to FastAPI's own app wiring, use python-build-fix.",
|
|
4
|
+
"decision": "fork",
|
|
5
|
+
"topMatch": "nestjs/nestjs-build-fix",
|
|
6
|
+
"recordedAt": "2026-09-25T18:06:20.873Z",
|
|
7
|
+
"skillName": "fastapi-build-fix",
|
|
8
|
+
"justification": "top match python/python-build-fix is the same build-fix category for generic Python tooling (mypy/ruff/pip), not FastAPI's own Depends resolution, router registration, and Pydantic validation-error failure modes; a FastAPI-scoped fork is warranted since python-build-fix has no visibility into FastAPI's own dependency-injection wiring."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "Use when reviewing FastAPI changes for correctness and safety risks -- checks a synchronous driver or library call left in a route handler's coroutine, request bodies accepted as dict/Any instead of Pydantic models, missing response_model filtering, missing or misplaced auth dependencies, unsafe CORS configuration, and hard-coded secrets. Read-only: reports findings, does not edit code, and is scoped to FastAPI's own request lifecycle rather than general-purpose backend correctness.",
|
|
12
|
+
"decision": "create",
|
|
13
|
+
"topMatch": "fastapi/fastapi-testing",
|
|
14
|
+
"recordedAt": "2026-09-25T18:06:21.226Z",
|
|
15
|
+
"skillName": "fastapi-code-review",
|
|
16
|
+
"justification": "top match python/python-code-review is the same review category for generic Python risks, not FastAPI's own async/blocking-call, response_model, and auth-dependency concerns; a FastAPI-scoped fork covers concerns python-code-review cannot see."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Use when implementing or extending a FastAPI path operation, dependency, or Pydantic schema -- covers async vs. sync path operations and FastAPI's threadpool behavior, Depends()-based dependency injection, Pydantic v2 request/response models, response_model filtering, background tasks, and OpenAPI/router conventions. Scoped to FastAPI's own request-handling surface, not general-purpose Python application code with no HTTP layer.",
|
|
20
|
+
"decision": "fork",
|
|
21
|
+
"topMatch": "fastapi/fastapi-testing",
|
|
22
|
+
"recordedAt": "2026-09-25T18:06:21.577Z",
|
|
23
|
+
"skillName": "fastapi-implementation",
|
|
24
|
+
"justification": "top match fastapi/fastapi-code-review is this same pack's read-only review skill, sharing only generic FastAPI-project vocabulary from its own audit scope, not implementation guidance; decision is fork since both are legitimately FastAPI-scoped but cover disjoint workflows (writing new code vs. auditing an existing diff without editing it)."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Use when you write, extend, or fix a FastAPI project's test suite -- driving synchronous or async HTTP requests against the running app in-process, app.dependency_overrides for auth/DB fixtures, asserting response status codes and response_model filtering, and mocking an external HTTP call. Scoped to a project that actually has a FastAPI HTTP layer under test, not a generic Python test suite with none.",
|
|
28
|
+
"decision": "fork",
|
|
29
|
+
"topMatch": "fastapi/fastapi-implementation",
|
|
30
|
+
"recordedAt": "2026-09-25T18:06:21.899Z",
|
|
31
|
+
"skillName": "fastapi-testing",
|
|
32
|
+
"justification": "top match python/python-testing is the same test-authoring category for generic pytest suites, not FastAPI's own TestClient/dependency_overrides/response-model-assertion idiom; a FastAPI-scoped fork is warranted since python-testing has no FastAPI-specific HTTP-layer guidance."
|
|
33
|
+
}
|
|
34
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "fastapi",
|
|
3
|
+
"family": "framework",
|
|
4
|
+
"extends": "python",
|
|
5
|
+
"modules": ["fastapi-rules", "fastapi-skills"],
|
|
6
|
+
"detectionMarkers": ["fastapi"],
|
|
7
|
+
"provenance": {
|
|
8
|
+
"origin": "authored",
|
|
9
|
+
"sourceRef": "flow 335, Wave 4 batch 3"
|
|
10
|
+
},
|
|
11
|
+
"stability": "experimental",
|
|
12
|
+
"skills": {
|
|
13
|
+
"implement": ["fastapi-implementation"],
|
|
14
|
+
"test": ["fastapi-testing"],
|
|
15
|
+
"review": ["fastapi-code-review"],
|
|
16
|
+
"build-fix": ["fastapi-build-fix"],
|
|
17
|
+
"migrate": []
|
|
18
|
+
},
|
|
19
|
+
"agentProfile": {
|
|
20
|
+
"displayName": "FastAPI",
|
|
21
|
+
"auditFocus": [
|
|
22
|
+
"a blocking call (sync DB driver, `requests`, `time.sleep`, sync file I/O) inside an `async def` path operation or dependency instead of a `def` one FastAPI can offload to its threadpool",
|
|
23
|
+
"a request body or query model that skips Pydantic validation by typing a parameter as `dict`/`Any` instead of a `BaseModel`",
|
|
24
|
+
"a path operation with no `response_model`/return type where one would prevent leaking internal fields (e.g. `hashed_password`) in the response",
|
|
25
|
+
"a dependency-injected auth check (`Depends(get_current_user)`) missing from a path operation that should require it",
|
|
26
|
+
"`CORSMiddleware` configured with `allow_origins=[\"*\"]` alongside `allow_credentials=True`, which browsers reject and which is unsafe if they didn't",
|
|
27
|
+
"secrets/config read from hard-coded values instead of `pydantic-settings`/environment, or a `SECRET_KEY` committed in source"
|
|
28
|
+
],
|
|
29
|
+
"buildCommands": [
|
|
30
|
+
"ruff check .",
|
|
31
|
+
"ruff format --check .",
|
|
32
|
+
"mypy . || pyright",
|
|
33
|
+
"pytest -x -q",
|
|
34
|
+
"python -c \"import <app_module>\" # or: uvicorn <app_module>:app --port 0 as an app-startup smoke check"
|
|
35
|
+
],
|
|
36
|
+
"fixGuardrails": [
|
|
37
|
+
"never add `# type: ignore` or `# noqa` to silence a checker without fixing or explaining the underlying issue",
|
|
38
|
+
"never move a blocking call into an `async def` path operation to \"simplify\" it -- keep it `def` so FastAPI's threadpool handles it, or make the call itself async",
|
|
39
|
+
"never widen a Pydantic model's field type to `Any`/`dict` just to make validation errors stop",
|
|
40
|
+
"fix the smallest root cause; do not refactor unrelated code while resolving a build/lint/type failure"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.py"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# FastAPI coding style
|
|
9
|
+
|
|
10
|
+
Narrows `python`'s coding style to FastAPI's own project conventions. A
|
|
11
|
+
FastAPI project is still a Python project — everything not specific to
|
|
12
|
+
FastAPI's own idiom still comes from `python`'s `rules/coding-style.mdc`,
|
|
13
|
+
which this file extends.
|
|
14
|
+
|
|
15
|
+
## Path operations
|
|
16
|
+
|
|
17
|
+
- Name path operation functions for the action they perform
|
|
18
|
+
(`read_item`, `create_user`), not for the HTTP method alone — the
|
|
19
|
+
decorator (`@app.get(...)`, `@app.post(...)`) already states the method.
|
|
20
|
+
- Group related path operations behind an `APIRouter` per resource/domain
|
|
21
|
+
(`routers/items.py` exposing `router = APIRouter(prefix="/items",
|
|
22
|
+
tags=["items"])`) rather than registering every route directly on a single
|
|
23
|
+
`app` instance once the project has more than a handful of endpoints.
|
|
24
|
+
- Declare an explicit `response_model` (or a return type annotation FastAPI
|
|
25
|
+
can use as one) on every path operation that returns a Pydantic model or
|
|
26
|
+
ORM object — this is what filters the response to the declared fields and
|
|
27
|
+
keeps an internal-only field (a password hash, an audit flag) from
|
|
28
|
+
leaking into the API response.
|
|
29
|
+
- Use `Annotated[<type>, Depends(...)]`/`Annotated[<type>, Query(...)]` etc.
|
|
30
|
+
for parameter declarations (the current recommended style) rather than
|
|
31
|
+
the bare `= Depends(...)`/`= Query(...)` default-value form, unless the
|
|
32
|
+
project's own code still uses the older form consistently.
|
|
33
|
+
|
|
34
|
+
## Pydantic models
|
|
35
|
+
|
|
36
|
+
- Define request/response schemas as Pydantic `BaseModel` subclasses, not
|
|
37
|
+
as bare `dict`/`Any` parameters — see `rules/patterns.mdc` for why this is
|
|
38
|
+
also the project's injection defense, not just a style preference.
|
|
39
|
+
- Keep a model's field constraints on the field itself (`Field(gt=0,
|
|
40
|
+
le=100)`, `Literal[...]` for a closed set of values) instead of
|
|
41
|
+
re-validating the same constraint by hand in the path operation body.
|
|
42
|
+
- Separate the model used for input (what a client may send) from the model
|
|
43
|
+
used for output (what the API returns) when they differ by even one field
|
|
44
|
+
— do not reuse a single model for both and rely on route logic to strip
|
|
45
|
+
the extra field before responding.
|
|
46
|
+
- Configure a model with `model_config = ConfigDict(...)` (Pydantic v2), not
|
|
47
|
+
the v1 inner `class Config:` — the project's Pydantic version is declared
|
|
48
|
+
in `pyproject.toml`; confirm it before assuming v2-only syntax.
|
|
49
|
+
|
|
50
|
+
## Dependencies
|
|
51
|
+
|
|
52
|
+
- Keep a dependency function small and single-purpose (fetch the current
|
|
53
|
+
user, open a DB session, parse a header) — a dependency that both
|
|
54
|
+
authenticates and performs business logic should split so the auth part
|
|
55
|
+
is reusable on its own.
|
|
56
|
+
- Prefer a `yield`-based dependency (`def get_db(): ... yield session ...
|
|
57
|
+
finally: session.close()`) for anything that needs teardown, over a
|
|
58
|
+
plain-return dependency plus a manual close call in the path operation.
|
|
59
|
+
|
|
60
|
+
## Project layout
|
|
61
|
+
|
|
62
|
+
- Match the project's existing layout — a `src/<package>/`
|
|
63
|
+
`routers/`, `models/`/`schemas/`, `dependencies.py`, `main.py` split for a
|
|
64
|
+
multi-router app, or a single `main.py` for a small service — rather than
|
|
65
|
+
introducing a different structure alongside it.
|
|
66
|
+
- Keep the FastAPI `app = FastAPI(...)` instantiation and top-level
|
|
67
|
+
middleware/router registration in one place (typically `main.py`); do not
|
|
68
|
+
scatter `app.include_router(...)` calls across unrelated modules.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.py"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# FastAPI patterns
|
|
9
|
+
|
|
10
|
+
Narrows `python`'s stack-agnostic design guidance (and its own
|
|
11
|
+
`asyncio`/resource patterns) to FastAPI's own request lifecycle: dependency
|
|
12
|
+
injection, async vs. sync path operations, response modeling, and
|
|
13
|
+
background work. Applies only to FastAPI project files.
|
|
14
|
+
|
|
15
|
+
## Async vs. sync path operations
|
|
16
|
+
|
|
17
|
+
- Declare a path operation `async def` only when its body actually awaits
|
|
18
|
+
something async (an async DB driver, `httpx.AsyncClient`, another
|
|
19
|
+
awaitable). An `async def` path operation runs directly on the event
|
|
20
|
+
loop, so a blocking call inside it (a sync DB driver, `requests.get`,
|
|
21
|
+
`time.sleep`, blocking file I/O, a CPU-bound loop) stalls every other
|
|
22
|
+
in-flight request on that worker.
|
|
23
|
+
- Declare a path operation with plain `def` when its body calls a blocking
|
|
24
|
+
library with no async equivalent. FastAPI runs a `def` path operation in
|
|
25
|
+
an external threadpool automatically — this is current, documented
|
|
26
|
+
behavior, not a workaround — so a blocking call inside it does not block
|
|
27
|
+
the event loop.
|
|
28
|
+
- The same rule applies to dependencies: a `def` dependency runs in the
|
|
29
|
+
threadpool, an `async def` dependency runs on the loop and must not
|
|
30
|
+
block. FastAPI accepts either kind of dependency under either kind of
|
|
31
|
+
path operation (an `async def` dependency under a `def` path operation
|
|
32
|
+
and vice versa), so choose per-dependency based on what its own body
|
|
33
|
+
does, not to match the path operation's own declaration.
|
|
34
|
+
- For a compute-only path operation with no I/O at all, prefer `async def`
|
|
35
|
+
— it avoids the threadpool's scheduling overhead for work that was never
|
|
36
|
+
going to block anything.
|
|
37
|
+
|
|
38
|
+
## Dependency injection
|
|
39
|
+
|
|
40
|
+
- Use `Depends(...)` (or `Security(...)` for an auth dependency that also
|
|
41
|
+
carries OAuth2 scopes) to inject anything a path operation needs but
|
|
42
|
+
should not construct itself: a DB session, the current authenticated
|
|
43
|
+
user, pagination parameters, a feature flag. This is what makes each
|
|
44
|
+
piece independently testable via FastAPI's `app.dependency_overrides`.
|
|
45
|
+
- Chain dependencies (a dependency that itself depends on another) instead
|
|
46
|
+
of duplicating the same setup logic across sibling dependencies — FastAPI
|
|
47
|
+
caches a dependency's result per request by default, so a shared
|
|
48
|
+
sub-dependency runs once even when several path-level dependencies need
|
|
49
|
+
it.
|
|
50
|
+
- Use `yield` in a dependency that owns a resource needing teardown (a DB
|
|
51
|
+
session, a lock, an open connection); the code after `yield` runs after
|
|
52
|
+
the response is sent, even when the path operation raised — this is
|
|
53
|
+
FastAPI's own resource-lifecycle pattern for a request-scoped resource,
|
|
54
|
+
not a manual `try`/`finally` bolted onto a plain-return dependency.
|
|
55
|
+
|
|
56
|
+
## Request/response modeling
|
|
57
|
+
|
|
58
|
+
- Model every request body as a Pydantic `BaseModel`, never as a raw
|
|
59
|
+
`dict`/`Any` parameter — FastAPI validates the incoming JSON against the
|
|
60
|
+
model before the path operation body runs, which is the framework's own
|
|
61
|
+
first line of defense against malformed or malicious input (see
|
|
62
|
+
`rules/security.mdc`).
|
|
63
|
+
- Use `field_validator`/`model_validator` (Pydantic v2) for a constraint
|
|
64
|
+
that spans multiple fields or needs custom logic beyond `Field(...)`'s
|
|
65
|
+
declarative constraints (`gt`, `max_length`, `pattern`), not a manual
|
|
66
|
+
`if`-check inside the path operation body after the model has already
|
|
67
|
+
validated.
|
|
68
|
+
- Declare `response_model` (or a return-type annotation FastAPI can infer
|
|
69
|
+
one from) so the response is filtered to the declared fields even when
|
|
70
|
+
the object returned from the path operation carries more — this is a
|
|
71
|
+
correctness property, not just documentation: it is what keeps an
|
|
72
|
+
internal-only field out of the response body.
|
|
73
|
+
|
|
74
|
+
## Background work
|
|
75
|
+
|
|
76
|
+
- Use `BackgroundTasks` (injected the same way as any other dependency)
|
|
77
|
+
for work that must run after the response is sent but is short, best-
|
|
78
|
+
effort, and does not need retries, a result, or cross-process durability
|
|
79
|
+
(writing a log line, sending a best-effort notification).
|
|
80
|
+
- Reach for a real task queue (Celery, arq, a message broker) instead of
|
|
81
|
+
`BackgroundTasks` once the work needs retries, visibility into failure,
|
|
82
|
+
distribution across multiple worker processes, or survival past the
|
|
83
|
+
serving process' lifetime — `BackgroundTasks` runs in-process and is lost
|
|
84
|
+
if the process restarts before it completes.
|
|
85
|
+
|
|
86
|
+
## OpenAPI and schema
|
|
87
|
+
|
|
88
|
+
- Let FastAPI generate the OpenAPI schema from type hints, `response_model`,
|
|
89
|
+
and `Field(...)` constraints rather than hand-maintaining a separate
|
|
90
|
+
OpenAPI YAML/JSON file alongside the code — the generated schema is what
|
|
91
|
+
keeps documentation and implementation from drifting apart.
|
|
92
|
+
- Use `tags=[...]` on each `APIRouter`/path operation and a `summary`/
|
|
93
|
+
`description` (or a docstring, which FastAPI uses as the description)
|
|
94
|
+
for anything beyond an obvious CRUD endpoint, so the generated docs stay
|
|
95
|
+
usable as the API grows.
|
|
96
|
+
|
|
97
|
+
## Anti-patterns to flag, not introduce
|
|
98
|
+
|
|
99
|
+
- An `async def` path operation that calls a synchronous DB driver or
|
|
100
|
+
`requests.get` directly — this silently degrades every concurrent request
|
|
101
|
+
on that worker instead of raising an obvious error.
|
|
102
|
+
- Accepting the request body as `request: Request` and manually calling
|
|
103
|
+
`await request.json()` to bypass Pydantic validation — this discards
|
|
104
|
+
FastAPI's own validation, error responses, and OpenAPI schema generation
|
|
105
|
+
for that endpoint.
|
|
106
|
+
- Using `BackgroundTasks` for work that must not be lost (payment capture,
|
|
107
|
+
an email a user is relying on) instead of a real task queue with
|
|
108
|
+
retries.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.py"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# FastAPI security
|
|
9
|
+
|
|
10
|
+
Narrows `python`'s security guidance to where FastAPI's own attack surface
|
|
11
|
+
actually lives: path operations, dependencies, and request-body/response
|
|
12
|
+
models — the code an unauthenticated or under-authenticated client can
|
|
13
|
+
reach directly. In practice that means `routers/`/`api/` modules,
|
|
14
|
+
`dependencies.py`, and `schemas/`/`models/` request-body definitions, not
|
|
15
|
+
test files, where the same patterns (a hard-coded test JWT, a permissive
|
|
16
|
+
CORS config asserted against) are often deliberately exercised rather than
|
|
17
|
+
shipped.
|
|
18
|
+
|
|
19
|
+
## Input validation
|
|
20
|
+
|
|
21
|
+
- Type every request body, query, and path parameter through Pydantic (a
|
|
22
|
+
`BaseModel` for the body, typed function parameters for query/path
|
|
23
|
+
params) rather than reading raw JSON off `Request` — Pydantic validation
|
|
24
|
+
is FastAPI's primary injection defense: a field typed `int` cannot carry
|
|
25
|
+
a SQL-injection string or an oversized payload past validation the way an
|
|
26
|
+
untyped `dict`/`Any` parameter can.
|
|
27
|
+
- Use `Field(...)` constraints (`max_length`, `gt`/`le`, `pattern`) on any
|
|
28
|
+
field whose size or shape matters for downstream safety (a filename, a
|
|
29
|
+
free-text field written to a file path, a numeric field used in a
|
|
30
|
+
calculation) — validating shape at the model is cheaper and more
|
|
31
|
+
reliable than validating it again deeper in the call stack.
|
|
32
|
+
- Never accept a client-supplied value as a filesystem path, shell
|
|
33
|
+
argument, or SQL fragment without the same validation/parameterization
|
|
34
|
+
`python`'s `rules/security.mdc` already requires — FastAPI's own
|
|
35
|
+
validation does not substitute for parameterized SQL or `shell=True`
|
|
36
|
+
avoidance once the value leaves the Pydantic model.
|
|
37
|
+
|
|
38
|
+
## Authentication and authorization
|
|
39
|
+
|
|
40
|
+
- Put an auth check in a `Depends(...)` (or `Security(...)` for scoped
|
|
41
|
+
OAuth2) dependency shared across every path operation that needs it, not
|
|
42
|
+
duplicated inline in each path operation body — a dependency added once
|
|
43
|
+
and reused is much harder to accidentally omit from a new endpoint than
|
|
44
|
+
a copy-pasted inline check.
|
|
45
|
+
- Use `OAuth2PasswordBearer`/`OAuth2PasswordRequestForm` (or the project's
|
|
46
|
+
existing auth scheme) with a real JWT library (`pyjwt`) and a password
|
|
47
|
+
hashing library (`pwdlib`/`passlib`) for credential handling — never
|
|
48
|
+
compare a plaintext password directly or hash it with a fast general-
|
|
49
|
+
purpose hash (`md5`, `sha256` with no salt/work factor).
|
|
50
|
+
- Run the password-verify step even on a failed username lookup (compare
|
|
51
|
+
against a dummy hash) — skipping verification when the username is
|
|
52
|
+
unknown lets an attacker time the difference between "unknown user" and
|
|
53
|
+
"wrong password" and enumerate valid usernames.
|
|
54
|
+
- Never trust a client-supplied role/permission field on the request body
|
|
55
|
+
to grant elevated access — authorization decisions come from the
|
|
56
|
+
authenticated user record (via the auth dependency) or a trusted store,
|
|
57
|
+
never from data the same request also supplies.
|
|
58
|
+
|
|
59
|
+
## CORS
|
|
60
|
+
|
|
61
|
+
- Configure `CORSMiddleware`'s `allow_origins` with an explicit list of
|
|
62
|
+
trusted origins for any API that accepts cookies/credentialed requests;
|
|
63
|
+
never combine `allow_origins=["*"]` with `allow_credentials=True` —
|
|
64
|
+
browsers reject that combination, and if a proxy or older client allowed
|
|
65
|
+
it anyway it would let any origin make authenticated requests on the
|
|
66
|
+
user's behalf.
|
|
67
|
+
- Scope `allow_methods`/`allow_headers` to what the API actually needs
|
|
68
|
+
rather than defaulting every project to `["*"]`/`["*"]` out of habit.
|
|
69
|
+
|
|
70
|
+
## Secrets and configuration
|
|
71
|
+
|
|
72
|
+
- Read secrets (`SECRET_KEY`, database URLs, third-party API keys) through
|
|
73
|
+
`pydantic-settings`' `BaseSettings` (or the project's existing settings
|
|
74
|
+
module) backed by environment variables, never hard-coded in source —
|
|
75
|
+
including in an example/tutorial-style constant left over from scaffolding.
|
|
76
|
+
- Generate a real `SECRET_KEY` per environment (e.g. `openssl rand -hex
|
|
77
|
+
32`) and never reuse a tutorial/example key found in documentation or a
|
|
78
|
+
starter template in anything beyond local experimentation.
|
|
79
|
+
|
|
80
|
+
## Data access
|
|
81
|
+
|
|
82
|
+
- Build SQL through the ORM's query builder or parameterized raw queries,
|
|
83
|
+
never by interpolating a Pydantic model's field into a SQL string — the
|
|
84
|
+
same rule `python`'s `rules/security.mdc` states for any Python code,
|
|
85
|
+
restated here because a validated Pydantic field is still just a string
|
|
86
|
+
once it reaches a raw query.
|
|
87
|
+
|
|
88
|
+
## Red flags
|
|
89
|
+
|
|
90
|
+
- "I'll read the body with `await request.json()` here, the Pydantic model
|
|
91
|
+
felt like overkill for one field" — skips validation entirely for that
|
|
92
|
+
endpoint; use a `BaseModel`, even a one-field one.
|
|
93
|
+
- "`allow_origins=[\"*\"]` is fine, we don't use cookies" — true only until
|
|
94
|
+
the API adds a credentialed endpoint later; scope origins explicitly from
|
|
95
|
+
the start rather than relying on the current absence of cookies.
|
|
96
|
+
- "This SECRET_KEY from the FastAPI tutorial is just a placeholder, I'll
|
|
97
|
+
swap it before we ship" — a placeholder committed to source control is a
|
|
98
|
+
leaked secret the moment the repository is; generate the real one at
|
|
99
|
+
deploy time from the environment instead.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
extends: common
|
|
3
|
+
paths: ["**/*.py"]
|
|
4
|
+
metadata:
|
|
5
|
+
origin: authored
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# FastAPI testing
|
|
9
|
+
|
|
10
|
+
Narrows `python`'s `pytest` conventions to FastAPI's own test client and
|
|
11
|
+
dependency-override patterns. Applies only to FastAPI project files; see
|
|
12
|
+
`skills/fastapi-testing/SKILL.md` for the full test-authoring workflow this
|
|
13
|
+
rule file backs.
|
|
14
|
+
|
|
15
|
+
## Test client
|
|
16
|
+
|
|
17
|
+
- Test path operations through `fastapi.testclient.TestClient` (sync tests)
|
|
18
|
+
or `httpx.AsyncClient` with `ASGITransport` (async tests) against the
|
|
19
|
+
real `app` instance, rather than calling the path operation function
|
|
20
|
+
directly — calling the function directly skips FastAPI's own request
|
|
21
|
+
parsing, dependency resolution, and response-model filtering, which is
|
|
22
|
+
exactly what most bugs in a path operation live in.
|
|
23
|
+
- Build the `TestClient`/`AsyncClient` from a fixture shared across a test
|
|
24
|
+
module (or `conftest.py` once a second module needs it), not
|
|
25
|
+
re-instantiated per test, unless a test needs a differently configured
|
|
26
|
+
app instance.
|
|
27
|
+
|
|
28
|
+
## Dependency overrides
|
|
29
|
+
|
|
30
|
+
- Override a dependency for a test with `app.dependency_overrides[real_dep]
|
|
31
|
+
= fake_dep`, not by monkeypatching the dependency function's module
|
|
32
|
+
attribute — `dependency_overrides` is FastAPI's own supported mechanism
|
|
33
|
+
and is what keeps the override scoped to the app instance under test.
|
|
34
|
+
- Clear `app.dependency_overrides` after each test that sets one (a fixture
|
|
35
|
+
`yield`ing the override and clearing it in teardown, or `.clear()` in the
|
|
36
|
+
test itself) — an override left in place leaks into unrelated tests run
|
|
37
|
+
afterward in the same process.
|
|
38
|
+
- Use a dependency override for the current-user/auth dependency to test
|
|
39
|
+
both an authenticated and an unauthenticated path without standing up
|
|
40
|
+
real credential handling in every test.
|
|
41
|
+
|
|
42
|
+
## Request/response assertions
|
|
43
|
+
|
|
44
|
+
- Assert on the parsed response body (`response.json()`) and
|
|
45
|
+
`response.status_code`, not just that the call didn't raise — a path
|
|
46
|
+
operation that returns the wrong status code or a malformed body with no
|
|
47
|
+
exception is a real bug a bare "it didn't crash" assertion misses.
|
|
48
|
+
- For a validation-error case, assert the `422` status FastAPI returns for
|
|
49
|
+
a Pydantic validation failure, not a generic exception — a request that
|
|
50
|
+
fails Pydantic validation never reaches the path operation body at all.
|
|
51
|
+
- When `response_model` is declared, assert that a field the model
|
|
52
|
+
excludes (e.g. `hashed_password`) is actually absent from the response
|
|
53
|
+
body for at least one test — this is the behavior `response_model`
|
|
54
|
+
exists to guarantee, and a typo in the model's field list would
|
|
55
|
+
otherwise ship unnoticed.
|
|
56
|
+
|
|
57
|
+
## Database and external dependencies
|
|
58
|
+
|
|
59
|
+
- Use a real test database (a throwaway SQLite file, a test-scoped
|
|
60
|
+
Postgres schema, or the project's own configured test-DB fixture) behind
|
|
61
|
+
a `get_db`-style dependency override, not a hand-rolled mock of the ORM
|
|
62
|
+
session, for anything beyond the simplest unit test — an ORM session
|
|
63
|
+
mock only checks that the code called the mock the way the mock expects.
|
|
64
|
+
- Mock genuinely external services (a third-party HTTP API, an email
|
|
65
|
+
provider) at the client boundary (`httpx`/`requests` mock, or the
|
|
66
|
+
project's configured HTTP mocking library), matching `python`'s own
|
|
67
|
+
testing rule: mock external dependencies, not internal collaborators.
|
|
68
|
+
|
|
69
|
+
## Async tests
|
|
70
|
+
|
|
71
|
+
- Mark an async test `pytest.mark.asyncio` (or the project's configured
|
|
72
|
+
async plugin/mode) exactly as `python`'s testing rule requires; this
|
|
73
|
+
applies the same way to a test that awaits `AsyncClient` calls against a
|
|
74
|
+
FastAPI app.
|
|
75
|
+
|
|
76
|
+
## Coverage expectations
|
|
77
|
+
|
|
78
|
+
- Every new or touched path operation gets at least one test for its
|
|
79
|
+
success path, its validation-failure path (a `422` for bad input when
|
|
80
|
+
the body is a Pydantic model), and, when it requires auth, both an
|
|
81
|
+
authenticated and an unauthenticated case.
|
|
82
|
+
- A dependency with nontrivial logic (parses a header, decodes a token,
|
|
83
|
+
applies a business rule) gets its own test independent of any path
|
|
84
|
+
operation that happens to use it, in addition to the path operation's
|
|
85
|
+
own integration-level test.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fastapi-build-fix
|
|
3
|
+
description: "Use when a FastAPI app itself fails to start, import, or register a router -- app-startup failures, dependency-injection wiring errors (Depends() on the wrong callable, a missing sub-dependency), Pydantic v1/v2 model/validation errors, and a checker flagging a path-operation signature or response_model specifically, with the smallest root-cause fix. For a generic Python ModuleNotFoundError/packaging/dependency-resolver failure not specific to FastAPI's own app wiring, use python-build-fix."
|
|
4
|
+
triggers:
|
|
5
|
+
- "fix this FastAPI app startup error"
|
|
6
|
+
- "the FastAPI app fails to import after adding a new router"
|
|
7
|
+
- "this Depends() dependency is failing to resolve"
|
|
8
|
+
- "this FastAPI path operation's response_model is rejecting valid data"
|
|
9
|
+
- "the linter flags this FastAPI router file specifically"
|
|
10
|
+
- "this Pydantic model keeps raising an unexpected validation error"
|
|
11
|
+
- "fix this FastAPI router registration error causing a 404"
|
|
12
|
+
metadata:
|
|
13
|
+
origin: authored
|
|
14
|
+
category: build-fix
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
17
|
+
license: "MIT"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# FastAPI build-fix
|
|
21
|
+
|
|
22
|
+
Resolve a broken FastAPI app startup, dependency-injection wiring, type-
|
|
23
|
+
check, or lint failure with the smallest change that fixes the actual
|
|
24
|
+
cause. Scoped to FastAPI's own framework layer — for a generic Python
|
|
25
|
+
`ModuleNotFoundError`, packaging, or dependency-resolver failure not
|
|
26
|
+
specific to FastAPI's own app wiring, use `python-build-fix`; for adding a
|
|
27
|
+
feature use `fastapi-implementation`; for writing/fixing test content (not
|
|
28
|
+
a collection error) use `fastapi-testing`; for reviewing without fixing use
|
|
29
|
+
`fastapi-code-review`.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
### Step 1: Reproduce and classify the failure
|
|
34
|
+
|
|
35
|
+
Run the project's own configured commands (discover the run prefix — `uv
|
|
36
|
+
run`, `poetry run`, or none — from `pyproject.toml`):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ruff check .
|
|
40
|
+
ruff format --check .
|
|
41
|
+
mypy . # or: pyright
|
|
42
|
+
pytest -x -q
|
|
43
|
+
python -c "import <app_module>" # or: uvicorn <app_module>:app --port 0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Read the *first* error in each tool's output. Classify:
|
|
47
|
+
|
|
48
|
+
- **App startup/import** — `app = FastAPI(...)` module fails to import, or
|
|
49
|
+
`uvicorn` fails to start the app (distinct from a generic Python
|
|
50
|
+
`ModuleNotFoundError` unrelated to FastAPI wiring — that goes to
|
|
51
|
+
`python-build-fix`).
|
|
52
|
+
- **Router registration** — `app.include_router(...)` raises, a route
|
|
53
|
+
conflict (duplicate path + method), or a route silently 404s because it
|
|
54
|
+
was never registered.
|
|
55
|
+
- **Dependency-injection wiring** — `Depends(...)` resolves to the wrong
|
|
56
|
+
callable, a sub-dependency is missing, or FastAPI reports it cannot
|
|
57
|
+
determine how to resolve a dependency's own parameters.
|
|
58
|
+
- **Pydantic model errors** — a `BaseModel` raises at class-definition time
|
|
59
|
+
(bad field type, invalid `Field(...)` constraint) or at request-parse
|
|
60
|
+
time in a way that shouldn't be a validation error at all (a v1/v2
|
|
61
|
+
syntax mismatch is a common cause).
|
|
62
|
+
- **Type errors** — `mypy`/`pyright` reports a real mismatch on a path
|
|
63
|
+
operation's parameters, return type, or a `Depends(...)`-injected value.
|
|
64
|
+
- **Lint failures** — `ruff check` reports a rule violation.
|
|
65
|
+
|
|
66
|
+
### Step 2: Find the root cause
|
|
67
|
+
|
|
68
|
+
- **App startup/import**: does `app = FastAPI(...)` fail because a router
|
|
69
|
+
module it imports itself fails to import (circular import between
|
|
70
|
+
`main.py` and a router, or a router importing something not yet
|
|
71
|
+
defined)? Read the actual traceback's innermost frame, not just the
|
|
72
|
+
outermost "app failed to start" message.
|
|
73
|
+
- **Router registration**: check `app.include_router(router, prefix=...,
|
|
74
|
+
tags=[...])` — a duplicated `prefix` across two routers, or a path
|
|
75
|
+
operation decorated on the wrong router variable, are the common causes
|
|
76
|
+
of a route that 404s despite the code appearing correct.
|
|
77
|
+
- **Dependency-injection wiring**: read FastAPI's own error message about
|
|
78
|
+
which parameter it couldn't resolve; check whether the dependency
|
|
79
|
+
function's own parameters are all themselves either request data
|
|
80
|
+
(query/path/body) or other `Depends(...)` — a parameter with no default
|
|
81
|
+
and no `Depends(...)`/request-data annotation is what breaks resolution.
|
|
82
|
+
- **Pydantic model errors**: confirm the pinned Pydantic version
|
|
83
|
+
(`pyproject.toml`) and whether the failing syntax is v1-only (`class
|
|
84
|
+
Config:`, `@validator`) or v2-only (`model_config = ConfigDict(...)`,
|
|
85
|
+
`@field_validator`) — mixing the two is the most common cause of a
|
|
86
|
+
confusing model-definition error.
|
|
87
|
+
- **Type errors**: read the exact mismatch; a `Depends(...)`-injected
|
|
88
|
+
parameter's annotated type must match what the dependency function
|
|
89
|
+
actually returns — fix whichever side is actually wrong given the
|
|
90
|
+
dependency's real contract.
|
|
91
|
+
- **Lint failures**: apply `ruff check --fix .` for mechanical fixes; for a
|
|
92
|
+
substantive rule, fix the flagged code.
|
|
93
|
+
|
|
94
|
+
### Step 3: Apply the smallest fix
|
|
95
|
+
|
|
96
|
+
- Fix the actual cause identified in Step 2 — the wrong `Depends(...)`
|
|
97
|
+
target, the router registered on the wrong variable, the v1/v2 syntax
|
|
98
|
+
mismatch, the real type mismatch.
|
|
99
|
+
- Touch only what the failure requires; do not refactor unrelated
|
|
100
|
+
path operations or schemas while fixing a build failure.
|
|
101
|
+
|
|
102
|
+
### Step 4: Verify
|
|
103
|
+
|
|
104
|
+
Re-run every command from Step 1 in order; all must exit 0, including the
|
|
105
|
+
app-import/startup check — a fix that satisfies `mypy`/`ruff` can still
|
|
106
|
+
leave the app failing to start if a router or dependency wiring mistake
|
|
107
|
+
remains.
|
|
108
|
+
|
|
109
|
+
### Step 5: Report
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
Fixed: new `/webhooks/stripe` endpoint returns 404 in every environment
|
|
113
|
+
Root cause: routers/webhooks.py defines `router = APIRouter()` and the
|
|
114
|
+
route with `@router.post("/stripe")`, but main.py still calls
|
|
115
|
+
`app.include_router(legacy_router)` -- a leftover router object from
|
|
116
|
+
an earlier refactor that was never renamed -- so the new router
|
|
117
|
+
object is built but never mounted onto the app at all.
|
|
118
|
+
Fix: changed main.py to `app.include_router(webhooks_router,
|
|
119
|
+
prefix="/webhooks")`, pointing at the actual router object the new
|
|
120
|
+
route was added to, and removed the unused `legacy_router` import.
|
|
121
|
+
Verified: ruff check, mypy, pytest -x -q, and app import all green
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Rules
|
|
125
|
+
|
|
126
|
+
- NEVER add `# type: ignore` or `# noqa` as a blanket suppression to make a
|
|
127
|
+
real error disappear without fixing or explicitly justifying it inline.
|
|
128
|
+
- NEVER move a blocking call into an `async def` path operation (or leave
|
|
129
|
+
one there) to make an error disappear — if the fix requires the function
|
|
130
|
+
to stay blocking, keep it `def` so FastAPI's threadpool handles it.
|
|
131
|
+
- NEVER widen a Pydantic field's type to `Any`/`dict` to make a validation
|
|
132
|
+
error stop, when the actual cause is a v1/v2 syntax mismatch or a genuine
|
|
133
|
+
schema bug.
|
|
134
|
+
- Fix the root cause with the smallest change; do not refactor beyond what
|
|
135
|
+
the failure requires.
|
|
136
|
+
|
|
137
|
+
## Red Flags
|
|
138
|
+
|
|
139
|
+
| Rationalization | Why it is wrong |
|
|
140
|
+
|---|---|
|
|
141
|
+
| "This Pydantic validation error is annoying, I'll just type the field `Any`" | Hides the real schema mismatch (often a v1/v2 syntax mix) instead of fixing it; find and fix the actual cause |
|
|
142
|
+
| "The dependency resolution error is confusing, I'll just remove the `Depends(...)` and call the function directly" | Removes dependency injection entirely, losing `dependency_overrides` testability and any caching FastAPI provided; fix the actual parameter FastAPI couldn't resolve |
|
|
143
|
+
| "I'll add `# type: ignore` here, the real fix is bigger" | Hides the type hole permanently; if the real fix is out of scope, say so in the report and leave the error visible |
|
|
144
|
+
| "ruff flagged something in this router, I'll disable the rule in `pyproject.toml`" | Disables it project-wide for all future code, not just this failure; fix the flagged code instead |
|
|
145
|
+
|
|
146
|
+
## Verification
|
|
147
|
+
|
|
148
|
+
Do not report the fix done until all of the following hold:
|
|
149
|
+
|
|
150
|
+
- The originally failing command now exits 0.
|
|
151
|
+
- `ruff check .`, `ruff format --check .`, `mypy .`/`pyright`, `pytest -x
|
|
152
|
+
-q`, and the app import/startup check all exit 0.
|
|
153
|
+
- No new `# type: ignore`/`# noqa` was added without an inline reason, and
|
|
154
|
+
none was added as a blanket suppression.
|
|
155
|
+
- `git status` shows only the files whose actual cause was diagnosed in
|
|
156
|
+
Step 2 — no unrelated refactor.
|
|
157
|
+
- The report names the root cause, not just the symptom that was fixed.
|