---
name: audit-dev-data
description: Audit code generated by the Data phase against the PRD slice — migrations, seed providers, navigation/permissions coverage, FK constraints, drift detection
group: D
phase: devData
kind: audit
audit_only: true
section_label: 'AUDIT-DEV-DATA (rules to apply against generated migrations + seed providers vs the PRD Data slice)'
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# audit-dev-data — Data Phase Code-vs-PRD Audit

## Context

You are auditing the output of the Data phase of `ba-develop`.
Compare the PRD slice (spec) against the generated EF Core migrations and
seed providers (reality), and emit findings for every drift.

You receive in the system prompt:

- `--- PRD SLICE: DATA ---` — Markdown listing migrations to create + seed
  data the user wants for this application.
- `--- PROJECT INVENTORY (data) ---` — deterministic scan of:
  - `migrations[]` (file, name, timestamp, tablesCreated, tablesDropped, action),
  - `seedProviders[]` (file, className, entitiesReferenced, tablesReferenced,
    seedsNavigation, seedsPermissions),
  - the entities visible from the Domain layer (already generated upstream).
- `--- 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 (UI green checks). Use `err` only when the gap is unambiguous (an
entity from the slice has no migration covering it; a seed provider that
should bring navigation declares none); use `warn` for conventions and
nice-to-haves.

## Rules

### DEV-DAT-001 — Every PRD entity has at least one migration creating its table
- **Severity**: err (if any entity uncovered), ok (if all covered)
- Check: for each entity in the Domain inventory (passed alongside the
  Data slice), scan `inventory.data.migrations[*].tablesCreated`. There
  must be at least one migration whose `tablesCreated` contains a table
  whose name matches the entity (case-insensitive, stripping table prefix
  like `crm_`, `inv_`, `auth_`).
- Skip entities whose classification is `component` — they are owned by
  a parent entity and don't get their own table.
- **ok**: label=`DEV_DAT_001_ok`, params=`{ count: <number> }`
- **err**: label=`DEV_DAT_001_err`, params=`{ entities: "<comma-separated entity names without table>" }`
- **fixSkill**: `backend-data-layer`, **fixPhaseKey**: `data`
- **solution** (mandatory on err): "Re-run the Data phase. The subagent
  must call `scaffold-migration` once per missing entity (the migration
  generator infers `tablesCreated` from the EF Core ModelSnapshot)."

### DEV-DAT-002 — Migration table names follow the `<prefix>_<EntityPlural>` convention
- **Severity**: warn (if any violation), ok (if all comply)
- Check: every entry in `inventory.data.migrations[*].tablesCreated`
  must match `^[a-z]{2,5}_[A-Z][A-Za-z0-9]*$` — a 2-5 letter lowercase
  prefix + underscore + PascalCase entity-plural form. Use `Grep` on
  the migration `.cs` if a name looks suspicious.
- **ok**: label=`DEV_DAT_002_ok`
- **warn**: label=`DEV_DAT_002_warn`, params=`{ tables: "<comma-separated bad table names>" }`
- **solution** (mandatory on warn): "Update the EF Core configuration of
  each entity (`b.ToTable(\"<prefix>_<EntityPlural>\")`) and regenerate
  the migration. Untyped table names will collide once another module
  introduces an entity with the same simple name."

### DEV-DAT-003 — No duplicate migrations for the same table
- **Severity**: err (if any table appears twice in `tablesCreated`), ok (otherwise)
- Check: build a set of `tablesCreated` across all migrations; flag any
  table that appears in `tablesCreated` more than once. A duplicate
  create migration breaks `dotnet ef database update` — the second
  attempt errors on `table already exists`.
- **ok**: label=`DEV_DAT_003_ok`
- **err**: label=`DEV_DAT_003_err`, params=`{ tables: "<comma-separated duplicate tables>" }`
- **solution** (mandatory on err): "Delete one of the duplicate migrations
  AND its `.Designer.cs` companion. Squash via `efcore squash` if you
  want to consolidate without losing history."

### DEV-DAT-004 — At least one seed provider exists for the module
- **Severity**: err (if zero providers AND PRD declares any seed data), ok otherwise
- Check: if the Data slice mentions seed entries (lookup rows, navigation
  entries, permissions) AND `inventory.data.seedProviders` is empty,
  emit err. Skip when the slice has zero `seed` mentions.
- **ok**: label=`DEV_DAT_004_ok`
- **err**: label=`DEV_DAT_004_err`, params=`{ moduleCode: "<from PRD>" }`
- **fixSkill**: `backend-seed-data`, **fixPhaseKey**: `data`
- **solution** (mandatory on err): "Run `scaffold-seed --module <moduleCode>`
  in the Data phase. The generated provider implements `ISeedDataProvider`
  and is auto-discovered via DI registration."

### DEV-DAT-005 — Module seed provider declares navigation if PRD adds menu entries
- **Severity**: err (if PRD has new menu entries AND no provider sets `seedsNavigation`), ok otherwise
- Check: if the PRD slice introduces an application/module/section
  (the menu or seed sections of the slice mention `NavigationApplication`,
  `NavigationModule`, or `NavigationSection`), at least one entry in
  `inventory.data.seedProviders[*].seedsNavigation` must be `true`.
  Otherwise the new module never appears in the Studio menu at runtime.
  This rule mirrors the existing seed-coverage gate (`verifySeedCoverage`).
- **ok**: label=`DEV_DAT_005_ok`
- **err**: label=`DEV_DAT_005_err`, params=`{ providers: "<comma-separated providers without nav>" }`
- **solution** (mandatory on err): "Edit the main provider's `SeedAsync`
  method to call `NavigationApplication.Create(...)` /
  `NavigationModule.Create(...)` / `NavigationSection.Create(...)` for
  the new menu entries. A `ref-data only` provider that seeds lookup rows
  but skips navigation leaves the module orphaned."

### DEV-DAT-006 — Module seed provider declares permissions if PRD adds RBAC entries
- **Severity**: err (if PRD permissions exist AND no provider sets `seedsPermissions`), ok otherwise
- Check: same shape as DEV-DAT-005 but on `seedsPermissions`. If the PRD
  slice declares permission codes for the module, at least one
  `inventory.data.seedProviders[*].seedsPermissions` must be `true`.
- **ok**: label=`DEV_DAT_006_ok`
- **err**: label=`DEV_DAT_006_err`, params=`{ providers: "<comma-separated providers without perms>" }`
- **solution** (mandatory on err): "Add `Permission.CreateForModule(...)`
  + `Role.Create(...)` calls in the provider's `SeedAsync`. Without these,
  no role can ever access the module's actions and the API gates with
  `403 Forbidden` for every authenticated user."

### DEV-DAT-007 — Migration filenames follow `<timestamp>_<Verb><Subject>.cs`
- **Severity**: warn (if any violation), ok (if all comply)
- Pattern: `/^\d{14}_[A-Z][A-Za-z0-9]+\.cs$/` — 14-digit timestamp +
  underscore + PascalCase verbsubject (e.g. `20260430120000_AddBudget.cs`).
- Check `inventory.data.migrations[*].file` (basename only).
- **ok**: label=`DEV_DAT_007_ok`
- **warn**: label=`DEV_DAT_007_warn`, params=`{ files: "<comma-separated bad filenames>" }`
- **solution** (mandatory on warn): "Rename via `efcore rename` so the
  EF Core toolchain keeps a coherent history. Manual renames must update
  the `Designer.cs` and the `[Migration(\"...\")]` attribute too."

### DEV-DAT-008 — Every declared relationship is realized as a real FK constraint with the declared cascade (ANY scope)
- **Severity**: err (if any declared relationship has no FK constraint OR a wrong cascade), ok (if all match or no relationships)
- Catches the gap where a PRD relationship produced only a bare `Guid` column —
  no `HasForeignKey` / `OnDelete` — so referential integrity was silently absent.
  **A cross-table reference is ALWAYS a FK, regardless of module** — the historical
  "same-module ONLY" scope was WRONG (it left cross-module / Core / Tenant ids as
  bare `Guid`s). `scaffold-entity` emits the constraint from `relations[]` for
  EVERY scope. Since v3.55 the realization is:
    - `same-module` → navigation property + `HasOne(e => e.X).WithMany().HasForeignKey(...)`.
    - `core` whitelist (User, Role, Tenant, Company, Department, JobTitle,
      Office, Language, Group) → navigation property + `HasOne(e => e.X)...`,
      resolved against the base class `SmartStackExtensionDbContext`.
    - `cross-module` → typed `HasOne<Target>().WithMany().HasForeignKey(...)`,
      no navigation.
    - Legacy code may still carry a `*Reference` principal stub (pre-v3.55) for
      `core` refs and Tenant — accept both shapes as valid below.
  This rule guards that the constraint reached the EF configuration **and** the
  migration — for every scope.
- Check, for every (source, target, scope, cascade) declared in this module's PRD
  relationships:
  1. **Constraint exists** — `Read` the entity's EF configuration AND `Grep` the
     migration `.cs`: there MUST be either a `HasOne(e => e.X).WithMany().HasForeignKey(...)`
     (modern same-module / core-whitelist) OR a `HasOne<...>().WithMany().HasForeignKey(...)`
     (cross-module / legacy stub) for the relationship AND a corresponding
     `AddForeignKey` / `OnDelete(...)` in the migration. For a `core` ref the
     migration FK targets `principalSchema: "core"` (e.g. `tenant_Tenants`,
     `auth_Users`, `ref_Companies`). Absent → **err** (FK not realized — bare `Guid`).
  2. **Cascade matches** — the `DeleteBehavior` must equal the PRD cascade
     (`Cascade` ↔ cascade, `Restrict` ↔ restrict, `SetNull` ↔ set-null,
     `NoAction` ↔ no-action). Mismatch → **err**.
- The per-entity `TenantId` FK (from `tenantMode`) is in scope: a tenant entity
  MUST carry either `HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId)`
  (v3.55+ via the base class) OR the legacy `HasOne<TenantReference>().WithMany().HasForeignKey(e => e.TenantId)`.
- Identity/audit allowlist columns (`CreatedByUserId`, … — `lib/fk-allowlist.ts`)
  are plain `Guid`s by design and are NOT relationships — out of scope here.
- Skip when the PRD slice declares no relationships and the entity has no tenant.
- **ok**: label=`DEV_DAT_008_ok`
- **err (missing)**: label=`DEV_DAT_008_missing`, params=`{ refs: "<comma-separated source→target>" }`
- **err (mismatch)**: label=`DEV_DAT_008_mismatch`, params=`{ refs: "<comma-separated source.fk:expected:got>" }`
- **solution** (mandatory on err): "Pass the relationship to `scaffold-entity` as a
  `relations[]` entry (`type`, `targetEntity`, `foreignKey`, `onDelete`,
  `targetScope`, and for `core` also `targetTable`/`targetSchema`) so it emits the
  `HasOne(...).HasForeignKey(...).OnDelete(DeleteBehavior.<X>)` + (for core) the
  `*Reference` stub, then regenerate the migration. A reference that exists only as
  a bare `Guid` has NO referential integrity; a wrong cascade silently corrupts
  data at delete time."

## Output

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

```json
{
  "auditReport": {
    "scope": "devData",
    "applicationCode": "<from PRD>",
    "moduleCode": "<from PRD>",
    "findings": [
      {
        "dimension": "devData",
        "code": "DEV-DAT-005",
        "severity": "err",
        "label": "DEV_DAT_005_err",
        "params": { "providers": "CrmSeedDataProvider" },
        "solution": "Add NavigationApplication.Create / NavigationModule.Create / NavigationSection.Create in SeedAsync.",
        "fixSkill": "backend-seed-data",
        "fixPhaseKey": "data"
      }
    ]
  }
}
```

Stop immediately after the JSON block. Do not narrate.
