---
name: audit-dev-data
description: >
  Audit the generated persistence layer against the BA data model — deterministic CLI
  (DEV-DAT-001/002/003/007/008/009/010 — every entity has its migration, table naming, no duplicate
  CreateTable, migration filenames, every declared relationship a REAL FK constraint in the
  Configuration AND the migration with the declared cascade, every declared **Index** —
  non-unique included — present in a migration, the business test dataset `jeu-de-test.md`
  reached its guarded `{Module}TestDataSeedDataProvider`). After-Phase-1 (Entities) gate of /ba-develop.
  DEV-DAT-004/005/006 stay conversational (twins: DEV-CORE-011, DEV-UI-046)
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.

## Deterministic engine — how this audit runs

The mechanical rules below run through the colocated CLI, never by reading the
sources by hand:

```bash
npx --prefer-offline tsx skills/development/audit-dev-data/cli/audit-dev-data/index.ts \
  --project-path "<dotnet-root>" \
  --module-path  "<absolute path to .smartstack/ba/{appCode}/{moduleCode}>" \
  --module-code  "{moduleCode}" \
  --app-code     "{appCode}" \
  --mode audit
```

It reads the BA data model through `lib/ba-entities` (the parser audit-ba
runs) and the persistence layer as EF Core writes it — `CreateTable`,
`table.ForeignKey` / `AddForeignKey`, `CreateIndex` in every non-Designer,
non-Snapshot migration, plus the `*Configuration.cs` sources. Exit 1 on any
`err`; `_audit/dev-data-<module>.md` written next to the backend.

Until this CLI existed the skill was prose only and **no /ba-develop gate
invoked it**: DEV-DAT-008 — the one rule that proves a declared relationship
reached the database as a constraint — never ran. The FK axis had nothing
between DM-013 (BA time) and `smartstack-entity-audit` (post-deploy, live DB).

Implemented: DEV-DAT-001, 002, 003, 007, 008, 009. **DEV-DAT-004/005/006**
(seed providers ↔ PRD navigation/RBAC) stay conversational — their
deterministic twins are DEV-CORE-011 (`derive-rbac-grants --mode check`) and
DEV-UI-046 (reachable componentKeys against the seeded nav).

## 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**: **err** (if any table's prefix ∈ {`ext`,`extensions`,`core`}),
  warn (other shape 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. **Additionally, the prefix
  (the part before `_`) MUST NOT be `ext`, `extensions` or `core`** — those are
  the migration/schema reserved words, not a business-domain prefix (`ext_X`
  passes the shape regex but is the `[extensions].[ext_X]` bug). A reserved
  prefix is an **err**, not a warn.
- **ok**: label=`DEV_DAT_002_ok`
- **warn**: label=`DEV_DAT_002_warn`, params=`{ tables: "<comma-separated bad table names>" }`
- **err**: label=`DEV_DAT_002_err`, params=`{ tables: "<comma-separated reserved-prefix 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."
- **solution** (mandatory on err): "Re-scaffold the entity with the real
  `domainPrefix` from `entité.md` (`**Préfixe table**`, e.g. `ref_` → `ref`):
  `scaffold-entity/validate.ts` now rejects `ext`/`extensions`/`core` at source.
  Regenerate the entity + migration so the `ext_*` table becomes `{domain}_*`."

### 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, TenantOrganisation, 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`, `tenant_TenantOrganisations`). 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."

### DEV-DAT-009 — Every declared **Index** reached a migration (non-unique included)
- **Severity**: err (if any declared index has no matching `CreateIndex`), ok otherwise
- The EMISSION half of the index contract at MIGRATION grain. `DEV-API-031`
  audits the unique ones in the Configuration only and excludes non-unique
  declarations by design (« EF auto-indexes FK columns; perf indexes are
  advisory »); `DM-023` audits the DECLARATION at BA time. Nothing proved that
  a declared `(Status, CreatedAt)` — the composite the BA authored for a
  frequent filter — ever reached the schema.
- Check, for every `**Index**` entry of every entity of the module whose table
  a migration creates: a `CreateIndex` on that table whose column SET equals
  the declared set (the tenant-composite variant — declared set ∪ `TenantId` —
  counts, since the scaffolder prefixes uniques with the discriminator); a
  declared `unique` must match a `unique: true` index.
- Entities without a created table are DEV-DAT-001's — skipped here.
- **ok**: label=`DEV_DAT_009_ok`, params=`{ count }`
- **err**: label=`DEV_DAT_009_err`, params=`{ indexes: "<Entity (raw), …>" }`
- **fixSkill**: `backend-data-layer`, **fixPhaseKey**: `entities`
- **solution**: "Pass the index to `scaffold-entity` `indexes[]` (or
  `relations[].unique` for an FK-bearing unique) and regenerate the migration."

### DEV-DAT-010 — The business test dataset reached its guarded provider
- **Severity**: err (any leg below), ok otherwise — including no
  `jeu-de-test.md` (the dataset is optional: `params.note` says so).
- The second seed tier's EMISSION half. `jeu-de-test.md` (BA, optional —
  `/ba-create-test-data`) is derived by `derive-test-data --mode derive` and
  passed to `scaffold-seed` as `testData[]` in Phase 1, which emits
  `{Module}TestDataSeedDataProvider.cs`. Until this rule existed nothing
  proved the dataset reached the provider — a block added after the last
  Phase 1 run silently stayed BA prose, and a hand-edited provider could lose
  its guard and seed FICTITIOUS rows in production.
- Check, when the module carries a `jeu-de-test.md` with ≥ 1 block:
  - `{ModuleCode}TestDataSeedDataProvider.cs` exists under the backend
    (`DEV_DAT_010_missing`, params `{ entities, expected }`);
  - every `JT-` block has its `Seed{Entity}Async` (`_missing_entity`) and as
    many `{ // Entity « key »` row blocks as declared rows (`_mismatch`,
    params `{ entities: "Entity (N row(s) declared, M seeded)" }`);
  - the provider carries BOTH `IsDevelopment()` and
    `SmartStack:EnableDevSeeding` (`_unguarded` — an unguarded provider seeds
    fiction in production);
  - a `.cs` under `src/` registers `…TestDataSeedDataProvider>()` in DI
    (`_unregistered` — an unregistered provider is inert while the file looks
    complete).
- **ok**: label=`DEV_DAT_010_ok`, params=`{ count, rows }` — or
  `{ count: 0, note }` when no dataset is authored.
- **fixSkill**: `backend-seed-data`, **fixPhaseKey**: `entities`
- **solution**: "Run `derive-test-data --mode derive`, then `scaffold-seed`
  with its `testData[]` (phases-detail § « Jeu de test ») — never hand-edit
  the provider or its guard."
- Boundary: this provider is NEVER a feeding path for `audit-dev-api
  DEV-API-030` (it is excluded by file name) — a setup row belongs to
  `**Valeurs initiales**`.

## 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.
