# F2 — Núcleo DDD-lite (core · services · persistência)

> **Épico:** `F2` · **Depende de:** F1 · **Habilita:** F3 · **Planos:** 07, 08

## Objetivo

O núcleo invisível: contratos e descoberta de módulos, mensageria, e os **services headless** com
persistência segura. É o que torna qualquer módulo possível sem acoplamento.

## Estado de execução (revisão 2026/10/15)

> Fonte de progresso: `kd task graph --root epic_013336uf` (F2) e `epic_00zzn363` (telemetria).
> Confrontado com o código/testes reais.

| Issue/Task | `kd` | Código/testes | Estado |
|---|---|---|---|
| **F2.I1** (T1–T4) | `task_00q7k1vu` closed | `Contracts` + portas; `ModuleDiscovery` + manifesto; eventos nativos + RFC 7807; `arch-check` + fixtures | ✅ concluído |
| **F2.I2.T1** | `task_00npsp2i` closed | contrato no service + `ServiceProvider` de raiz + fakes + testes (`RichTextRendererTest`, `ContractsBindingTest`, `ServiceDiscoveryTest`) | ✅ |
| **F2.I2.T2** | `task_01nsxjan` closed | `src/services/{Media,SEO,RichText,Security}` com `ServiceProvider.php`; descoberta por convenção + catálogo; nenhum registro manual no `bootstrap/providers.php` | ✅ |
| **F2.I2.T3** | `task_01y8twea` closed | migrations base: `cms_users`, `core_roles`/`role_id`/seed suporte, `core_module_discovery_settings`, `core_audit_logs`; guardas `hasTable`/`IF NOT EXISTS`/`DROP TRIGGER IF EXISTS`; refs idempotentes | ✅ |
| **F2.I2.T4** | `task_01p9s8bz` closed | specs `10-migracoes-e-dados.md` e `12-postgres-18.md` publicadas; `tools/migration-check` ativo (fixtures + árvore real) | ✅ |
| **F2.I3.T1** | `task_00jktgjf` closed | `Access/Permission` (helper `allows`/`authorize`), `AccessGate`, `Role` + `core_roles.permissoes` jsonb validado, `cms_users.role_id`, `permissions` no `ModuleProvider`; testes | ✅ |
| **F2.I3.T2** | `task_00jxgd3w` closed | `Role` protegido + `SupportAccess`; grupo `Suporte` invisível (`grouped`) e inacessível (middleware `support.group` → 404) | ✅ |
| **F2.I3.T3** | `task_01qzlawe` closed | `ModuleActivation` (Valkey/TTL curto + DB, fail-open); guard `module.activation` em todas as áreas → 404; toggle invalida o cache na hora | ✅ |
| **F2.I3.T4** | `task_0137mv4o` closed | spec `11-acesso-e-ativacao.md`; testes de acesso/ativação (`PermissionTest`/`RoleProtectionTest`/`AccessGateTest`/`RolePermissionTest`/`SupportGroupAccessTest`/`ModuleActivationTest`) | ✅ |
| **F2.I4.T1** | (task de core) | spans `http.server`, `audit.persist`, `module.discovery` + `RecordingTelemetry` + `TelemetryPervasivenessTest` | ✅ core |
| **F2.I4.T2–T4** | `epic_00zzn363` | services, Actions de módulos e portas | ❌ pendente |

**Evidências executadas:** `make check` (arch/name/migration/vendor + selftest), `make check`
(91 testes/238 asserções · PHPStan max 0 erros · baseline 147 · optimize-check OK), `bash tools/docs-check.sh`
(6 specs).

**Fechamento F2.I2.T2 (2026/10/15):** `make check` OK (inclui fixture `service-impl-invalid`),
`make check` OK (**91 testes / 238 asserções**) cobrindo `ServiceDiscoveryTest`/`ContractsBindingTest`/
`RichTextRendererTest`, `make check` OK, `bash tools/docs-check.sh` OK (6 specs); `kd task close
--id task_01nsxjan --outcome success` (F2 → 6/12).

**Fechamento F2.I2.T3 (2026/10/15):** guardas de idempotência (`hasTable`/`CREATE INDEX IF NOT EXISTS`/
`DROP TRIGGER IF EXISTS`) nas migrations base e afins; **zero branching de driver** — migrations/DDL
são PostgreSQL puro (sem `$isPgsql`/`getDriverName`), `decision_00a4mc8g`; `MigrationIdempotencyTest`
(4 testes/12 asserções) cobre refs idempotentes, `migrate:fresh` sem seeder e `down()`→`backup_*`;
`make check` OK (**95 testes / 250 asserções**, PHPStan max 0 erros, baseline 147, gates/optimize
verdes); `kd task close --id task_01y8twea --outcome success` (F2 → 7/12).

**Fechamento F2.I2.T4 (2026/10/16):** `wiki/specs/10-migracoes-e-dados.md` (template canônico,
idempotência, refs×seeds, LGPD, gate) e `wiki/specs/12-postgres-18.md` (PG único alvo, tipos,
expand & contract, tuning) publicadas — `docs-check` **8 specs**; `tools/migration-check` ativo e
verde; `kd task close --id task_01p9s8bz --outcome success` (F2 → 8/12).

**ADR-033 emendado (2026/10/16):** Opção B — perfis em `core_roles.permissoes` **jsonb**
(`NOT NULL DEFAULT '[]'` + `CHECK jsonb_typeof = 'array'`), tabela `core_role_permissions`
descartada; helper `Core\Access\Permission` (`ACTIONS`/`isToken`/`allows`/`authorize`) com
**negação por padrão**; escrita validada em `Role::saving`. `make check` OK (**103 testes / 264
asserções**); `decision_00e0lzsj`.

**Fechamento F2.I3.T1 (2026/10/16):** vocabulário `{modulo}.{acao}` + `Gate` nativo com o helper
`Permission::allows/authorize` (negação por padrão); `core_roles.permissoes` jsonb validado na
escrita; um perfil por usuário (`cms_users.role_id`); `make check` OK (**103 testes / 264
asserções**); `kd task close --id task_00jktgjf --outcome success` (F2 → 9/12).

**Fechamento F2.I3.T2 (2026/10/16):** grupo `Suporte` **invisível** no manifesto
(`ModuleDiscoveryService::grouped(false)` por padrão) e **inacessível** no roteamento (middleware
`EnsureSupportGroupAccess`/alias `support.group` em todas as áreas → **404**, mesmo com perfil
máximo); abilities do grupo restritas à equipe. `SupportGroupAccessTest` (5 testes); `make check`
OK (**108 testes / 273 asserções**); `kd task close --id task_00jxgd3w --outcome success`
(F2 → 10/12).

**Fechamento F2.I3.T3 (2026/10/16, ADR-034 Aceito):** ativação em tempo real —
`ModuleActivation` (Valkey/TTL curto + fallback DB, fail-open); guard `CheckModuleActivation`
(alias `module.activation`) em **todas** as áreas → **404** para módulo inativo (portal/cms/api);
toggle `core:module:activate` grava o dado e **invalida a chave no mesmo instante** (sem
rebuild/reload); rotas de todos os módulos ficam registradas e o guard corta no runtime;
manifesto/navegação filtram inativos. `ModuleActivationTest` (6 testes) + e2e no dev
(`disable`→`enable`); `make check` OK (**114 testes / 281 asserções**); `kd task close --id
task_01qzlawe --outcome success` (F2 → 11/12).

**Fechamento F2.I3.T4 (2026/10/16):** spec `wiki/specs/11-acesso-e-ativacao.md` publicada
(`docs-check` 9 specs); testes de acesso/ativação consolidados (`AccessGateTest`,
`RoleProtectionTest`, `RolePermissionTest`, `PermissionTest`, `SupportGroupAccessTest`,
`ModuleActivationTest` — com rota real 200→404); `make check` OK (**115 testes / 283 asserções**);
issues `F2.I2` e `F2.I3` fechadas; `kd task close --id task_0137mv4o --outcome success` (**F2 12/12**).
**Fechamento F2.I4 (2026/10/16):** telemetria pervasiva concluída (`epic_00zzn363`, 4/4) —
`ServicesTelemetryTest`, `ModulesTelemetryTest`, `CoreTelemetryTest`; `make check` OK
(**127 testes / 309 asserções**). **F2 (12/12 + F2.I4) — DoD completo.**

## Issues e tasks

### `F2.I1` — Core: contratos, discovery e mensageria (plano 07)
- **T1** `core/Contracts` (Clock, IdGen, AuditWriter, WidgetRegistry, portas) — interfaces só onde há
  DI real (ADR-001).
- **T2** **Autodescoberta** de módulos (`modules/*/*`), `ModuleProvider.php` único, manifesto em
  cache (zero DB no boot) — ADR-014.
- **T3** Eventos/Listeners nativos (+ `ShouldQueue`); log de domínio; erros nativos/RFC 7807.
- **T4** Gate `make check`: `modules/*` não importa `modules/*` (falha de propósito).

### `F2.I2` — Services headless e persistência (plano 08, **revisado**)
> **Escopo reduzido por ADR-033/ADR-034 e diretriz de simplicidade:** só ficam no caminho crítico
> `Media`, `Seo`, `RichText` e `Audit`. Vão para o **backlog**: `Notifications`, `Search`, `Theme`,
> `LandingSections`, `FeedRSS`, `ExcelImporter`, `DigitalSignature`.
- **T1** Contrato **no service** (`Services\{Nome}\Contracts`) + `ServiceProvider.php` de raiz
  (`Core\Contracts\Service` — `contracts()` + `config()`) + fake + teste de contrato para `Media`,
  `Seo`, `RichText` e `Audit` (ADR-015). `Audit` mantém o contrato universal no Core
  (`Core\Contracts\AuditWriter`).
- **T2** `src/services/{Media,Seo,RichText}` no escopo mínimo; `Audit` permanece no Core com a porta
  `AuditWriter`. Descoberta por convenção (`ServiceDiscoveryService`) + catálogo acionável; nenhum
  service registrado à mão no `bootstrap/providers.php`.
- **T3** Migrações base (identidade, **perfis/permissões** — ADR-033 —, **ativação de módulos** —
  ADR-034 —, auditoria) no template do ADR-017; dados de referência **idempotentes**; seeds só `db:seed` (check/reset).
- **T4** Specs `10-migracoes-e-dados.md` e `12-postgres-18.md` publicadas; `tools/migration-check`.

### `F2.I3` — Acesso, ativação e suporte (ADR-033 · ADR-034)
- **T1** Vocabulário de permissões (`{modulo}.{ver|editar|aprovar|arquivar}`) + resolução via **`Gate`
  nativo** com o helper `Permission::authorize()/allows()` (negação por padrão); `core_roles.permissoes`
  (**jsonb validado na escrita** — ADR-033 emendado); **um perfil por usuário** (`cms_users.role_id`).
- **T2** Perfil `suporte` **protegido** (imutável/invisível/não atribuível) e grupo de módulos
  `Suporte` invisível/inacessível a quem não é da equipe — inclusive com perfil máximo.
- **T3** **Ativação em tempo real**: guard `CheckModuleActivation` (Valkey, TTL curto, **404** em
  portal/cms/api) + toggle exclusivo do suporte com **invalidação imediata** do cache + filtro do
  manifesto/sidebar.
- **T4** Testes de acesso/ativação + **spec `11-acesso-e-ativacao.md`** (ADR-033/034).

### `F2.I4` — Telemetria pervasiva (ADR-024 · spec 06) — ✅ (`epic_00zzn363`, 4/4)
> Telemetria é **capacidade de primeira classe** (não opcional); a instrumentação acompanha o
> código (core, services e módulos), não apenas a borda.
- **✅ T1** Core: spans em operações cross-cutting (`http.server`, `audit.persist`, `module.discovery`)
  + `RecordingTelemetry` para testes determinísticos.
- **✅ T2** Services: `media.ingest`/`media.store`/`media.retrieve`, `richtext.render`/`richtext.text`,
  `seo.metadata`, `security.hash`/`security.encrypt`/`security.decrypt` (`ServicesTelemetryTest`).
- **✅ T3** Módulos: `blueprint.saudacao`, `noticias.process_cover_media`, `demo.process_milestone_media`;
  convenção estrutural (`ModulesTelemetryTest` garante `Telemetry` em toda Action + span `{modulo}.{acao}`).
- **✅ T4** Portas/mensageria do Core (`core.port`/`core.listen`, `widget.register`/`widget.get`),
  `health.check` e o `TelemetryStatus` exposto no health da API (`CoreTelemetryTest`); atributos **sem PII**.
  `Clock`/`IdGen` excluídos (triviais/quentes) — documentado na spec 06.

## DoD (binário) — estado em 2026/10/16

- [x] Blueprint passa por todos os gates; import cruzado entre módulos **quebra o build**
  (`make check` + fixture `modules-invalid`).
- [x] Um módulo usa um Service **sem** importar a implementação (`Services\{Nome}\{Contracts,DTOs,Enums}\*`;
  `make check` + fixture `service-impl-invalid`).
- [x] `migrate` + `down()` preservam dados (`backup_*`); `migrate:fresh` sem seeder é utilizável.
  _(`MigrationIdempotencyTest`: `down()` renomeia para `backup_cms_users_*` preservando linhas;
  `CoreTestCase`/RefreshDatabase roda `migrate:fresh` sem seeder na suíte inteira.)_
- [x] Nenhum dado operacional depende de seeder; seeds = demo.
  _(perfil `suporte` vem de migration idempotente — `MigrationIdempotencyTest` prova 1 linha e
  inexistência de duplicação ao reexecutar a migration de referência.)_
- [x] Manifesto de módulos em cache; boot sem query (`bootstrap/cache/modules_manifest.php`, `core:cache-modules`).
- [x] Cada service expõe `ServiceProvider.php` (`contracts()`+`config()`) descoberto por convenção;
  `ServiceDiscoveryService::catalog()` o lista — nenhum registro manual no `bootstrap/providers.php`
  (`ServiceDiscoveryTest`).
- [x] Permissões por módulo (`ver/editar/aprovar/arquivar`) resolvidas pelo `Gate` nativo com o
  helper `Permission::authorize()`/`allows()` e **negação por padrão** (`AccessGateTest`/
  `RolePermissionTest`/`PermissionTest`); `core_roles.permissoes` jsonb validado na escrita.
- [x] Perfil e grupo `suporte` blindados por código (`Role::booted` + `SupportGroupAccessTest`:
  manifesto oculto e 404 no roteamento, mesmo com perfil máximo).
- [x] Desativar um módulo responde **404** imediatamente em portal/cms/api e invalida o cache
  (`ModuleActivation` + guard `module.activation`; `ModuleActivationTest`, inclusive rota real).
- [x] Telemetria pervasiva em core (`http.server`/`audit.persist`/`module.discovery`/`core.port`/
  `core.listen`/`widget.*`/`health.check`), services (`{service}.{operacao}`) e módulos (`{modulo}.{acao}`);
  `TELEMETRY_ENABLED` liga o exportador OTLP (fail-open), o status aparece no health e o Jaeger
  (`profile telemetry`) inspeciona o trace.
- [x] `make check` 2× verde.

## Evidência

`--note "boundaries red on fixture; services com contrato; migrate/down ok; acesso e flags ok"`.
