# `/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`, group permission tuples by
   module segment. Compare against modules declared in `rbac.md`. Any
   BA module with < 2 permissions → ERR.

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** (mirrors DEV-API-001): run
   `project-inventory --domains api`. For each entity in `entité.md`
   that the PRD declares should have an API surface, verify a matching
   controller in `report.api.controllers[].className`. Missing → 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.

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 5 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 5
  `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.
- Every entity in `structured.entities[]` has a `.cs` under `Domain/{ModuleCode}/`.
- 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.
- Module-scoped reference-data providers under
  `Persistence/Seeding/Modules/{ModuleCode}/` are OPTIONAL (only required for
  pre-populated lookup tables). Soft warning — nav/roles/perms are Phase 0's job.
- `scaffold-tests --layer=domain` produced ≥ 1 `{Entity}Tests.cs` per entity
  and `dotnet test --filter "Category=Domain&FullyQualifiedName~{ModuleCode}"` passes.

## After Phase 2a (API Integration)
- `.NET build` still succeeds.
- Every controller carries `[RequirePermission]` matching a path from
  `structured.permissions[]`.
- Integration tests for CRUD on each entity pass
  (`dotnet test --filter "Category=Integration&FullyQualifiedName~{ModuleCode}"`).
- **BLOCKING** — Business-rule tests pass
  (`dotnet test --filter "Category=Business&FullyQualifiedName~{ModuleCode}"`) —
  one `[Theory]` per `structured.businessRules[]` entry, fed by
  `validExamples` / `invalidExamples`. Non-negotiable.
- **BLOCKING — No residual stubs**:
  `grep -rn "NotImplementedException\|TODO\[BR-\|TODO\[UC-"` across
  `src/{Ns}.Application` + `src/{Ns}.Infrastructure` returns nothing.
  Audit DEV-API-008/009 perform the same check with structured findings.
- **BLOCKING — Business-rule test parity**: count of `Category=Business` test
  cases ≥ number of rules in `règles-métier.md`. (Vacuous "0 rules → 0 tests
  → green" is disallowed.)
- **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
  ```
  Runs DEV-API-010 (every pagespec `kind:api` non-CRUD has a matching
  `[HttpVerb]` on the integration controller) + DEV-API-011 (no phantom
  endpoint). An `err` here = 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.

## After Phase 2b (API Screen-driven)
- `.NET build` still succeeds 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).
- `dotnet test --filter "Category=Acceptance"` still green (no AC regression).

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

**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.
- `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).
- Locale parity: every `t('key')` in generated pages resolves in fr/en/it/de.
- `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.
- 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 2 — `run-smoke` (runtime, ~30-120 s)

- **Gate (defer-not-halt)** — boots `dotnet run` + `npm run dev` in background,
  probes every page route + API endpoint, flags any `4xx`/`5xx`.
- Pass condition: `report.passed == true` AND `report.failedProbes == 0`.
- Failure routes to auto-healing via `smoke.4xx` / `smoke.5xx` / `smoke.boot`;
  an unhealable failure is **deferred** as a blocker (`smoke.5xx` high,
  `smoke.boot` medium) — the run continues, it never stops here.
- Skips boot if Step 1 already emitted `err` findings (no point booting a
  backend known to be missing routes).
- 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` with `factsEmitted == acsParsed`.
- **`audit-dev-tests` returns `success: true`** (all of DEV-TEST-001/002/003 are
  `ok` or `warn`, no `err`):
  ```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 (warn) — no stale `[Trait("AC", "<ref>")]` references.
- `dotnet test --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> }`.
