---
name: audit-dev-api
description: Audit code generated by the API phase against the PRD slice — controllers, routes, HTTP actions, RBAC attributes, DTOs, integration tests
group: D
phase: devApi
kind: audit
audit_only: true
section_label: 'AUDIT-DEV-API (rules to apply against generated controllers vs the PRD API slice)'
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# audit-dev-api — API Phase Code-vs-PRD Audit

## Context

You are auditing the output of the API phase of `ba-develop`.
Compare the PRD slice (spec) against the generated controllers, DTOs,
validators and integration tests (reality), and emit findings for every
drift.

You receive in the system prompt:

- `--- PRD SLICE: API ---` — Markdown listing controllers, routes, HTTP
  actions and permissions the user wants for this application.
- `--- PROJECT INVENTORY (api) ---` — deterministic scan of:
  - `controllers[]` (file, className, route, httpActions),
  - the entities visible from the Domain layer.
- `--- FILES TOUCHED THIS PHASE ---` — files modified during the current run.
- `--- APPLICATION CODE / MODULE CODE ---` — active scope.

You may use `Read` / `Glob` / `Grep` to verify rules. **No `Edit` /
`Write` / `Bash`** — you are read-only.

Apply every rule across the module scope. Emit `ok` findings for passing
rules. Use `err` only when the gap is unambiguous (entity missing its
controller; HTTP action declared in spec but absent from the controller);
use `warn` for conventions and missing tests.

## Rules

### DEV-API-001 — Every PRD entity has a `<Entity>Controller.cs`
- **Severity**: err (if any entity uncovered), ok (if all covered)
- Check: for each entity from the Domain inventory whose PRD declares an
  HTTP API surface, find an `inventory.api.controllers[*].className`
  matching `^<Entity>Controller$` (case-insensitive). Skip entities the
  PRD slice marks `internal: true` or `api: skip`.
- **ok**: label=`DEV_API_001_ok`, params=`{ count: <number> }`
- **err**: label=`DEV_API_001_err`, params=`{ entities: "<comma-separated entity names without controller>" }`
- **fixSkill**: `backend-controller`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Re-run the API phase. The subagent
  must call `scaffold-controller` for each missing entity. The generator
  emits the controller + DTOs + FluentValidation in one shot."

### DEV-API-002 — Each controller declares the HTTP actions listed in the PRD
- **Severity**: err (if any required action missing), ok (if all present)
- Check: for each controller in `inventory.api.controllers[*]`, the
  PRD slice declares the expected `httpActions` (typically `GET list`,
  `GET detail`, `POST create`, `PUT update`, `DELETE remove`). Cross
  the spec list against `controllers[*].httpActions`. Missing actions
  block the frontend from completing CRUD.
- **ok**: label=`DEV_API_002_ok`
- **err**: label=`DEV_API_002_err`, params=`{ actions: "<comma-separated controller.action pairs>" }`
- **solution** (mandatory on err): "Re-run the API phase. The subagent
  must add the missing `[HttpGet]` / `[HttpPost]` / `[HttpPut]` /
  `[HttpDelete]` methods. The DTO + validator scaffolders cover Create
  / Update — Delete only needs a route."

### DEV-API-003 — Controller route follows `/<applicationCode>/<moduleCode>/<entity-kebab-plural>`
- **Severity**: warn (if any violation), ok (if all comply)
- Check: each `inventory.api.controllers[*].route` must match the pattern
  `/<applicationCode>/<moduleCode>/<entity-kebab-plural>` where the
  application + module codes come from the PRD scope and the entity
  segment is the kebab-cased plural form of the controller's entity.
- **ok**: label=`DEV_API_003_ok`
- **warn**: label=`DEV_API_003_warn`, params=`{ controllers: "<comma-separated controller:expected:got>" }`
- **solution** (mandatory on warn): "Update the controller's `[Route(...)]`
  attribute. The DB-driven frontend router builds page links from this
  exact pattern; deviations break navigation silently at runtime."

### DEV-API-004 — Write actions carry `[RequirePermission(...)]`
- **Severity**: warn (if any unguarded write action), ok (if all guarded)
- Check: `Read` each controller and `Grep` for `[HttpPost]`, `[HttpPut]`,
  `[HttpDelete]` declarations. Each must have a `[RequirePermission(...)]`
  attribute on the SAME method (within ~3 lines above the method
  signature). Read actions (`[HttpGet]`) are warnings only when the PRD
  marks the entity sensitive (e.g. `audit: required`).
- **ok**: label=`DEV_API_004_ok`
- **warn**: label=`DEV_API_004_warn`, params=`{ actions: "<comma-separated controller.action pairs missing perm>" }`
- **solution** (mandatory on warn): "Add `[RequirePermission(\"<app>.<module>.<section>.<action>\")]`
  on each listed method. The format must match the seed provider's
  `Permission.CreateForModule` calls — otherwise the gate denies
  everyone."

### DEV-API-005 — Each controller has matching DTOs (Response / Create / Update)
- **Severity**: warn (if any missing), ok (if all triplets present)
- Check: for each controller, `Glob` the project for
  `<Entity>ResponseDto.cs`, `<Entity>CreateDto.cs`, `<Entity>UpdateDto.cs`
  (the latter two only required when the controller exposes POST/PUT).
  Use `Grep` to confirm the file declares the type, not just imports it.
- **ok**: label=`DEV_API_005_ok`
- **warn**: label=`DEV_API_005_warn`, params=`{ entities: "<comma-separated entity:missing-DTO list>" }`
- **solution** (mandatory on warn): "Re-run the DTO scaffolder for the
  listed entities (`scaffold-dto --entity <Entity>`). The generator emits
  the missing files alongside FluentValidation validators."

### DEV-API-006 — Each entity with Create/Update has a `<Entity>Validator.cs`
- **Severity**: warn (if any missing), ok (if all comply)
- Check: for each controller exposing POST or PUT, `Glob` for
  `<Entity>Validator.cs` (FluentValidation validator). Skip controllers
  that only expose `[HttpGet]` or `[HttpDelete]` — they don't accept a
  body.
- **ok**: label=`DEV_API_006_ok`
- **warn**: label=`DEV_API_006_warn`, params=`{ entities: "<comma-separated entity names>" }`
- **solution** (mandatory on warn): "Run `scaffold-business --entity <Entity>`
  which emits the validator alongside the service / MediatR handler.
  Without the validator, MVC binds the DTO without enforcing the PRD's
  business rules at the API boundary."

### DEV-API-007 — Each controller has at least one `<Entity>ControllerTests.cs`
- **Severity**: warn (if any controller untested), ok (if all covered)
- Check: for each controller in `inventory.api.controllers[*]`, `Glob`
  for `<Entity>ControllerTests.cs` under `tests/` or any `*.Tests/`
  project. The xUnit class must reference the controller (`Grep` for
  `using <namespace>` or `<Entity>Controller`).
- **ok**: label=`DEV_API_007_ok`
- **warn**: label=`DEV_API_007_warn`, params=`{ controllers: "<comma-separated controllers without tests>" }`
- **solution** (mandatory on warn): "Run `scaffold-tests --layer api
  --entity <Entity>` to emit the integration test class. Untested
  controllers regress silently when the next phase touches the
  service layer."

### DEV-API-008 — Business rules are enforced, not stubbed
- **Severity**: err (any rule left as a TODO marker), ok (if none)
- Check: `Grep` the Application-layer validators
  (`*Application/**/Validators/*.cs`) and the Infrastructure services
  (`*Infrastructure/**/Services/**/*.cs`) for the literal `// TODO[BR-`.
  Any occurrence means a business rule from `règles-métier.md` was emitted
  by `scaffold-business` as a fallback comment and never translated.
  Additionally, for each `BR-…` id referenced in the API PRD slice, confirm
  a `RuleFor(...)` or a `// BR-…` trace exists in a validator or service.
- **ok**: label=`DEV_API_008_ok`, params=`{ count: <number of rules traced> }`
- **err**: label=`DEV_API_008_err`, params=`{ rules: "<comma-separated BR ids left as TODO or untraced>" }`
- **fixSkill**: `backend-business-layer`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Complete the post-scaffold
  business-logic pass (see `backend-business-layer` SKILL.md § Post-scaffold
  pass). Translate every `// TODO[BR-…]` into a `RuleFor(...)` in BOTH the
  Create and Update validators, or — for multi-entity / stateful / temporal
  rules — into a guard in the service method. No `// TODO[BR-` may survive
  the API gate."

### DEV-API-009 — Use-case actions are implemented, not `NotImplementedException`
- **Severity**: err (any stubbed action), ok (if none)
- Check: `Grep` the Infrastructure services
  (`*Infrastructure/**/Services/**/*.cs`) for `NotImplementedException`
  or `// TODO[UC-`. Either is a non-canonical custom action (anything beyond
  archive / restore / activate / deactivate / duplicate) that
  `scaffold-business` stubbed and the agent never implemented.
- **ok**: label=`DEV_API_009_ok`
- **err**: label=`DEV_API_009_err`, params=`{ actions: "<comma-separated service method / UC id stubbed>" }`
- **fixSkill**: `backend-business-layer`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Implement each stubbed service method
  from the linked use case's main flow (`backend-business-layer` SKILL.md
  § Post-scaffold pass): load the aggregate, apply the state change +
  invariants, persist, return the response DTO. Keep the `// UC-…` trace
  comment. No `NotImplementedException` may survive the API gate."

### DEV-API-010 — Pagespec `kind:api` actions land on the controller
- **Severity**: err (any pagespec action missing on the controller), ok (if all matched)
- Check: load every `<MODULE>/pagespecs/*.md` of the audited module and
  parse the fenced ```json blocks. For each `pagespec.actions[]` entry
  with `kind: "api"` AND `code ∉ STANDARD_CRUD_CODES` (the set declared
  in `lib/page-spec-actions.ts`), compute the expected controller route
  per `expectedControllerRoute(scope, endpoint)`:
  - `scope: "row"`    → `[Http<VERB>("{id:guid}/<endpoint>")]`
  - `scope: "bulk"`   → `[Http<VERB>("bulk/<endpoint>")]`
  - `scope: "header"` → `[Http<VERB>("<endpoint>")]`
  Then `Read` the entity's `<Entity>sController.cs` and `Grep` for
  the exact attribute (verb + route). De-duplicate pagespec entries by
  `(scope, endpoint, httpMethod)` before counting — the same action MAY
  appear on multiple views and is expected to land ONCE on the controller.
- **ok**: label=`DEV_API_010_ok`, params=`{ count: <matched> }`
- **err**: label=`DEV_API_010_err`, params=`{ actions: "<comma-separated entity:VERB:expectedRoute pairs missing>" }`
- **fixSkill**: `backend-controller`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Re-run Phase 2 of `/ba-develop` after
  refreshing the pagespecs. The orchestrator derives `customActions[]` from
  `pagespec.actions[]` and forwards them to `scaffold-controller`. If the
  pagespec endpoint differs from the legacy controller route, either:
  (a) update the pagespec `endpoint` to match the controller, or (b) let
  the regenerator rewrite the controller route to match the pagespec.
  Pick the source of truth via `/audit-dev-actions-alignment`."

### DEV-API-011 — No phantom controller endpoints outside the pagespec contract
- **Severity**: warn (any non-CRUD endpoint not in any pagespec), ok (if none)
- Check: reciprocal of DEV-API-010. For each `[HttpVerb("…")]` route
  attribute on `<Entity>sController.cs` whose path is NOT one of the
  canonical CRUD shapes (`""`, `"{id:guid}"`), confirm a matching
  `pagespec.actions[]` entry exists somewhere in this module's pagespecs.
  Catches legacy hand-coded endpoints that pre-date the per-page contract
  and silently drift from what the BA actually declared.
- **ok**: label=`DEV_API_011_ok`
- **warn**: label=`DEV_API_011_warn`, params=`{ endpoints: "<comma-separated entity:VERB:route pairs without pagespec backing>" }`
- **solution** (mandatory on warn): "Either declare the missing custom
  action in the corresponding `screen.md` (then re-run `/ba-create-prd`)
  or delete the orphan endpoint. Phantom endpoints are unreachable from
  the UI under the per-page contract — leaving them in place misleads
  future audits."

### DEV-API-012 — Every pagespec view has its endpoint on the screen controller
- **Severity**: err (any pagespec list/detail/form view missing an endpoint on its `{EntityPlural}ScreenController.cs`), ok otherwise
- **Transition behaviour**: when the project has NO screen controllers at all
  (i.e. Phase 2b of `/ba-develop` has not yet been executed for any entity),
  the rule emits `ok` with `params.note = "no screen-driven controllers
  detected — Phase 2b not yet executed"` instead of flooding the report. The
  per-entity gate only checks pagespecs whose entity already has at least
  one screen controller — partial migration is supported.
- Check: load every `<MODULE>/pagespecs/*.md` of the audited module. For
  each pagespec with `view ∈ {list, detail, form}` whose entity has a
  `{EntityPlural}ScreenController.cs` on disk, verify the expected
  endpoint(s) exist:
  - `list`   → `[HttpGet("list")]`
  - `detail` → `[HttpGet("detail/{id:guid}")]`
  - `form`   → BOTH `[HttpPost("form")]` AND `[HttpPut("form/{id:guid}")]`
  The class-level mount route is irrelevant — only the method-level path
  matters because the scaffolder always emits `[Route("api/screens/{plural}")]`.
- **ok**: label=`DEV_API_012_ok`, params=`{ count: <matched pagespecs> }`
- **err**: label=`DEV_API_012_err`, params=`{ screens: "<comma-separated SCR-…:VERB route>" }`
- **fixSkill**: `backend-screen-controller`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Re-run Phase 2b of `/ba-develop` for
  each (section, entity) missing an endpoint. The `scaffold-screen-controller`
  CLI must emit list/detail/form endpoints on the screen controller per
  `pagespec.view`."

### DEV-API-013 — No `// TODO[SCREEN-…]` markers in screen controllers
- **Severity**: err (any marker remains), ok (if none)
- Check: `Grep` every `.cs` file under any `Screens/` folder of the
  backend for `// TODO[SCREEN-<screenCode>]:`. The
  `scaffold-screen-controller` CLI emits these markers whenever it cannot
  prove the matching Business method exists or the DTO mapping is
  incomplete. The Phase 2b subagent is expected to clear every marker
  before staging its commit.
- **ok**: label=`DEV_API_013_ok`
- **err**: label=`DEV_API_013_err`, params=`{ count: <number>, files: "<comma-separated files>" }`
- **fixSkill**: `backend-screen-controller`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Each `// TODO[SCREEN-<screenCode>]:`
  marker means the screen-controller scaffolder left a placeholder.
  The Phase 2b subagent must add the missing method to `I{Entity}Service`
  (reusing Phase 2a's query primitives — never duplicate rules) and
  replace the marker with the real LINQ projection."

### DEV-API-014 — Same `I{Entity}Service` injected in both strata
- **Severity**: err (any entity injects different service interfaces across strata), ok otherwise
- Check: build the index `entity → { integration: injectedService, screens: injectedService }`
  from every `*Controller.cs` (integration) and `*ScreenController.cs`
  (screens). For every entity present in BOTH strata, the two
  `injectedService` values MUST be identical. A divergence
  (e.g. `ITypeAffaireService` vs `ITypeAffaireScreenService`) means a
  parallel service was created — Business rules can drift between strata.
  Entities present in only one stratum are skipped (legitimate during the
  transition).
- **ok**: label=`DEV_API_014_ok`, params=`{ entities: <count compared> }`
- **err**: label=`DEV_API_014_err`, params=`{ entities: "<E:integration=I…Service,screens=I…Service ...>" }`
- **fixSkill**: `backend-screen-controller`, **fixPhaseKey**: `api`
- **solution** (mandatory on err): "Both strata must inject the SAME
  `I{Entity}Service`. If the screen controller needs methods not on the
  integration interface, extend the EXISTING interface — do not create a
  parallel service. Business rules live in ONE Business layer."

## CLI mode — Phase 2 gate of `/ba-develop`

`audit-dev-api` ships a deterministic CLI sibling that implements **only**
the rules that need cross-source verification (DEV-API-010..014):

```
npx --prefer-offline tsx skills/development/audit-dev-api/cli/audit-dev-api/index.ts \
  --project-path "<dotnet-root>" \
  --module-path  "<absolute path to .smartstack/ba/<APP>/<MODULE>>" \
  --module-code  "<MODULE>" \
  --app-code     "<APP>" \
  --mode audit
```

The CLI loads every `pagespecs/*.md` of the module + every `*Controller.cs`
of the backend, then cross-references them:

| Rule | Severity | What it checks |
|---|---|---|
| DEV-API-010 | err | Every `pagespec.actions[]` `kind:api` ∧ `code ∉ STANDARD_CRUD` has a matching `[HttpVerb("<expected-route>")]` on the **integration** `<Entity>Controller.cs` |
| DEV-API-011 | warn | Reciprocal — no non-CRUD integration controller endpoint without a pagespec backing |
| DEV-API-012 | err | Every pagespec view ∈ {list, detail, form} has its expected endpoint on the **screen-driven** `{EntityPlural}ScreenController.cs`. Skipped when no screen controllers exist yet (Phase 2b not run). |
| DEV-API-013 | err | No `// TODO[SCREEN-…]` markers remain in any `Controllers/.../Screens/*.cs` |
| DEV-API-014 | err | When an entity has BOTH an integration and a screen controller, both inject the SAME `I{Entity}Service` (no parallel service forks) |

The report is written to `<backendPath>/_audit/dev-api-<module>.md` and
emitted on stdout as a JSON envelope. **The orchestrator (`/ba-develop` Phase 2 gate)
invokes this CLI automatically.** A non-zero `err` count blocks the phase with a
precise message (`<Entity>:<VERB>:<expectedRoute>` missing on controller).

Other DEV-API rules (001-009) remain conversational — Claude reads / Greps
via the rule definitions above. They do not need a deterministic CLI because
they verify structural conventions (one controller per entity, RBAC attribute
present, DTO triplet, …) that the scaffolders already guarantee by construction.

## Output

Emit EXACTLY ONE JSON code block matching the standard `auditReport`
envelope. Set `dimension` to `devApi` on every finding. Include one
`ok` finding per passing rule and one finding per failing rule.

```json
{
  "auditReport": {
    "scope": "devApi",
    "applicationCode": "<from PRD>",
    "moduleCode": "<from PRD>",
    "findings": [
      {
        "dimension": "devApi",
        "code": "DEV-API-002",
        "severity": "err",
        "label": "DEV_API_002_err",
        "params": { "actions": "BudgetController.Delete,BudgetController.Update" },
        "solution": "Re-run the API phase. Add the missing [HttpDelete] / [HttpPut] methods on BudgetController.",
        "fixSkill": "backend-controller",
        "fixPhaseKey": "api"
      }
    ]
  }
}
```

Stop immediately after the JSON block. Do not narrate.
