---
name: project-inventory
description: >
  Deterministic scanner for a generated SmartStack project. Produces a
  structured JSON manifest of existing entities, migrations, seed providers,
  controllers, React pages, and extension registries. Consumed by each phase
  of ba-develop so the Claude subagent can distinguish
  "already implemented" from "needs to be created" before writing any file.
  Zero LLM — regex-based scan, runs under 2 seconds on a normal app.
phase: validation
cli: project-inventory
allowed-tools: [Read, Glob, Grep, Bash]  # Bash: CLI invocation
---

# project-inventory

Scans a SmartStack-generated application and emits a JSON manifest of the
observable facts: what entities exist, which migrations have been created,
which seed providers cover which entities, which controllers expose which
routes, which React pages exist under `src/features/**`. **No inference, no
interpretation** — only facts regex can extract deterministically.

## Why this exists

Without this, each phase of `ba-develop` (domain/data/api/frontend)
hands the subagent a PRD slice and lets it "vibe-code" against an unknown state.
Symptoms:
- Duplicate migrations for entities that already have a CreateX migration.
- Gate false positives ("migration without seed") when the seed provider
  actually covers the entity but wasn't touched this phase.
- Regressions — correct files rewritten from LLM assumptions.

Running this CLI **before** the phase loop gives the subagent a deterministic
ground truth it can consume in its "Explore" step. The subagent then produces
a `PLAN:` that references inventory entries by path and can skip items
already implemented with a motivated reason.

## When to use

- **Before each ba-develop run** — the handler spawns this CLI
  once, caches the result in memory, and injects it into every phase's
  system prompt.
- **Manual debugging** — when the user wants to audit what's in a generated
  project without trusting the subagent's self-report.
- **CI sanity** — can be wired into a GitHub Actions step to detect drift
  (e.g. migration created without matching seed provider).

## Invocation

```bash
npx tsx skills/validation/project-inventory/cli/project-inventory/index.ts \
  --project-path "/abs/path/to/generated-project" \
  [--output-file .ba-develop/inventory.json]
```

If `--output-file` is provided, the JSON is also written to that path
(convenient for caching / diffing across runs). Stdout always contains the
full envelope.

## Arguments

| Arg | Type | Required | Description |
|-----|------|----------|-------------|
| `--project-path` | string | yes | Absolute path to the generated project root. |
| `--output-file` | string | no | Optional path to dump the inventory JSON. Default: stdout only. |
| `--domains` | string list | no | Comma-separated subset of scan domains (`domain`, `data`, `api`, `frontend`). Default = all. |

## Output shape

Envelope + report structure:

```json
{
  "success": true,
  "command": "project-inventory",
  "data": { "scannedAt": "2026-04-22T15:00:00Z", "durationMs": 1234 },
  "report": {
    "projectPath": "/abs/...",
    "hasDotnet": true,
    "hasFrontend": true,
    "domain": {
      "entities": [
        {
          "name": "Contact",
          "file": "src/TestV2.Domain/Crm/Contact.cs",
          "baseClass": "BaseEntity",
          "namespace": "TestV2.Domain.Crm"
        }
      ]
    },
    "data": {
      "migrations": [
        {
          "file": "src/TestV2.Infrastructure/Persistence/Migrations/20260418184515_CreateContacts.cs",
          "name": "CreateContacts",
          "timestamp": "20260418184515",
          "tablesCreated": ["Contacts"],
          "tablesDropped": [],
          "action": "create"
        }
      ],
      "seedProviders": [
        {
          "file": "src/TestV2.Infrastructure/Persistence/Seeding/Providers/CrmContactsSeedDataProvider.cs",
          "className": "CrmContactsSeedDataProvider",
          "entitiesReferenced": ["Contact"],
          "tablesReferenced": ["Contacts"],
          "seedsNavigation": true,
          "seedsPermissions": true
        }
      ]
    },
    "api": {
      "controllers": [
        {
          "file": "src/TestV2.Api/Controllers/ContactsController.cs",
          "className": "ContactsController",
          "route": "api/contacts",
          "httpActions": ["GET", "POST", "PUT", "DELETE"]
        }
      ]
    },
    "frontend": {
      "pages": [
        {
          "file": "src/features/crm/contact/pages/ContactListPage.tsx",
          "entity": "contact",
          "kind": "list"
        }
      ],
      "registries": ["src/extensions/crmRegistry.ts"],
      "i18n": { "locales": ["fr","en","it","de"], "namespaces": ["contact"] }
    }
  },
  "errors": [],
  "warnings": [],
  "nextSteps": []
}
```

## Detection rules (deterministic)

| Domain | Pattern |
|--------|---------|
| **Entity** | C# class declaration in `**/Domain/**/*.cs` where the base class is `BaseEntity`, `TenantEntity`, `AuditableEntity`, or similar SmartStack roots. Namespace extracted from `namespace X.Y.Z;` or `namespace X.Y.Z { ... }`. |
| **Migration** | C# class in `**/Migrations/*.cs` inheriting `Migration`. Name = class name stripped of timestamp prefix. Tables created = arguments to `migrationBuilder.CreateTable("{name}", ...)`. Action = `create` if any `CreateTable` call, `alter` if only `Alter*`, `drop` otherwise. |
| **Seed provider** | C# class whose file path matches `**/Seeding/**/*Seed*.cs` or `**/Seeding/**/*Provider*.cs`. `entitiesReferenced` = match `new <Entity>(`, `typeof(<Entity>)`, `nameof(<Entity>)`. `seedsNavigation` = contains `NavigationApplication`/`NavigationSection`/`NavigationModule`. `seedsPermissions` = contains `Permission(` or `permissionCode:`. |
| **Controller** | C# class ending in `Controller.cs` with `[ApiController]` or `: ControllerBase` or `: Controller`. Route from `[Route("...")]` on class or `[HttpGet("...")]` on method. |
| **React page** | `*.tsx` file ending in `ListPage.tsx`, `DetailPage.tsx`, `FormPage.tsx`, `DashboardPage.tsx`, `CreatePage.tsx`, `EditPage.tsx`. Entity name = filename root. Kind = suffix (list/detail/form/...). |
| **Extension registry** | `.tsx`/`.ts` files under `src/extensions/*Registry*.ts` containing `PageRegistry.register(`. |
| **i18n** | Locales = folder names under `i18n/`. Namespaces = filename roots of `*.json` under each locale folder. |

## Design constraints

- **Zero LLM** — pure regex + filesystem walk. No ts-morph, no Roslyn. The CLI
  must run in under 2 seconds on a typical generated project (≤ 100 files
  per domain).
- **No side effects by default** — `--output-file` is opt-in. Default mode is
  stdout only.
- **Ignore noise** — `node_modules`, `bin`, `obj`, `dist`, `out`, `.git`, and
  any file matching `*.Designer.cs` or `*.g.cs` (EF snapshot generated code).
- **Forgiving on missing layers** — if the project has no `Domain/` folder
  (pure frontend), the `domain` + `data` + `api` keys are empty objects, not
  errors.
- **Cross-platform** — paths in output are forward-slash relative paths from
  `projectPath`, regardless of host OS.
