# `/ba-develop` — Per-phase gates

> Loaded on demand when running a specific phase's gate. The SKILL.md just
> mentions "gates between phases enter the auto-healing loop on failure".

A phase passes **only if all these hold**. **When a gate fails, the phase
enters the auto-healing loop** (see `references/auto-healing.md`) — it does
NOT halt. A failure that cannot be healed is **deferred** (a blocker is
recorded, the safest best-effort action is taken) and the run continues. No
gate failure ever stops the orchestrator or asks the user mid-run — everything
that needs a human decision waits in `blockers[]` until the final report.

## Pre-entry coverage checks (Phases 0-2) — BEFORE skip decision

On re-runs, Phases 0-2 were historically skipped when artifacts exist and
gates pass. This missed PRD additions (new entities, new attributes, new
custom actions) that arrived between runs. The pre-entry coverage check
closes this gap by comparing the PRD/BA spec against what is actually on
disk BEFORE deciding to skip.

**The check runs BEFORE launching any subagent** — it uses fast,
deterministic tools (< 5 seconds total) so no agent context is wasted on
a phase that genuinely needs no work.

### Decision flow (applies to Phases 0, 1, 2a, 2b individually)

```
if artifacts_do_not_exist:
    → ENTER phase (first run)
elif pre_entry_coverage_check() has ERR findings:
    → RE-ENTER phase (PRD has items not yet implemented)
elif compile_and_test_gate_passes:
    → SKIP phase (everything implemented, everything green)
else:
    → RE-ENTER phase (existing code is broken)
```

### Phase 0 — Core Foundation Seed coverage

1. **Module navigation coverage** (mirrors DEV-CORE-002): Grep
   `CoreNavigationSeedDataProvider.cs` for every module code declared
   in the BA `index.md` tree. Any BA module WITHOUT a matching
   `NavigationModule.Create(... code: "{moduleCode}" ...)` line → ERR.
2. **Permission floor** (mirrors DEV-CORE-004): Read
   `CorePermissionsSeedDataProvider.cs`. A pre-floor 3-field tuple
   `(Path, Action, SectionCode)` → ERR (regenerate with the floor). On the
   4-field tuple, verify each BA node carries its floor rows —

   <!-- permission-floor:v1 — drift-tested against lib/permission-actions.ts (edit ALL carriers or the suite fails) -->
   | Grain | Node | Floor permissions |
   |---|---|---|
   | Application | `{app}` | `access` |
   | Module | `{app}.{module}` | `access` |
   | Section | `{app}.{module}.{section}` | `access` `lookup` `read` `create` `update` `delete` `execute` |
   | Resource | `{app}.{module}.{section}.{resource}` | `access` `lookup` `read` `create` `update` `delete` `execute` |
   <!-- /permission-floor:v1 -->

   Any missing floor path → ERR (the floor is derived by build-spec; a gap
   means a stale provider — re-enter Phase 0, never hand-add rows).
3. **Grant parity — DEV-CORE-011** (the rbac.md ⇄ seed bidirectional check;
   until 2026-08 this verdict had no executor — the pack documents the rule
   but nothing ran it):

   ```bash
   npx --prefer-offline tsx skills/business-analyse/create-rbac/cli/derive-rbac-grants/index.ts \
     --spec '{"baRoot":".smartstack/ba","app":"<APP>","module":"<MODULE>","mode":"check","projectPath":"<projectPath>"}'
   ```

   `missingGrants` (a specified right never seeded) or `extraGrants` (a seeded
   right nothing explains) non-empty → ERR; `unmatchedActors`/`needsResolution`
   → ERR (fix the BA docs, re-run — never type the row by hand).

If ANY check returns ERR → Phase 0 MUST re-enter.

### Phase 1 — Entities coverage

Run `project-inventory`:
```bash
npx --prefer-offline tsx skills/validation/project-inventory/cli/project-inventory/index.ts \
  --project-path "<projectPath>" --domains domain
```

1. **Entity existence** (mirrors DEV-DOM-001): for each entity declared
   in `entité.md`, check `report.domain.entities[].name`. Any BA entity
   NOT in the inventory → ERR.
2. **Attribute completeness** (mirrors DEV-DOM-002): for each entity
   that IS in the inventory, Read its `.cs` file and verify every
   attribute from `entité.md` has a matching `public <Type> <Name>`
   property. Any missing attribute → ERR.

If ANY check returns ERR → Phase 1 MUST re-enter.

### Phase 2a — API Integration coverage

1. **Controller existence** (DEV-API-024, deterministic): run the audit CLI
   with `--rules DEV-API-024` (same invocation shape as below). EVERY entity of
   `entité.md` must have a controller on some stratum, an explicit
   `**API** : none` marker, or an exempt junction/component classification —
   the old "that the PRD declares an API surface" wording was the escape hatch
   that shipped a migrated entity with no endpoint, silently. Any `err` → ERR.
2. **Custom action coverage** (DEV-API-010): run the audit CLI:
   ```bash
   npx --prefer-offline tsx skills/development/audit-dev-api/cli/audit-dev-api/index.ts \
     --project-path "<dotnet-root>" \
     --module-path  "<moduleDir>" \
     --module-code  "{moduleCode}" \
     --app-code     "{appCode}" \
     --mode audit \
     --rules DEV-API-010
   ```
   Parse JSON stdout. Any `findings[].severity === "err"` → ERR.
3. **Container registrations** (DEV-API-026): run the audit CLI with
   `--rules DEV-API-026` (same invocation shape as above). Any `err` → ERR:
   a controller-injected `I{X}Service` without its `AddScoped`, or client
   Handlers/Validators without the assembly scan — the leg the build and
   the unit tests never exercise (every request 500s at runtime). Heal =
   re-run `scaffold-business` for the entity (it lands the registration in
   the `BUSINESS-SERVICES-DI` / `CLIENT-APPLICATION-ASSEMBLY-DI` markers).

If ANY check returns ERR → Phase 2a MUST re-enter.

### Phase 2b — API Screen-driven coverage

Run the audit CLI:
```bash
npx --prefer-offline tsx skills/development/audit-dev-api/cli/audit-dev-api/index.ts \
  --project-path "<dotnet-root>" \
  --module-path  "<moduleDir>" \
  --module-code  "{moduleCode}" \
  --app-code     "{appCode}" \
  --mode audit \
  --rules DEV-API-012
```
Parse JSON stdout. Any `findings[].severity === "err"` on DEV-API-012
(pagespec view missing its endpoint on screen controller) → ERR.

If no screen controllers exist at all (Phase 2b never ran), the CLI
emits `ok` — this is NOT a coverage gap but a first-run signal.

If ANY check returns ERR → Phase 2b MUST re-enter.

### Interaction with `--force`

`--force` affects Phase 3 only. It does NOT bypass coverage checks for
Phases 0-2 because they auto-detect PRD changes. The two mechanisms
are orthogonal:

| Scenario | Phases 0-2 | Phase 3 |
|----------|------------|---------|
| PRD unchanged, no --force | SKIP | Re-enter (always) |
| PRD changed, no --force | RE-ENTER | Re-enter |
| PRD unchanged, --force | SKIP | Full regen |
| PRD changed, --force | RE-ENTER | Full regen |

### Performance budget

Each pre-entry check MUST complete in < 5 seconds:
- `project-inventory` runs in < 2 s (regex-based, no LLM).
- `Grep` on Core providers is instant (single file).
- `audit-dev-api` CLI runs in < 3 s (glob + regex, no LLM).

Total overhead for all 4 checks: < 15 seconds.

## After Phase 0 (Core Foundation Seed)
- `.NET build` succeeds (the 6 generated `Core/*SeedDataProvider.cs` files
  reference SmartStack NuGet types only — should compile in isolation).
- `verifyCoreFoundationSeed` PASSES every per-module check:
  - Each declared module has a `NavigationModule.Create(... code: "{moduleCode}" ...)`
    call in `CoreNavigationSeedDataProvider.cs`.
  - `CoreRolesSeedDataProvider.cs` contains ≥ 1 `Role.Create(... applicationId: {Pascal}ApplicationId ...)`
    for the application.
  - `CorePermissionsSeedDataProvider.cs` contains ≥ 2 permission tuples per
    declared module (access + read floor).
- `DependencyInjection.cs` (or sibling) carries the 6
  `services.AddScoped<IClientSeedDataProvider, Core*SeedDataProvider>()` lines
  between `<<< CORE-SEED-DI BEGIN/END >>>` markers.

> Phase 0 is deterministic. The agent does not write any `.cs` — `scaffold-core-seed`
> does. If the gate fails, root cause is upstream (stale CoreSeedSpec) — rebuild
> the spec and re-run. Hand-editing the Core providers is forbidden.

## After Phase 1 (Entities)
- `.NET build` succeeds. This `dotnet build` is the run's restore point; later
  phases build with `--no-restore`.
  > **`--no-build` invariant** — the `dotnet test` gates of Phases 1 / 2a / 2b run
  > with `--no-build --no-restore`: a green `dotnet build` ALWAYS precedes them in
  > the same gate pass, so they reuse its binaries. On an auto-heal retry the fix
  > re-scaffolds → the retry re-runs `dotnet build` (which picks up the fix) THEN
  > `dotnet test --no-build`. Never run `--no-build` without a preceding build — it
  > would test stale binaries. Phase 4 is the exception: it AUTHORS the AC test
  > bodies, so its `dotnet test` keeps the build (`--no-restore` only).
- Every entity in `structured.entities[]` has a `.cs` under `Domain/Entities/<App>/<Module>/`.
- Every entity has an EF Core Configuration registered via
  `ApplyConfigurationsFromAssembly`.
- EF migration created under the appropriate `Migrations.{Provider}` project —
  `dotnet ef migrations script` generates valid SQL.
  - **Apply step (sanctioned)** — once created, scan the migration's `Up()` for
    destructive ops FIRST (the apply policy has no destructive axis — this scan is
    the only destructive gate):
    - destructive → `migration.destructive` blocker (critical), **no apply**;
    - additive → apply through `npx --prefer-offline tsx
      skills/efcore/cli/apply/index.ts --spec '{"cwd":"<projectPath>"}'` (spec
      **cwd-only**, never `connectionString`; parse the JSON envelope, not the exit
      code). `success:true` → applied, note it in the phase summary; `blocked:true`
      (remote/unknown DB, the CLI's 🔴 fail-closed verdict) → `migration.not-applied`
      blocker (high) with the CLI's `reason`, continue — the app may still
      auto-migrate at boot.
  - **Deferred path** — when the project's standing rule reserves `dotnet ef
    migrations add` to the user (or the migration step is otherwise skipped), do
    NOT run a migration and do NOT fail the phase: the entities phase still passes
    on structure (entities + EF configurations present + build green). Instead emit
    the `migrationPlan` (status `deferred`, the `{domainPrefix}_{Plural}` table list,
    the sanctioned `/efcore create` command) AND a `migration.deferred` blocker
    (severity `high`). Phase 3e runtime smoke STILL runs — a routing 404 is
    independent of DB state, so it catches the route-parity class regardless; if the
    un-migrated DB makes data endpoints fail, those are subsumed by this same
    `migration.deferred` blocker. Smoke is never silently skipped. This is the
    edge-case fallback: normally the migration is created (sanctioned `scaffold-migration`
    / `/efcore create`) and is incontournable. See `output-contract.md`.
- **BLOCKING — `audit-dev-data --mode audit` returns 0 `err`** — the persistence
  gate: did the schema the BA declared reach the migrations?
  ```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
  ```
  DEV-DAT-001 (every entity has its `CreateTable`), 002/003/007 (naming,
  duplicates, filenames), **DEV-DAT-008** (every declared relationship is a REAL
  FK constraint in the Configuration AND in a migration, with the declared
  cascade — tenant FK included: a bare `Guid` has no referential integrity),
  **DEV-DAT-009** (every declared `**Index**` — non-unique included, which
  DEV-API-031 excludes by design — reached a `CreateIndex`), **DEV-DAT-010**
  (the business test dataset `jeu-de-test.md`, when authored, reached its
  `{Module}TestDataSeedDataProvider` — every block, every row, DI-registered and
  GUARDED by `IsDevelopment() || SmartStack:EnableDevSeeding`; no dataset = ok). Until this gate
  existed the skill was prose only and nothing invoked it — DEV-DAT-008 never
  ran. Skipped ONLY on the deferred-migration path above (no migration →
  nothing to audit; the `migration.deferred` blocker already says so).
- **`audit-dev-domain --mode audit`** (same arguments, `skills/development/audit-dev-domain/cli/audit-dev-domain/index.ts`)
  — BLOCKING on `err`: **DEV-DOM-001** (every BA entity has its Domain `.cs`) and
  **DEV-DOM-002** (every stored attribute of entité.md is a property of that `.cs` —
  a stale or hand-edited class drops the column for good); warn, carried into the
  phase summary: **DEV-DOM-006** (a generated `.cs` under `Entities/<App>/<Module>/` the BA
  model does not declare — a ghost keeps a table, FK targets and a DbSet alive
  that nobody owns) and **DEV-DOM-007** (every `*Id` Guid paired with a
  navigation / `[ForeignKey]` / `HasForeignKey` — a bare Guid gets NO
  constraint from EF Core).
- The business TEST DATASET provider (`{Module}TestDataSeedDataProvider.cs`)
  is CONDITIONAL too: when the module carries a `jeu-de-test.md`, Phase 1 runs
  `derive-test-data --mode derive` and passes `testData[]` (+ `testDataRank`) to
  the same `scaffold-seed` call (phases-detail § « Jeu de test ») — `DEV-DAT-010`
  (err) is the net: present, complete, guarded, registered. It is NEVER a feeding
  path for `DEV-API-030` (dev/test/qual only).
- Module-scoped reference-data providers under
  `Persistence/Seeding/Applications/{App}/Modules/{ModuleCode}/` are
  CONDITIONAL: for every entity whose `entité.md` carries a
  `**Valeurs initiales**` table, the
  `{Module}ReferenceDataSeedDataProvider.cs` MUST exist and be DI-registered
  (run `scaffold-seed` with `referenceData[]` — see phases-detail § "Valeurs
  initiales"). Absent declaration → absent provider is fine (nav/roles/perms
  are Phase 0's job). The downstream net is `audit-dev-api DEV-API-030` (err):
  an entity with no create endpoint, no seed provider and no declared feeding
  action is UNPOPULATABLE.
- `scaffold-tests --layer=domain` produced ≥ 1 `{Entity}Tests.cs` per entity
  and `dotnet test --no-build --no-restore --filter "Category=Domain&FullyQualifiedName~{ModuleCode}"` passes.

## After Phase 2a (API Integration)
- `.NET build` still succeeds (`dotnet build --no-restore` — Phase 1's restore holds).
- Every controller carries `[RequirePermission]` matching a path from
  `structured.permissions[]`.
- Integration tests for CRUD on each entity pass
  (`dotnet test --no-build --no-restore --filter "Category=Integration&FullyQualifiedName~{ModuleCode}"`).
- **BLOCKING — container gate**: the generated `DiResolutionTests` pass
  (`dotnet test --no-build --no-restore --filter "FullyQualifiedName~DiResolutionTests"`) —
  one theory per controller resolving its ctor from the REAL container. A red
  theory = an unregistered generated service (the "green build, every endpoint
  500" class) → heal as `di.service-unregistered` (re-run `scaffold-business`).
- **BLOCKING** — Business-rule tests pass
  (`dotnet test --no-build --no-restore --filter "Category=Business&FullyQualifiedName~{ModuleCode}"`) —
  the rule tests are tagged `[Trait("BR","BR-NNN")]`. Non-negotiable.
- **BLOCKING — No residual stubs**: the deterministic gate is
  **`audit-dev-api DEV-API-008/009`** (below) — a surviving `// TODO[BR-…]` /
  `// TODO[UC-…]` / `NotImplementedException` in any generated validator or
  service is a hard `err`. (Equivalent manual probe:
  `grep -rn "NotImplementedException\|TODO\[BR-\|TODO\[UC-"` across
  `src/{Ns}.Application` + `src/{Ns}.Infrastructure` returns nothing.)
- **BLOCKING — Business-rule test parity**: the deterministic gate is
  **`audit-dev-tests DEV-TEST-009`** — every enforceable `err` rule of
  `règles-métier.md` (lib/ba-rules-rows, shared exemptions: access/numbering/
  info/**Enforcement** opt-out) carries a `[Trait("BR","BR-NNN")]` test, and a
  trait citing a dead rule is flagged. (This line used to promise a count with
  NO implementation — vacuous green was the norm, not the exception.)
- **BLOCKING — Use-case action coverage**: every custom action linked to a
  `UC-…` has a service method with a real body + `// UC-…` trace comment.
- **BLOCKING — `audit-dev-api --mode audit` returns 0 `err`**:
  ```bash
  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/{appCode}/{moduleCode}>" \
    --module-code  "{moduleCode}" \
    --app-code     "{appCode}" \
    --mode audit
  ```
  The full-run (no `--rules`) executes the whole deterministic set. Key legs:
  DEV-API-010 (every pagespec `kind:api` non-CRUD has a matching `[HttpVerb]`
  on the integration controller) + DEV-API-011 (no phantom endpoint) +
  **DEV-API-008** (business rules enforced/traced — no surviving `// TODO[BR-…]`,
  every `linkedBusinessRules` id traced) + **DEV-API-009** (no
  `NotImplementedException`/`// TODO[UC-…]` in services) + **DEV-API-020**
  (planned own/assigned scope actually enforced across the four legs) +
  **DEV-API-021** (every permission constant exists verbatim in the same app's
  `*CorePermissionsSeedDataProvider.cs` — an unseeded or app-less 3-segment
  constant 403s every role-based user at runtime). An `err`
  on 010 = orchestrator's custom-action derivation skipped an entry — auto-heal
  applies the derivation, retries; if still red, `ba.gap-persistent` → DEFER
  (blocker `ba.gap`, high), continue. An `err` on 008/009 → auto-heal re-runs the
  business-logic pass (`business.todo-br`/`business.todo-uc`); on 020 → re-runs the
  data-scope cascade; on 021 → re-runs `scaffold-controller` for the entity when
  `reason: missing-app-segment` (the fixed fallback derives the 4-segment prefix)
  or `scaffold-core-seed` for the app when `reason: absent-from-seed` /
  `no-seed-provider`; persistent → DEFER, continue.
- **BLOCKING — `audit-dev-api DEV-API-016` (integration controller contract)**:
  every integration `<Entity>Controller.cs` carries `// @generated-by
  scaffold-controller`, exposes `[HttpGet("lookup")]`, returns a paginated list
  (`PaginatedResult<…ListDto>`, not a bare `IReadOnlyList<…>`), and has no
  unexpanded `[controller]` route token. This is the BACKEND twin of the
  frontend `@generated-by` gate — it catches a controller that was **hand-written
  off `scaffold-controller`** (the recurring 404-lookup + empty-table cause).
  An `err` means the backend drifted off the wire contract: **auto-heal re-runs
  `scaffold-controller` for the listed entity** (it emits the marker + lookup +
  paginated list by construction). If the entity's pagespec is malformed and the
  scaffolder still cannot produce a clean controller, **hard-fail that entity**
  (record the blocker, skip its frontend — coupling); never hand-write the
  controller (see `references/auto-healing.md` § anti-patterns).

## After Phase 2b (API Screen-driven)
- `.NET build` still succeeds (`dotnet build --no-restore`) with every new
  `{EntityPlural}ScreenController.cs` + `{Entity}{View}ScreenDto.cs` compiled.
- Every pagespec in `<MODULE>/pagespecs/` has a matching endpoint on its
  `{EntityPlural}ScreenController`.
- The Swagger document group `screens` lists exactly N endpoints, where N =
  count of pagespecs with view ∈ {list, detail, form, dashboard} + sum of
  `actions[]` `kind:api` entries.
- No `// TODO[SCREEN-` markers in generated controllers (audit-dev-api
  DEV-API-012/013 BLOCKING).
- Every screen-driven endpoint calls the SAME `I{Entity}Service` exposed by
  Phase 2a (DEV-API-014 enforces structurally — no parallel service forks).
- Every pagespec column carrying a `source` block is projected through its Core
  navigation in the service (DEV-API-015, warn-level — a warn routes the item
  back to the Phase 2b subagent, it does not block the gate).
- `dotnet test --no-build --no-restore --filter "Category=Acceptance"` still green
  (no AC regression). This single post-fan-out run is where the screen acceptance
  tests execute — Phase 2b subagents no longer run them per (section, entity) tuple
  (see `phases-detail.md` "Phase 2b").
- **Platform-seam wiring** (phases-detail § "Platform-seam registrations") —
  every leg is now gated by a BLOCKING rule, none is a prose-only check:
  - the module's list entities are registered in global search — the
    `<<< EXTENSION-SEARCH-DI >>>` markers carry one ACTIVE `search.Entity<…>`
    per list screen, each scoped entity carrying its derived `.RestrictTo(...)`
    (**DEV-API-029, err** — run `scaffold-extension-search` build-spec + CLI to
    heal; the engine is fail-closed by design, an empty seam returns 0 results
    with no error anywhere);
  - every own/assigned entity has its 4 data-scope legs (DEV-API-020, err);
  - every coded entity has its descriptor registered between the
    `<<< CODED-ENTITY-KEYS-DI >>>` markers (DEV-API-022, err);
  - NO hand-rolled code generator survives anywhere in the backend C# — no
    `Generate*/GetNext*` method, counter `DbSet`/`NextValue` column, or
    `Max(x => x.Code) + 1` scan (**DEV-API-034, err** — the socle's
    `CodedEntitySaveHandler` is the ONLY allocator; heal = declare the
    `**Code pattern**` in entité.md, scaffold both halves, DELETE the
    generator — never "fix" the generator itself).
  Time-entry imputation stays OPT-IN — absence is not a gate failure, but report
  unregistered candidates.

## Phase 3 pre-entry — MANDATORY even on re-runs

### Normal mode (no `--force`) — targeted re-gen via spec-diff ∪ disk-drift

Phase 3 ALWAYS re-enters this gate, but on a re-run it regenerates only the
**delta**, not all 28 pages. This is the frontend pendant of the Phases 0-2
`pre_entry_coverage_check` (§ above): compute what genuinely needs work BEFORE
fanning out, so a single-label edit costs ~15 s instead of ~3 min.

The regeneration set is the **union of two independent drifts**:

**A. Spec-drift** — `compute-page-diff` (pure, < 1 s, no LLM):
```bash
npx --prefer-offline tsx skills/ba-develop/cli/compute-page-diff/index.ts \
  --spec '{"moduleRoot":"<.smartstack/ba/{appCode}/{moduleCode}>"}'
```
Reads `report.diff.{added,modified,unchanged,removed}` and
`report.toRegenerate` (= `added ∪ modified`). On the first run there is no
`.run-snapshot.json` (`snapshotFound:false`) → every page is `added` →
regenerate all, naturally.

**Run this AFTER sub-phase 3.1** (the UI-design pre-pass): a `uiDesign` overlay
freshly written by 3.1 changes the pagespec's canonical hash, so the page lands
in `modified` and gets regenerated — that is a **legitimate, expected**
spec-drift (the judgment being applied), not an anomaly to investigate. On the
next run the overlay is already in both the pagespec and the snapshot → the
page is `unchanged` and 3.1 skips it (idempotent).

**B. Disk-drift** — close the **silent-skip class**. A page whose pagespec
hash is `unchanged` can still have a drifted `.tsx` (deleted, hand-edited into
an err state, broken i18n/import). For **each** key in `report.diff.unchanged`,
run `validate-page` on its `.tsx`:
```bash
npx --prefer-offline tsx skills/development/frontend/component/cli/validate-page/index.ts \
  --project-path "<web-root>" --page-file "<src/pages/{appLower}/{module}/{section}/{Entity}{View}Page.tsx>"
```
`validate-page` returns an `err` for a **missing** file too (it short-circuits
to `imports-resolve: pageFile does not exist`), and for the
`cli-generated-marker` rule — so a page that exists but was NOT produced by
`scaffold-component` (no `@generated-by` header) counts as drift. Any
`unchanged` page with ≥ 1 `err` → add it to the disk-drift set.

**Decision flow** (mirrors `gates.md` Phases 0-2 lines 23-34):
```
regenerate = unique( report.toRegenerate  ∪  disk-drift )
if regenerate == [] :
    → SKIP Phase 3a   (spec AND disk both clean — the real perf win)
else :
    → RE-ENTER Phase 3a for the entities owning a page in `regenerate`,
      regenerating exactly those views (see phases-detail.md § Phase 3a loop)
```
`removed` pages (pagespec gone, `.tsx` orphaned) are logged as a non-blocking
warning — not auto-deleted (git is the backup; the BA-side `reconcile-menu`
owns orphan removal).

**This gate is NOT optional.** The orchestrator MUST NOT skip Phase 3 on
artifact presence alone — skip requires BOTH an empty spec-diff AND zero
disk-drift. The `@generated-by scaffold-component` marker is the proof of
provenance; `validate-page` enforces it on every `unchanged` page.

**After a successful regeneration** (Phase 3 build gate green), write the new
baseline so the NEXT run diffs against it:
```bash
npx --prefer-offline tsx skills/ba-develop/cli/update-snapshot/index.ts \
  --spec '{"moduleRoot":"<.smartstack/ba/{appCode}/{moduleCode}>"}'
```
Snapshot the run ONLY after success — a failed run must never poison the baseline.

### `--force` mode

When `forceMode = true`, skip the spec-diff + validate-page scan entirely.
Re-enter Phase 3 from sub-phase 3.0 unconditionally. In Phase 3a, the skip
rule is disabled — every entity × every view invokes `scaffold-component` and
`scaffold-api-client` even if the files already exist. The CLIs overwrite in
place; no files are deleted. `update-snapshot` still runs after the build gate
so the baseline reflects the forced regeneration.

The run report logs every overwritten file under `phase3.regeneratedFiles[]`.

## After Phase 3 (Frontend)
- `validate-page` returns success for every page under
  `src/pages/{appLower}/{module}/` — ALL pages, not just those written during
  the current run.
- `audit-dev-frontend --mode audit` returns **0 `err` findings** after the
  apply pass. Remaining `warn` are tolerated. This includes the two detail-page
  360-completeness rules (both err, both need `--module-path`): **DEV-UI-031**
  (every declared related tab rendered + permission-gated + FK-filtered) and
  **DEV-UI-032** (every detail page carries its anchored Edit/Delete buttons +
  one `data-testid="detail-action-<code>"` per header custom action — the
  "detail page complete with clean tabs + the necessary actions" gate), plus the
  FK-Guid gates **DEV-UI-022** (form) / **DEV-UI-033** (list column, free-text FK
  filter, detail `<dd>`) — a raw Guid reaching the user is always a BLOCKING err;
  **remediation is deterministic**: re-run `derive-fk-specs` for the entity and
  re-invoke `scaffold-component` with the spliced `fkTo`, never hand-patch. For
  a `filter-input` finding the fix is upstream in the PAGESPEC — run
  `derive-filter-fks` on the module (it renames the filter onto the FK property
  and injects its `fkTo`), then re-scaffold; a filter it reports `unresolved` is
  a BA gap, not a scaffolder bug. Pass `--module-path` so DEV-UI-033 can
  cross-check the pagespec: without it the filter leg only sees `…Id` keys and a
  relation-named filter (`filters['client']`) stays invisible. **DEV-UI-011**
  (page guard ↔ pagespec permission, err) also needs `--module-path`; pass
  `--backend-path "<apiProjectDir>"` too so its frontend-only warn leg (spec
  permission enforced by NO controller, const-expressions resolved) actually
  runs — without the flag that leg is silent.
  Persist the report to `_audit/dev-frontend-{module}.md` (same discipline as
  dev-api / dev-wire) so a skipped pass is visible post-hoc.
- **DEV-UI-046 (componentKey reachability, err)** — every registry key's last
  segment is an IMPLICIT_SUFFIX or its node chain resolves in the seeded nav
  (`.smartstack/core-seed/*.state.json`; provider fallback). A dead key means
  the Phase 3b closing step was skipped: run
  `ba-develop/cli/derive-nav-resources` on the module, merge the derived
  resources into the core-seed spec, re-invoke scaffold-core-seed, re-audit.
  This is the gate that would have caught the 40%-unreachable-pages incident
  (21/52 keys silently redirecting to /applications while every audit said
  PASS).
- **BLOCKING — custom-action coverage (every run)**: `audit-dev-actions-alignment`
  returns **0 `err`**. This is the frontend twin of Phase 2a's DEV-API-010 — it
  fails when a pagespec `kind:"api"` action did not produce a wired button + hook
  + service method (`ACTION-DRIFT-006` = no `use<…><Entity>` on any
  `<Entity>*Page.tsx`; `ACTION-DRIFT-001/002/005` = pagespec ↔ controller ↔
  service URL drift). Run it unconditionally — a dropped action button is exactly
  the "page actions are not implemented" gap:
  ```bash
  npx --prefer-offline tsx \
    skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/index.ts \
    --project-path "<web-root>" --backend-path "<dotnet-root>" \
    --module-path "<.smartstack/ba/{appCode}/{moduleCode}>" --mode report-only
  ```
  Exit 1 ⇒ `failureKind: "custom-action-missing"`. **Remediation is
  deterministic**: re-run `derive-action-specs` for the entity, re-invoke
  `scaffold-api-client` + `scaffold-component` with the spliced `apiClient` array
  and the WHOLE pageSpec, then re-run this gate. Never hand-write the button.
- `npm run build` passes (Vite resolves every
  `lazyWithRetry(() => import('@/…'))` — catches broken imports that
  type-check misses).
- Every screen code from `structured.screens[]` has a `.tsx` under
  `src/pages/…` or `src/features/…`.
- Every `PageRegistry.register(key, lazyWithRetry(() => import('@/…')))` in
  `src/extensions/*Registry*.ts` resolves to an existing file.
- **BLOCKING — ComponentKey sanity**: every registered key MUST match the
  shape `{appCode}.{module}.{section}[.{view}]` and MUST equal a
  `componentKey` returned by `GET /api/navigation/menu`. Verify with:
  ```bash
  curl -s -b $cookie -H "X-Tenant-Slug: $tenant" \
    http://localhost:5142/api/navigation/menu \
    | jq '.. | .componentKey? // empty' | sort -u
  ```
  Forbidden patterns (silent spinner with no console error):
  - kebab-case: `'contacts-directory'` → must be `'crm.contacts.directory'`
  - missing app prefix: `'contacts.directory'` → must be `'crm.contacts.directory'`
  - single segment: `'directory'` → must be `'crm.contacts.directory'`
  - appCode-prefixed legacy `'TestV2.crm.contacts.directory'` is wrong (appCode
    is the **navigation app code** lowercased, not the .NET project name)
- `src/main.tsx` imports `./extensions/componentRegistry.generated` (side-effect
  import — triggers `register()` at startup).
- The `apiClient.ts` reads tenant from `localStorage.getItem('currentTenantSlug')`
  (NOT `tenant_slug`), uses `withCredentials: true` (SmartStack auth = httpOnly
  cookies), does NOT auto-redirect on 401 (let `SmartStackProvider` handle).
- Every route in `structured.permissions[]` is registered in the dynamic router
  (backend `NavigationConfiguration` seed — Phase 0).
- **DEV-UI-038 (module-wide i18n catalogue completeness, err)** — part of the
  0-err audit above, called out because it is the assembled-bundle net: every
  bare `t('key')` of every business page resolves to a STRING in all 4 locale
  bundles (a key resolving to an OBJECT is the label/children collision), and
  the fr/en/it/de key SETS of each business namespace are identical.
  `validate-page` ran checks 1+2 per page at scaffold time; 038 re-runs them
  over the merged module bundles where a later scaffold or hand edit can have
  destroyed an earlier entity's keys. **Remediation is deterministic**:
  re-invoke `scaffold-component` for the offending entity × view (its
  self-merged emission preserves siblings), then `aggregate-component-registry`.
- `npm test` — component tests pass (one per page from `scaffold-tests
  --layer=frontend`).

## After Phase 3e (Wire-up gate — static parity + runtime smoke)

Inserted after Phase 3 build gate; runs before Phase 4 boots. Two-step
gate, fast then slow:

### Step 1 — `audit-dev-wire` (static, ~1 s)

- **Gate (defer-not-halt)** — runs the URL parity audit defined in
  `templates/skills/development/audit-dev-wire/SKILL.md`. Cross-references
  every URL the generated frontend calls against every route the generated
  backend exposes.
- Pass condition: `report.counts.errors == 0` (zero `DEV-WIRE-001 err`).
- Failure routes to auto-healing via `wire.frontend-orphan` or
  `wire.strata-mismatch` (see `references/auto-healing.md`). Auto-fix
  re-invokes the appropriate scaffolder; a `wire.verb-mismatch` that cannot be
  reconciled is **deferred** as a blocker (high), not a stop.
- **A PERSISTENT `DEV-WIRE-001` frontend-orphan is NOT shipped.** After the
  deterministic re-scaffold heal, if the frontend still calls a route no backend
  serves (the backend genuinely doesn't exist — a hand-write that was rejected,
  or a phantom service for a non-entity section), the orchestrator
  **disables/removes that unbacked frontend service** (a named file — never a
  directory) and marks the module **`failed` for that entity**. The run still
  continues to other modules (never-halt), but a guaranteed-404 surface is NEVER
  shipped as `completed-with-blockers`. This is the gate that makes the historic
  "31 DEV-WIRE-001 errors deferred, app shipped broken" outcome impossible.
- Invocation:
  ```bash
  npx --prefer-offline tsx skills/development/audit-dev-wire/cli/audit-dev-wire/index.ts \
    --project-path "<dotnet-root>" \
    --web-path     "<web-root>" \
    --module-code  "<MODULE>" \
    --app-code     "<APP>" \
    --mode audit
  ```

### Step 1b — i18n runtime channel (`audit-dev-frontend DEV-UI-028`, static)

- **Gate (defer-not-halt)** — every business i18n namespace that has a locale
  bundle on disk (`src/i18n/locales/<locale>/<module>.json`) MUST be registered
  through `addClientResources` in `src/extensions/moduleResources.generated.ts`.
  A namespace present on disk but unregistered renders as RAW KEYS at runtime
  (the SDK's i18next init replaces the resource store; only registrations that
  run after it, via the aggregator's generated file imported at the end of
  `componentRegistry.generated.ts`, survive).
- Pass condition: zero `DEV-UI-028 err`.
- Failure heal: re-run `aggregate-component-registry --project-path "<web-root>"`
  (it re-emits `moduleResources.generated.ts` from the on-disk locale files),
  then re-audit. A persistent failure is **deferred** as a blocker (high) — never
  a stop — but a known raw-keys surface is never silently shipped as completed.

### Step 1c — `@customised` contract drift (`audit-dev-customised`, static)

- **Gate (defer-not-halt)** — a page marked `@customised` is PRESERVED across
  regeneration, so it can silently drift from the regenerated service/hook
  contract. Especially relevant right after a `--force` full regen. Two checks:
  `DRIFT-001` (imports a name a regenerated `…/hooks/use…` or `…/services/…Service`
  no longer exports) and `DRIFT-002` (still calls the retired
  `/dashboard/consolidated` route — the dashboard is now on the screen stratum).
- Pass condition: `report.counts.errors == 0`.
- Invocation:
  ```bash
  npx --prefer-offline tsx skills/development/audit-dev-customised/cli/audit-dev-customised/index.ts \
    --web-path "<web-root>" --module-code "<MODULE>" --app-code "<APP>" --mode audit
  ```
- Failure: re-align the bespoke page with the current export / route (see
  `report.findings[].solution`) or drop the stale import. A persistent `DRIFT-*`
  is **deferred** as a blocker (high) — never a stop.

### Step 2 — `run-smoke` (runtime, ~30-120 s) — MANDATORY, runs unconditionally

- **Gate (defer-not-halt)** — boots `dotnet run` + `npm run dev` in background,
  then runs FOUR axes and fails on ANY of them:
  1. **HTTP probes** — every page route + API endpoint. Fails on any UNEXPECTED
     `4xx`/`5xx`: 404 (missing route — the NavRoute mismatch class), 405 (wrong
     verb), 400 (broken contract/config), 5xx. A 401/403 means the route EXISTS
     but is `[Authorize]`-gated → PASSES (anonymous probe; real-token coverage is `/uat`).
  2. **Browser pass (headless Chromium)** — loads `/` + each list/home/dashboard
     route in a REAL browser. This is the axis HTTP probing is structurally blind
     to (Vite serves `index.html` 200 for every SPA route). Fails on: a **Vite
     error overlay** (a CSS/PostCSS 500 — the `scaffold-theme` comment-`*/` class),
     a non-whitelisted **console.error / uncaught pageerror** (a client React crash
     — e.g. the cross-app "Absolute route path … nested … not valid"), or any
     4xx/5xx **sub-resource** (a CSS module 500, a missing JS chunk). A missing
     Playwright/Chromium is a **`smoke.browser-unavailable` blocker**, NEVER a
     silent pass — install it in the web app (`npx playwright install chromium`).
  3. **Nav-menu app-consistency** (BUG A guard, best-effort) — GET
     `/api/navigation/menu`: every section's route app-segment must match its
     componentKey app-prefix. A mismatch fails the gate; an auth-gated 401 SKIPS
     the check (not a failure — the seed-level scoping + aggregator guard cover it
     statically).
  4. **Action-contract probes (axis 4b)** — reads the module's `pagespecs/*.md`,
     derives every custom-action endpoint and fires each with a SYNTHESIZED VALID
     body (from `payloadParameters`) at the sentinel id (`00000000-…`, row) / an
     empty id set (bulk) so the handler answers **404 before any commit** — the
     contract is validated, no row is mutated. Classification: `415` (body /
     content-type rejected — the empty-body gap the anonymous HTTP axis is blind
     to) / `405` (wrong verb) / `5xx` (handler threw on a valid body) **FAIL**;
     `2xx` / `404` **PASS**; `400`/`422` INDETERMINATE (synth body may be
     insufficient); `401`/`403` **AUTH-GATED**. Because these endpoints are
     `[RequirePermission]`, the contract is only truly validated with an admin
     token: pass `adminToken` (e.g. from `/uat provision`). WITHOUT a token the
     axis still runs and surfaces `smoke.interaction-unavailable` (medium note,
     NOT a hard failure — never a silent pass).
- **Invocation** (MANDATORY — actually run this; never assume it ran):
  ```bash
  npx --prefer-offline tsx skills/development/smoke-test/cli/run-smoke/index.ts \
    --spec '{"projectPath":"<projectPath>","prdSlice":"<frontend PRD slice JSON>","moduleCode":"<MODULE>","backendPort":5000,"frontendPort":3000,"moduleRoot":"<.smartstack/ba/{appCode}/{moduleCode}>","adminToken":"<optional admin JWT>"}'
  ```
  The CLI **writes `<projectPath>/_audit/smoke-<MODULE>.md` itself** — the file's
  presence is the machine-checkable proof the gate ran.
- Pass condition: `report.passed == true` (HTTP + browser + nav + action-contract
  axes ALL clean — zero `verdict:"fail"` on axis 4b — and the browser was
  available unless `skipBrowser`).
- **Runs unconditionally** — NOT skipped when Step 1 emitted `err` (the runtime is
  the ground truth; a static-audit error is exactly when you want runtime
  confirmation), and NOT skipped when the migration was deferred (a routing 404 is
  independent of DB state). The migration is incontournable in `/ba-develop`
  (sanctioned `scaffold-migration`, then applied through the sanctioned
  `skills/efcore/cli/apply` CLI in the entities phase BEFORE 3e — with auto-migrate
  at boot as the fallback), so smoke normally probes a real migrated DB.
- A MISSING smoke report (`_audit/smoke-<MODULE>.md` not written) is itself a Phase
  3e failure — never treat "smoke didn't run" as a pass.
- Failure routes to auto-healing via `smoke.4xx` / `smoke.5xx` / `smoke.boot` /
  `smoke.browser` (console/overlay) / `smoke.nav-merge` / `smoke.action-body` (a 415
  on a valid body — the custom-action content-type contract); an unhealable failure
  is **deferred** as a blocker (`smoke.5xx` / `smoke.browser` / `smoke.action-body`
  high, `smoke.boot` / `smoke.browser-unavailable` / `smoke.interaction-unavailable`
  medium) — the run continues and still commits its checkpoints, it never stops here.
- Boot timeout default: 120 s per service (configurable). A boot timeout is a
  `medium` blocker (infra), never a stop.

The smoke gate is additive — it never bypasses the static audit. Both feed
Phase 3's result; an unhealable failure on either is **deferred** as a blocker,
so Phase 3 always completes and the run moves on to Phase 4.

## After Phase 4 (Acceptance Tests)
- `scaffold-tests-from-ac` returned `success: true`. That flag is REAL since
  the `lost` channel: any AC the contract will not carry (malformed bullet,
  duplicate id) fails the envelope (exit 1) — files are still written for the
  heal loop. (The old criterion here, `factsEmitted == acsParsed`, compared
  two numbers equal BY CONSTRUCTION — a gate that could not fail. The real
  emission proof is DEV-TEST-001/008 below.)
- **`audit-dev-tests` returns `success: true`** (no `err` across
  DEV-TEST-001..004 + 008 + 009 + 011; 005..007 and 010 are advisory `warn`):
  ```bash
  npx --prefer-offline tsx skills/development/audit-dev-tests/cli/audit-dev-tests/index.ts \
    --spec '{"moduleDir":"<moduleDir>","projectPath":"<projectPath>"}' --json
  ```
  - DEV-TEST-001 (err) — every BA AC has a generated `[Fact]`.
  - DEV-TEST-002 (err) — no `// TODO[AC-` markers remain.
  - DEV-TEST-003 (err) — no stale `[Trait("AC", "<ref>")]` references (a
    green `[Fact]` on a deleted/renumbered AC is a coverage lie — AC churn
    now blocks).
  - DEV-TEST-004 (err) — no stub assertion (`Assert.True(true)` = false
    coverage).
  - DEV-TEST-009 (err) — business-rule test parity, MODULE-SCOPED traits
    (see the 2.4 section above).
  - DEV-TEST-011 (err) — no AC lost at parse (malformed/duplicate bullet):
    the audit twin of the scaffolder's `lost` exit-1 — never-halt cannot
    swallow this one.
  - DEV-TEST-008 (err) — every UC declares ≥ 1 Acceptance Criterion (no
    untestable UC). Closes the "UC with zero ACs = silently uncovered" hole:
    a UC with no ACs emits no `[Fact]`, so DEV-TEST-001 would pass vacuously.
- `dotnet test --no-restore --filter "Category=Acceptance"` ideally returns 0 failures. Tests
  failing because the production code does not satisfy the AC ARE the signal —
  auto-heal tries `acceptance.todo-ac` first; persistent failure with no stub
  → `tests.acceptance.fail-real` is **deferred** (blocker
  `tests.acceptance.disagreement`, high): the test stays red and the run continues.
- `acCoverage` aggregates to `{ total: baAcCount, covered: baAcCount -
  <DEV-TEST-001.refs>.length, missing: <DEV-TEST-001.refs> }`. `ucsWithoutAc`
  (from DEV-TEST-008) surfaces the untestable UCs that inflate a 100% AC-coverage
  reading — an empty list is the real "every UC tested" proof.
