---
name: audit-dev-domain
description: Audit code generated by the Domain phase against the PRD slice — entity files, FK pairing, naming, traceability, drift detection
group: D
phase: devDomain
kind: audit
audit_only: true
section_label: 'AUDIT-DEV-DOMAIN (rules to apply against generated .cs entities vs the PRD Domain slice)'
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# audit-dev-domain — Domain Phase Code-vs-PRD Audit

## Context

You are auditing the output of the Domain phase of `ba-develop`.
Your job is to compare what the PRD slice says should exist (spec) against
what is actually present in the generated `.cs` files (reality), and emit
findings for every drift.

You receive in the system prompt:

- `--- PRD SLICE: DOMAIN ---` — Markdown listing entities, attributes, and
  relationships the user wants for this application.
- `--- PROJECT INVENTORY (domain) ---` — deterministic scan of `.cs` files
  with each entity's name, file path, base class and namespace.
- `--- FILES TOUCHED THIS PHASE ---` — files modified during the current run.
- `--- APPLICATION CODE / MODULE CODE ---` — the active scope of the audit.

You may also use `Read` / `Glob` / `Grep` on the project to verify rule
checks. **No `Edit` / `Write` / `Bash`** — you are read-only.

Apply every rule below across the whole module scope. Emit `ok` findings for
passing rules so the UI shows green checks. Emit `err` findings only when
the spec is unambiguous and the code is clearly wrong (missing entity,
broken FK target). Use `warn` for naming, conventions, and drift signals
that the user should review but won't block execution by themselves.

## Deterministic engine — how this audit runs

DEV-DOM-001, 002, 006 and 007 run through the colocated CLI, never by reading
the sources by hand. 001/002 were covered by the After-Phase-1 PROSE of
gates.md only (« every entity has a `.cs` ») — an LLM check, never a gate;
006/007 had no equivalent anywhere in the chain:

```bash
npx --prefer-offline tsx skills/development/audit-dev-domain/cli/audit-dev-domain/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 generated `Domain/**/Entities/**/*.cs` + `*Configuration.cs`.
Ghost detection is scoped to the folder that names the module
(`Entities/<App>/<Module>/`); a flat layout is reported as such
(`DEV_DOM_006_unscoped`), never as a storm of ghosts. 001/002 are **err**
(exit 1 — the gate blocks); 006/007 are warn. Findings land in the envelope and
`_audit/dev-domain-<module>.md`.

Until this CLI existed the skill was prose only and **no /ba-develop gate
invoked it**. DEV-DOM-003 (relation target `.cs`) is implied by 001 + DM-004;
005 (naming) is guaranteed by the scaffolder from DM-006/DM-007-checked names;
004 and 008 stay conversational.

## Rules

### DEV-DOM-001 — Every PRD entity has a generated `.cs` file
- **Severity**: err (if any entity from PRD missing in inventory), ok (if all present)
- Check: for each entity declared in the PRD slice, find an
  `inventory.domain.entities[*].name` matching case-insensitively. The
  comparison strips trailing/leading whitespace.
- **ok**: label=`DEV_DOM_001_ok`, params=`{ count: <number> }`
- **err**: label=`DEV_DOM_001_err`, params=`{ missing: "<comma-separated entity names>" }`
- **fixSkill**: `backend-data-layer`, **fixPhaseKey**: `domain`
- **solution** (mandatory on err): "Re-run the Domain phase. The subagent
  must call `scaffold-entity` for each missing entity (PascalCase class +
  EF Core configuration). Do not commit until all entities exist."

### DEV-DOM-002 — Every PRD attribute is declared in the matching `.cs` file
- **Severity**: err (if any attribute missing), ok (if all present)
- Check: for each (entity, attribute) pair in the PRD slice, `Read` the
  matching `.cs` file and verify a `public <Type> <Name>` declaration is
  present. The match is case-insensitive on the attribute name; the type
  may be the C# equivalent (`string` / `int` / `decimal` / `bool` /
  `DateTime` / `Guid`) or the EF nullable form (`<Type>?`).
- **ok**: label=`DEV_DOM_002_ok`
- **err**: label=`DEV_DOM_002_err`, params=`{ attrs: "<comma-separated entity.attr pairs>" }`
- **fixSkill**: `backend-data-layer`, **fixPhaseKey**: `domain`
- **solution** (mandatory on err): "Add the missing properties to each
  listed entity. Match the PRD type and respect EF Core nullability when
  the PRD marks the attribute optional."

### DEV-DOM-003 — Every relationship `targetEntityCode` resolves to an existing `.cs` file
- **Severity**: err (if any FK target missing), ok (if all valid)
- Check: for each relationship in the PRD slice, the target entity must
  appear in `inventory.domain.entities[*].name` (case-insensitive). A
  broken FK target means the migration phase will fail.
- **ok**: label=`DEV_DOM_003_ok`
- **err**: label=`DEV_DOM_003_err`, params=`{ refs: "<comma-separated source→target pairs>" }`
- **solution** (mandatory on err): "Either generate the missing target
  entity in this phase before continuing, or remove the relationship
  from the PRD slice if it was hallucinated upstream."

### DEV-DOM-004 — `baseClass` matches the PRD classification
- **Severity**: warn (if any mismatch), ok (if all match)
- Calque DM-015. Check `inventory.domain.entities[*].baseClass`:
  - PRD `classification: lookup` → baseClass should be `LookupEntity` or
    contain `Lookup` (e.g. `BaseLookupEntity`).
  - PRD `classification: component` → baseClass should be `Entity` (no
    soft-delete needed, owned-by-parent).
  - PRD `classification: full-module` (default) → baseClass should be
    `Entity` and the file should reference the audit/soft-delete fields.
- **ok**: label=`DEV_DOM_004_ok`
- **warn**: label=`DEV_DOM_004_warn`, params=`{ entities: "<entity:expected:got list>" }`
- **solution** (mandatory on warn): "For each entity, switch its base class
  so it matches the PRD classification. Lookup entities must inherit
  `LookupEntity` so the seed-data scaffolder picks them up."

### DEV-DOM-005 — Naming conventions (calque DM-006 + DM-007)
- **Severity**: warn (if any violation), ok (if all comply)
- Pattern (entities): `/^[A-Z][A-Za-z0-9]*$/` — PascalCase, no underscores.
- Pattern (attributes / properties): `/^[A-Z][A-Za-z0-9]*$/` for C# public
  properties (PascalCase mandatory in C#). The PRD camelCase form is
  expected to translate to PascalCase in the generated `.cs`.
- Check filenames + class declarations + property declarations against
  the patterns. Skip C# keywords and well-known framework members
  (`Equals`, `GetHashCode`, etc.).
- **ok**: label=`DEV_DOM_005_ok`
- **warn**: label=`DEV_DOM_005_warn`, params=`{ names: "<comma-separated bad names>" }`
- **solution** (mandatory on warn): "Rename the listed identifiers to
  PascalCase. The generated EF Core configuration relies on the property
  names matching the PRD attribute names exactly."

### DEV-DOM-006 — No "ghost" entities (drift detector)
- **Severity**: warn (if any ghost found), ok (if none)
- Reciprocal of DEV-DOM-001: every entity in
  `inventory.domain.entities[*].name` must appear in the PRD slice.
  Ghost entities indicate either a stale `.cs` from a previous run that
  the user has since removed from the PRD, or a hallucinated entity the
  subagent emitted without spec authority.
- **ok**: label=`DEV_DOM_006_ok`
- **warn**: label=`DEV_DOM_006_warn`, params=`{ entities: "<comma-separated ghost names>" }`
- **solution** (mandatory on warn): "Either re-add these entities to the
  PRD slice if they were removed by mistake, or delete their `.cs` file
  and the matching EF Core configuration. Leaving ghosts in the codebase
  triggers stale FK relationships in the next phase."

### DEV-DOM-007 — `*Id` Guid attributes are paired with a navigation property or `[ForeignKey]` (calque DM-013)
- **Severity**: warn (if any unpaired), ok (if all paired)
- Check: for each generated entity, `Read` the `.cs` and look for
  properties of the form `public Guid <Name>Id { get; set; }` (or
  `Guid?`). For each one:
  - PASS if the same class declares a `public <Target> <Name> { get; set; }`
    nav property (e.g. `BudgetId` ↔ `Budget`), OR
  - PASS if the property carries a `[ForeignKey(...)]` attribute, OR
  - PASS if a sibling EF configuration file (`<Entity>Configuration.cs`)
    declares `HasOne(...)/HasMany(...)` on that key — including the
    no-navigation `HasOne<...>().WithMany().HasForeignKey(...)` form used for
    cross-module / Core references, OR
  - PASS if the column is an identity/audit allowlist column
    (`CreatedByUserId`, … — `lib/fk-allowlist.ts`), intentionally not a FK.
  - Otherwise WARN with the entity.attr pair.
- Skip when the attribute is the primary key (`Id`).
- **ok**: label=`DEV_DOM_007_ok`
- **warn**: label=`DEV_DOM_007_warn`, params=`{ attrs: "<comma-separated entity.attr pairs>" }`
- **solution** (mandatory on warn): "Add a navigation property whose name
  matches the attribute without the trailing `Id`, OR decorate the key
  with `[ForeignKey(nameof(<Nav>))]`, OR declare the relationship in the
  EF configuration file. Without one of these, EF Core will not infer
  a constraint and the migration phase will produce a malformed schema."

### DEV-DOM-008 — Lookup entities expose `Label` and `IsActive` (calque DM-015 lookup contract)
- **Severity**: warn (if any lookup missing `Label` or `IsActive`), ok (if all comply)
- **A reference value does not carry a code** — its LABEL is its identity and
  its natural key. `Code` is therefore NOT part of the canonical set: whether a
  reference table carries one is a USER decision, written and dated in
  `entité.md` (`- **Code décidé**`, audit **DM-022**). Asking for it here would
  put back, on the code side, exactly what DM-015 stopped prescribing on the BA
  side — and would flag every correctly-reprised table as defective.
- Check: every inventory entity whose `baseClass` contains `Lookup` must
  declare two public properties named `Label`, `IsActive`.
  Use `Grep` on the `.cs` file with patterns `public string Label`,
  `public bool IsActive` (allow the nullable form). `SortOrder` is
  recommended, never required. A `public string Code` present on the class is
  NOT a finding here — DM-022 owns that judgment, from the BA where the decision
  is written; NEVER propose adding one.
- Skip when `inventory.domain.entities` has no lookup entity.
- **ok**: label=`DEV_DOM_008_ok`
- **warn**: label=`DEV_DOM_008_warn`, params=`{ entities: "<entity:missing list>" }`
- **solution** (mandatory on warn): "Add the missing properties on each listed
  lookup: the frontend renders `Label` and filters by `IsActive`, and the
  seed-data provider upserts on the natural key — the LABEL when no
  `**Code décidé**` authorises a code."

## Output

Emit EXACTLY ONE JSON code block matching the standard `auditReport`
envelope (same shape as `audit-data-model`). Set `dimension` to
`devDomain` on every finding. Include one `ok` finding per passing rule
(so the UI lights up green) and one finding per failing rule.

```json
{
  "auditReport": {
    "scope": "devDomain",
    "applicationCode": "<from PRD>",
    "moduleCode": "<from PRD>",
    "findings": [
      {
        "dimension": "devDomain",
        "code": "DEV-DOM-001",
        "severity": "err",
        "label": "DEV_DOM_001_err",
        "params": { "missing": "Budget,BudgetType" },
        "solution": "Re-run the Domain phase. The subagent must call scaffold-entity for each missing entity.",
        "fixSkill": "backend-data-layer",
        "fixPhaseKey": "domain"
      }
    ]
  }
}
```

Stop immediately after the JSON block. Do not narrate.
