# ADR-014 — Autodescoberta de módulos e agrupamento por pasta

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-16) |
| **Data** | 2026-10-14 |
| **Plano de origem** | `wiki/plan/07-core-contratos-discovery.md` |
| **Relacionadas** | ADR-013, ADR-009, ADR-004 |

## Contexto

O `sucesu-cms` resolve a navegação do CMS por **autodescoberta**: um único arquivo
`ModuleProvider.php` na raiz do módulo (`V2/Modules/Publicacoes/Noticias/ModuleProvider.php`)
declara título, ícone, rota e submódulos; o `V2ServiceProvider` varre `Modules/{Grupo}/{Módulo}`,
instancia o provider e **injeta o `group` a partir do nome da pasta** (`Publicacoes`). O `cms/`
usa um `ModuleDiscoveryService` com manifesto em cache. Precisamos disso **simples** e sem
consultar o banco no boot.

## Decisão

- Estrutura em **dois níveis**: `src/modules/{Grupo}/{Modulo}/`, onde `{Grupo}` é uma pasta de
  agrupamento (ex.: `Publicacoes`, `Institucional`, `Infraestrutura`) que define o grupo do módulo.
- **Um único arquivo na raiz do módulo:** `ModuleProvider.php`, que implementa
  `Core\Contracts\ModuleProvider` com:
  - `register(): void` — bindings do módulo (portas/singletons), opcional;
  - `config(): array` — título, descrição, ícone, `position`, rota e submódulos.
- O **Core** (`Core\Discovery\ModuleDiscovery`) varre `src/modules/*/*`, instancia cada
  `ModuleProvider`, chama `register()`/`config()` e **injeta `group = basename(pasta do grupo)`**.
- O Core carrega **migrations/views/rotas por convenção** da pasta do módulo — o módulo **não**
  precisa de um `ServiceProvider` próprio.
- O resultado vira o **manifesto em cache** (`bootstrap/cache/modules_manifest.php`), consumido
  pelo CMS (sidebar por grupos) e por permissões/flags — **zero consulta ao banco no boot**.
- Comando `core:cache-modules` regenera o manifesto (e no deploy).

## Justificativa (alternativas descartadas)

- **`ServiceProvider` completo por módulo** — descartada: boilerplate repetido; o provider
  central + convenção resolve.
- **Módulos "planos" (sem grupo)** — descartada: perde a organização visual do CMS que já
  funciona bem no `sucesu-cms`.
- **Config de módulo no banco consultada no boot** — descartada: custo por request; o manifesto
  em cache é O(1).
- **DTO/classe complexa para a config** — descartada: um **array** simples basta (ADR-001).

## Consequências

**Positivas**
- Um arquivo por módulo; agrupamento de graça pelo nome da pasta.
- Sidebar do CMS e permissões derivadas do mesmo manifesto; boot sem DB.
- Criar um módulo = criar a pasta + `ModuleProvider.php` (stub `make:module`).

**Custos / negativas**
- O Core precisa conhecer a convenção de pastas (uma vez).
- Manifesto precisa ser regenerado no deploy (`core:cache-modules`).

## Exemplo mínimo (comparativo)

```php
// ✅ src/modules/Publicacoes/Noticias/ModuleProvider.php — ÚNICO arquivo de declaração
declare(strict_types=1);
namespace Modules\Publicacoes\Noticias;

use Core\Contracts\ModuleProvider as ModuleProviderContract;

final class ModuleProvider implements ModuleProviderContract
{
    public function register(): void { /* bindings do módulo (opcional) */ }

    public function config(): array
    {
        return [
            'title'    => 'Notícias',
            'icon'     => 'newspaper',
            'position' => 2,
            'route'    => 'cms.noticia.index',
            'subModules' => [
                ['title' => 'Todas',    'route' => 'cms.noticia.index',    'position' => 1],
                ['title' => 'Categorias','route' => 'cms.noticia.categories','position' => 2],
            ],
            // 'group' => 'Publicacoes' injetado pelo Core (nome da pasta)
        ];
    }
}
```
```php
// ✅ Core: descoberta simples (sem banco no boot)
foreach (File::directories(base_path('src/modules')) as $groupPath) {
    $group = basename($groupPath);
    foreach (File::directories($groupPath) as $modulePath) {
        $class = 'Modules\\'.$group.'\\'.basename($modulePath).'\\ModuleProvider';
        if (! class_exists($class)) { continue; }
        $provider = new $class();
        $provider->register();
        $config = $provider->config();
        $config['group'] = $group;           // grupo vem da pasta
        $manifest[basename($modulePath)] = $config;
    }
}
```

## Verificação

- `core:cache-modules` gera o manifesto; um boot **não** consulta o banco (assert de queries = 0).
- Sidebar do CMS exibe os módulos **agrupados** por pasta; `position` ordena.
- `make create:module MODULE=Publicacoes/Demo` gera o módulo com `ModuleProvider.php` e passa nos gates.

## Referências

- `sucesu-cms/V2/Modules/Publicacoes/Noticias/ModuleProvider.php`;
  `sucesu-cms/V2/Core/Providers/V2ServiceProvider.php`;
  `wiki/plan/07-core-contratos-discovery.md`.
