---
name: backend-core-seed
description: >
  Phase 0 of /ba-develop — generates one Core Foundation Seed
  bundle per declared application (6 IClientSeedDataProvider classes per app
  covering navigation + tenant↔app links + roles + permissions +
  role-permission mappings + dev test users) by invoking the deterministic
  `scaffold-core-seed` CLI. Zero
  creative work for the agent: the spec is built by the Studio backend
  (`prepareCoreSeedContext`) from the BA menu / actors / permissions and
  dropped to a temp file before this skill runs.
phase: development/backend
cli: cli/scaffold-core-seed
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# Core Foundation Seed — Phase 0 of develop pipeline

This is the FIRST phase of `/ba-develop`. It generates the
per-application foundation that every subsequent phase (Entities, API,
Frontend) depends on:

For each application declared in the BA menu tree (one or more):

- Application + every Module + every Section + every Resource declared in
  the BA menu tree, in
  `Seeding/Applications/{AppPascal}/Core/{AppPascal}CoreNavigationSeedDataProvider.cs`
- One tenant↔application link per tenant (`core.tenant_TenantApplications`), in
  `{AppPascal}CoreTenantApplicationsSeedDataProvider.cs` — the platform only
  links its OWN apps (myspace/administration/support/api); without this
  provider no tenant ever sees the client app in the navigation menu and the
  deployed app boots with an EMPTY business sidebar (STU-RUN-005,
  runtime-proven). Self-healing at every boot; intentional no-op in
  extend-built-in mode (the platform already links its own apps)
- One `Role` per actor × `applicationCode`, in
  `{AppPascal}CoreRolesSeedDataProvider.cs`
- One `NavigationPermission` per `structured.permissions[]` row, in
  `{AppPascal}CorePermissionsSeedDataProvider.cs`
- Role → permission wiring in `{AppPascal}CoreRolePermissionsSeedDataProvider.cs`
- One dev test user per role (gated by `IHostEnvironment.IsDevelopment()`)
  in `{AppPascal}CoreTestUsersSeedDataProvider.cs`
- Each application's 6 providers registered in `DependencyInjection.cs`
  between `<<< CORE-SEED-DI-{AppPascal} BEGIN/END >>>` markers (one block
  per app)

## Why this skill exists

The previous per-module seeding pattern allowed a module to ship with 9
entities + 2 controllers but **empty navigation / roles / permissions**
(observed in TestV2/ba-002, Budget module). Each per-module agent only saw
its own slice, so the global navigation tree was never assembled
deterministically.

This skill moves seeding to a **single per-application deterministic generator**
that sees the full BA state for that app in one shot. Each `ApplicationSpec`'s
`modules[]` field is load-bearing: the CLI's `validate.ts` and the runtime gate
`verifyCoreFoundationSeed` both refuse to pass if any declared module lacks
nav + ≥ 1 role + ≥ 2 permissions in its owning application.

Multi-application projects (e.g. CRM + Budgeting in the same .NET solution)
get one Core bundle per app, with class names prefixed by the PascalCase app
code to avoid collisions and DI markers scoped per app for idempotent re-runs.

## Your job

The Studio backend has already assembled the spec and written it to a temp
file. Your job is exactly three steps:

1. **Invoke the CLI** with the spec file path provided in the prompt:

   ```bash
   npx --prefer-offline tsx skills/development/backend/core-seed/cli/scaffold-core-seed/index.ts \
     --spec-file <SPEC_FILE_PATH> \
     --outdir <PROJECT_PATH>
   ```

2. **Verify the output envelope** — the CLI prints a JSON envelope on
   stdout. Confirm `success: true` and that:
   - `filesCreated` lists `6 × N_apps` `{AppPascal}Core*SeedDataProvider.cs`
     files (and the test users manifest, if present)
   - `filesModified` lists the patched `DependencyInjection.cs`
   - `data.diPatched === true` (otherwise the providers will not be
     resolved at startup)
   - `data.appsPatched` lists every application code from the spec
   - `warnings[]` is empty (or only minor — review them)

3. **Emit a SHORT final report**, then STOP:
   ```
   Created: 6 × N apps Core providers + DI patch | Apps: <list> | Warnings: 0
   ```

## What you MUST NOT do

- Do not edit any of the generated `.cs` files. They are deterministic;
  any change you make will be overwritten on the next Phase 0 run.
- Do not regenerate the spec yourself. The backend builds it from the BA
  tables; agent-built specs reintroduce the drift this phase exists to kill.
- Do not run `dotnet build` from this skill — the runtime gate
  (`verifyCoreFoundationSeed` + `dotnet build`) runs after your phase ends.
- Do not add navigation / roles / permissions inline into module-specific
  seed providers in Phase 1. Phase 1's `scaffold-seed` is for **reference
  data only** (lookup tables, enum codes), placed under
  `Seeding/Applications/{AppPascal}/Modules/{ModuleCode}/`.

## Spec contract — what the CLI consumes

See `cli/scaffold-core-seed/types.ts` for the full Zod schema. The shape is:

```jsonc
{
  "appCode": "TestV2",                  // PascalCase — used in src/{AppCode}.Infrastructure/...
  "projectPath": "/path/to/target/app",
  "applications": [
    {
      "code": "crm",                    // kebab-case — must equal the application-level nav entry
      "modules": ["contacts", "prospecting"],   // gate verifies each is covered
      "navigation": [
        { "level": "application", "code": "crm", "label": "CRM", "route": "/crm" },
        { "level": "module", "code": "contacts", "parentCode": "crm", "label": "Contacts", "route": "/crm/contacts" },
        // previousCodes = declared renames (from the index.md anchors written by
        // /ba-reconcile-menu) — they feed the prod reconciliation (see below).
        { "level": "section", "code": "directory", "parentCode": "contacts", "label": "Directory", "route": "/crm/contacts/directory", "previousCodes": ["contact-list"] }
      ],
      "roles": [
        { "code": "admin", "name": "Administrator", "applicationCode": "crm", "isDefault": false }
      ],
      "permissions": [
        { "path": "crm.contacts.directory.access", "action": "access", "sectionCode": "directory" },
        { "path": "crm.contacts.directory.read",   "action": "read",   "sectionCode": "directory" }
      ],
      "rolePermissions": [
        { "roleCode": "admin", "permissionPath": "crm.contacts.directory.access" }
      ],
      "testUsers": [{ "roleCode": "admin" }]
    },
    {
      "code": "budgeting",
      "modules": ["budget"],
      "navigation": [/* … */],
      "roles": [/* … */],
      "permissions": [/* … */],
      "rolePermissions": [/* … */],
      "testUsers": [/* … */]
    }
  ],
  "testUserPassword": "DevTest!2026Secure",
  "testUserEmailDomain": "smartstack.local",
  "emitDevCredentialsManifest": true
}
```

### Extend-built-in mode — client modules under a PLATFORM app (e.g. `hr`)

When the BA planned an **extension of a built-in platform app** (the app node's
`## Contexte` carries « Extension de l'application plateforme `hr` » — see
`/ba-create-menu` § "extend, never duplicate"), the slice references the
platform app instead of creating one:

```jsonc
{
  "code": "hr",                                  // the PLATFORM app code — permission paths root here
  "extendsBuiltinApp": {                          // codes/GUIDs from lib/platform-catalog.ts
    "code": "hr",
    "guid": "9cbeae29-772f-43b1-ac93-c56f2cb90921"
  },
  "modules": ["trainings"],
  "navigation": [
    // NO level:"application" entry — the validator REJECTS one in this mode.
    { "level": "module", "code": "trainings", "parentCode": "hr", "label": "Formations", "route": "/hr/trainings", "displayOrder": 10 },
    { "level": "section", "code": "catalog", "parentCode": "trainings", "label": "Catalogue", "route": "/hr/trainings/catalog" }
  ],
  "roles": [ { "code": "training-manager", "name": "Training Manager", "applicationCode": "hr", "category": "Manager" } ],
  "permissions": [ /* hr.trainings.catalog.* — root at the platform code */ ],
  "rolePermissions": [ /* … */ ]
}
```

Effects (all deterministic, tested in `__tests__/extend-builtin.test.ts`):
- **No `NavigationApplication.Create`** is emitted — the platform owns the `hr`
  row; a second one would win the boot race and fork the nav tree.
- Every provider resolves the parent app **by the platform GUID**
  (`a.Id == Guid.Parse("9cbeae29-…")`), immune to seed ordering and duplicate
  codes; client modules attach to the existing ApplicationId.
- The **TenantApplications provider is an intentional no-op** — the platform
  already links its own applications to every tenant at boot; the class is
  kept so the 6-provider contract stays uniform across apps.
- The **state snapshot carries no application entry**, so `derive-seed-delta`
  can never rename/deactivate the platform row.
- Roles/permissions/rolePermissions work unchanged (scoped to the platform app).
- `administration` and `myspace` are NOT extendable (platform-managed) — only
  `hr`, `support` and `api` are (see `lib/platform-catalog.ts` `extendable`).

## Output structure (in the target app)

```
.smartstack/core-seed/
└── {appCode}.state.json                ← desired-state snapshot (committed — see Reconciliation)
src/{AppCode}.Infrastructure/
├── {AppCode}.Infrastructure.csproj     ← patched: EmbeddedResource Scripts/**/*.sql block
├── DependencyInjection.cs              ← patched: one block per app + the runner block
└── Persistence/Seeding/
    ├── Scripts/
    │   ├── CoreSeedScriptRunner.cs     ← Order = 5, applies pending delta scripts (see Reconciliation)
    │   └── {version}_{app}.sql         ← release delta scripts (generated by derive-seed-delta)
    └── Applications/
        ├── {AppPascalA}/Core/
        │   ├── {AppPascalA}CoreNavigationSeedDataProvider.cs         (Order = 10)
        │   ├── {AppPascalA}CoreTenantApplicationsSeedDataProvider.cs (Order = 15, tenant↔app links)
        │   ├── {AppPascalA}CoreRolesSeedDataProvider.cs              (Order = 20)
        │   ├── {AppPascalA}CorePermissionsSeedDataProvider.cs        (Order = 30)
        │   ├── {AppPascalA}CoreRolePermissionsSeedDataProvider.cs    (Order = 40)
        │   └── {AppPascalA}CoreTestUsersSeedDataProvider.cs          (Order = 50, IsDevelopment-gated)
        └── {AppPascalB}/Core/
            └── (6 more, prefixed with {AppPascalB}…)
```

C# namespaces are scoped per app:
`{AppCode}.Infrastructure.Persistence.Seeding.Applications.{AppPascal}.Core`.

## Idempotence — safe to re-run

GUIDs are deterministic (`Guid.Parse` with stable hash keys derived from
`nav:{appCode}:{level}:{code}`), so re-running the CLI does NOT churn
database row IDs. The DI patch uses `<<< CORE-SEED-DI-{AppPascal} BEGIN/END >>>`
markers (one pair per application) — each block is replaced wholesale on
re-runs without touching surrounding registrations or other apps' blocks.
The runner registration (`<<< CORE-SEED-SCRIPT-RUNNER BEGIN/END >>>`) and the
csproj `EmbeddedResource` block (`<<< CORE-SEED-SCRIPTS BEGIN/END >>>`) follow
the same marker discipline.

## Prod data reconciliation — how a seed CHANGE reaches an existing database

The boot seed is strictly **additive** (insert-if-not-exists). On a database
that already has data (prod), a re-release with a changed seed would silently
ignore property updates, duplicate renamed codes and leave removed entries +
revoked permissions in place. The reconciliation pipeline closes that gap:

1. **State snapshot** — this CLI emits `.smartstack/core-seed/{app}.state.json`
   per application (stable sort, `specHash` content hash, committed with the
   project). It also stamps seed-owned RolePermissions with
   `AssignedBy = "scaffold-core-seed:{app}"` so delta scripts can tell them
   apart from admin grants.

2. **Release delta** — on the release/hotfix branch, the colocated CLI
   `cli/derive-seed-delta` diffs the state at `origin/main` vs the working
   tree and generates ONE reviewable, idempotent SQL script per changed app
   under `Persistence/Seeding/Scripts/{version}_{app}.sql`:
   - renames (declared via `previousCodes`/`previousPaths` aliases, or
     resolved by a human via `resolvedRenames`) → `UPDATE … SET Code/Path`
     (GUID + FKs preserved);
   - property updates (label/icon/route/order/name/action) → `UPDATE`;
   - removed nav entries → `IsActive = 0` (soft — never a physical delete);
   - removed permissions → `DELETE` (their mappings first, counted);
   - revoked role-permission mappings → `DELETE` guarded on the seed
     `AssignedBy` marker (admin grants untouched);
   - additions carry NO SQL (the additive boot seed inserts them) except a
     guarded reactivation for nav entries a past delta deactivated.
   The PR review IS the human approval gate for anything destructive. The
   summary (envelope `data.summary`) is markdown for the PR description.
   Ambiguous rename candidates are reported, never guessed — re-run with
   `resolvedRenames` after the user decides.

3. **PR gate** — the gitflow `pr` CLI blocks a PR to main when a state file's
   `specHash` changed without a committed script whose header
   (`-- baseHash:`/`-- newHash:`) bridges exactly that change. Baseline
   (no state at main yet) is exempt.

4. **Boot apply** — the generated `CoreSeedScriptRunner` (Order 5, before the
   additive providers) applies pending embedded scripts in version order,
   exactly once per database (`extensions.core_SeedScriptHistory`), one
   transaction per script, serialized across concurrent instances via
   `sp_getapplock`. Failure = rollback + rethrow (fail-closed) → retried at
   the next boot.

Rename aliases flow from the BA layer: `/ba-reconcile-menu` persists each
validated rename as a `previousCodes=` attribute on the renamed node's
`index.md` anchor; the Phase 0 spec builder transports it into
`navigation[].previousCodes` (the permission `previousPaths` combinations are
derived from the nav aliases automatically when the state snapshot is built).

Typical release runbook (client project):
```bash
# on the release branch, before the PR to main:
npx --prefer-offline tsx skills/development/backend/core-seed/cli/derive-seed-delta/index.ts \
  --spec '{"version":"<release version>","projectPath":"<project root>"}'
# review the generated Scripts/{version}_{app}.sql, resolve ambiguous renames if any,
# commit via gitflow commit, put data.summary in the PR description, open the PR.
```

## Invocation summary

```bash
npx --prefer-offline tsx skills/development/backend/core-seed/cli/scaffold-core-seed/index.ts \
  --spec-file /tmp/core-seed-spec-<runId>.json \
  --outdir /path/to/target/app
```

Optional: `--dry-run` prints what would be generated without writing.
