---
id: CORE-DATA
tipo: arquitetura
onda: 1
status: concluido
unidades: [CORE-DATA]
fontes:
  - V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php
  - V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php
  - V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php
  - V2/Core/Traits/ApiResponse.php
depende_de: [CORE-BOOT]
consumidores: [CORE-AUTH, CORE-CMS, MOD-CONEXOES-ASSOCIADOS, MOD-CONEXOES-PARCEIROSINSTITUCIONAIS, MOD-CONEXOES-UNIDADEREGIONAL, MOD-PUBLICACOES-NOTICIAS, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, SVC-SEARCH]
criticidade: alta
dono: CoreData
---

# Arquitetura: persistência UUID, migrações e envelope de API

> **Legado × alvo.** UUIDv4 via helper, prefixo `v2_` e rollback com `backup_*` são fatos
> do V2. No alvo: `IdGen` injetado com UUIDv7, schema com nomes canônicos, migrations
> idempotentes e `down()` sem destruir dado (ADR-007/017).
> Ver [`pilha-alvo-public-v7.md`](../09_reimplementacao/pilha-alvo-public-v7.md) §3–4.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | toda tabela V2 nasce com PK `uuid`; migrations são atômicas e reversíveis por renomeação; respostas de API dos controladores que usam `ApiResponse` têm envelope fixo `{ok,data,message,error}` |
| Entrada | `php artisan migrate` / `migrate:rollback`; helper PHP `v2_uuid()`; função SQL `v2_gen_uuid()`; chamadas `$this->success()` / `$this->error()` |
| Saída | tabelas e índices no schema `public`; linhas com `id` UUID; `activity_log`/`Log` de migração; `JsonResponse` |
| Efeitos colaterais | `SET lock_timeout` na sessão; `Log::info` por migração; `Log::alert` no rollback; `Log::error` antes de relançar a exceção |
| Particularidade central | o README promete UUIDv7, mas os dois geradores produzem UUID **v4**; a função SQL monta 18 bytes e nunca é usada como gerador principal |
| Como se integra | migrations carregadas por `V2ServiceProvider` (Core, módulos e serviços); IDs atribuídos em hooks `creating` dos models; trait `V2\Core\Traits\ApiResponse` |
| Se removido, quebra | todas as FKs reais (`users.v2_role_id`, `v2_news.unidade_regional_id`, assinatura digital), os seeds de migration e os cinco controladores que herdam o envelope |
| Superfície pública | `v2_uuid()` (helper global); `v2_gen_uuid()` (função SQL); `ApiResponse::success()` / `ApiResponse::error()` |

## 1. Efeito

O que passa a valer, observavelmente:

1. `v2_uuid()` devolve uma string `8-4-4-4-12` com versão 4 e variante RFC 4122 — `V2/Core/Helpers/helpers.php:61-69`. É o gerador efetivo de IDs: chamado nos hooks `creating` dos models (`V2/Modules/Publicacoes/Noticias/Models/NewsCategory.php:32-39`, `V2/Core/Models/ConexaoTag.php:50-55`, `V2/Modules/Conexoes/UnidadeRegional/Models/UnidadeRegional.php:104-108`) e em inserts de migration/seeder.
2. `v2_gen_uuid()` existe no banco como função PL/pgSQL e é referenciada como `DEFAULT` em quatro tabelas — `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000000_create_v2_structural_portal_blocks_table.php:19`, `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_06_000000_create_v2_sle_assets_table.php:19`, `V2/Services/Search/Migrations/2026_06_14_000000_create_search_indexes_tables.php:29,44`. Nenhum desses `DEFAULT` é exercitado pelos caminhos de escrita (ver §3, RN-CORE-DATA-02).
3. A maioria das migrations do V2 executa `SET lock_timeout = '3s'` antes do DDL (**79 de 87**) e envolve o trabalho em `DB::beginTransaction()`/`DB::commit()` (**80 de 87**); falha → `DB::rollBack()`, `Log::error(...)`, `throw $e`. As exceções são os dois no-ops (`2024_01_01_000012`, `2024_01_01_000014`), `2024_01_01_000019` (reset de tags), `2026_07_31_000000` (rename de parceiros), `2026_07_22_000001` (Homepage), `2026_08_06_000001` (FK de notícias), `2026_09_03_000001` (flag) e `2026_07_24_000001` (fix do overlay).
4. O rollback das migrations de tabela **não apaga dados**: renomeia para `backup_<tabela>_<YmdHis>` (ex.: `V2/Modules/Conexoes/Associados/Migrations/2024_01_01_000004_create_v2_associado_instituicoes_table.php:56-66`). Exceções: `down()` da migração de tags core usa `Schema::dropIfExists` (`V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:70-76`) e a migração de cópia de tags é irreversível por desenho (`:56-61`).
5. `ApiResponse::success()` devolve HTTP 200 (ou o informado) com `{"ok":true,"data":...,"message":...,"error":null}`; `ApiResponse::error()` devolve `{"ok":false,"data":null,"message":...,"error":{"type","title","status","detail","instance"}}` — `V2/Core/Traits/ApiResponse.php:10-34`.

O que **não** acontece:

- Não há `uuid-ossp` nem `pgcrypto`: nenhuma migration cria extensão; `v2_gen_uuid()` só usa `random()`.
- Não há chatter de schema: as migrations não geram `down()` destrutivo por padrão, nem `truncate` fora de `2024_01_01_000019_reset_tags_to_default_categories.php:24-25`.
- `ApiResponse` não define media type próprio (usa `application/json` do `response()->json`), não versiona o payload e não inclui `trace`; `instance` é `request()->path()` sem barra inicial (`V2/Core/Traits/ApiResponse.php:31`).
- Migrations de módulo não são registradas em `database/migrations`: são descobertas por `loadMigrationsFrom` (`V2/Core/Providers/V2ServiceProvider.php:114-118`, `:178-182`).

## 2. Abordagem

### 2.1 Mecânica: dois geradores, um só em uso

```mermaid
flowchart LR
    A[Model::creating] --> B[v2_uuid PHP]
    B --> C[random_bytes 16]
    C --> D[byte6 &0x0f |0x40]
    D --> E[byte8 &0x3f |0x80]
    E --> F[bin2hex + vsprintf 8-4-4-4-12]
    G[INSERT sem id] --> H[DEFAULT v2_gen_uuid]
    H --> I[random x6 + lpad + decode hex]
    I --> J[bytea 18 bytes]
    J --> K["bytes::uuid"]
```

- **Geração em PHP** (caminho real): `random_bytes(16)`, força `0100` no nibble alto do byte 6 e `10` nos dois bits altos do byte 8, formata com `vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))` — `V2/Core/Helpers/helpers.php:63-68`. O helper é carregado no `register()` do provider (`V2/Core/Providers/V2ServiceProvider.php:347-351`), portanto está disponível **antes** de qualquer migration ou model — inclusive para migrations que chamam `v2_uuid()` no `up()` (`V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:50`).
- **Geração em SQL** (caminho de fallback): seis chamadas `random()` concatenadas em hexadecimal e decodificadas. O bit de versão é forçado por `((random()*65535)::int & 4095) | 16384` (`V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php:27`): `16384 = 0x4000` põe `4` no nibble de versão. O bit de variante é forçado por `& 16383 | 32768` (`:28`): `32768 = 0x8000` põe `10` no topo do byte 8. Nos offsets 6 e 8 de um buffer de 16 bytes, isso é exatamente UUID v4.
- **Divergência README × código.** `V2/Modules/README.md:13` anuncia "PG17 + UUIDv7" e o comentário de boot repete "UUIDv7" (`V2/Core/Providers/V2ServiceProvider.php:78`). O código não gera v7: não há timestamp nos 48 bits altos, e a versão é 4. Consequência prática: `id` **não** ordena cronologicamente, então `ORDER BY id` não substitui `created_at` e a localidade de índice é a de um UUID aleatório.
- **Tamanho real do buffer SQL.** As larguras dos `lpad` são 8+4+4+4+8+8 = 36 dígitos hexadecimais = **18 bytes**, não 16. O layout RFC 4122 termina com um nó de 6 bytes; aqui os dois últimos blocos somam 8 bytes. O `RETURN bytes::uuid` (`:33`) recebe, portanto, 18 bytes — ver LG-CORE-DATA-01.

### 2.2 Trechos de código pivô

```php
// V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php:19-36 — a função SQL:
// versão 4 em (…&4095)|16384 e variante 10 em (…&16383)|32768; concatenar 8+4+4+4+8+8
// dígitos hex resulta em 18 bytes, e o retorno converte bytea->uuid.
DB::statement("
    CREATE OR REPLACE FUNCTION v2_gen_uuid() RETURNS uuid AS \$\$
    DECLARE
        bytes bytea;
    BEGIN
        bytes = decode(
            lpad(to_hex((random() * 2147483647)::int), 8, '0') ||
            lpad(to_hex((random() * 65535)::int), 4, '0') ||
            lpad(to_hex(((random() * 65535)::int & 4095) | 16384), 4, '0') ||
            lpad(to_hex(((random() * 65535)::int & 16383) | 32768), 4, '0') ||
            lpad(to_hex((random() * 4294967295)::bigint), 8, '0') ||
            lpad(to_hex((random() * 4294967295)::bigint), 8, '0'),
            'hex'
        );
        RETURN bytes::uuid;
    END;
    \$\$ LANGUAGE plpgsql;
");
```

**O que mostra.** O único ponto do sistema que materializa UUID no banco. `random()` é o PRNG do PostgreSQL (não criptográfico), diferente de `random_bytes()` no PHP; isso é aceitável para PK, mas torna o valor previsível para quem observa a sequência do PRNG — não use `v2_gen_uuid()` para token/segredo. A migração em si só cria a função; `[I]` o corpo PL/pgSQL resolve tipos na primeira execução, então o `CREATE` passa mesmo que o cast final seja inválido (justificativa: o projeto não tem teste que chame a função e as migrations de 2026 não falham).

```php
// V2/Core/Helpers/helpers.php:61-69 — o gerador efetivo de IDs do V2 (v4, 16 bytes).
function v2_uuid()
{
    $data = random_bytes(16);
    // Versão 4 (0100 em binário nos 4 bits altos do 7º byte)
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40);
    // Variante RFC 4122 (10xx nos 2 bits altos do 9º byte)
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
```

**O que mostra.** Versão e variante idênticas às da função SQL, mas sobre 16 bytes exatos e com fonte criptográfica. É a prova de que o contrato de ID do sistema é "UUID v4 em string canônica", não v7.

```php
// V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:18-35 — tabela
// e pivô: PK uuid sem default, slug único com limite 100, pivô com PK composta e
// índices nomeados à mão (idx_v2_par_pivot_*), sem FK.
Schema::create('v2_conexao_tags', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name', 100);
    $table->string('slug', 100)->unique();
    $table->boolean('is_reserved')->default(false);
    $table->timestamps();
});

Schema::create('v2_parceiro_tag_pivot', function (Blueprint $table) {
    $table->uuid('parceiro_id');
    $table->uuid('tag_id');
    $table->boolean('is_primary')->default(false);
    $table->timestamps();

    $table->primary(['parceiro_id', 'tag_id'], 'pk_v2_par_tag_pivot');
    $table->index('parceiro_id', 'idx_v2_par_pivot_parceiro');
    $table->index('tag_id', 'idx_v2_par_pivot_tag');
});
```

**O que mostra.** A convenção canônica de tabela core: `v2_` + `uuid` PK + `timestamps()`, nenhum `id` autoincremental, nenhuma FK declarada. O nome do índice/PK é explícito porque o rollback posterior pode precisar derrubá-lo por nome (ver o `down()` de `2024_01_01_000004`).

```php
// V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:48-56 — seed
// dentro da migration: a tag reservada "Geral" é criada na mesma transação do DDL.
DB::table('v2_conexao_tags')->insert([
    'id'         => v2_uuid(),
    'name'       => 'Geral',
    'slug'       => 'geral',
    'is_reserved' => true,
    'created_at' => \Carbon\Carbon::now(),
    'updated_at' => \Carbon\Carbon::now(),
]);
```

**O que mostra.** Dado inicial obrigatório sem seeder dedicado: quem reimplementar sem semear `slug=geral` quebra o `destroy()` do CMS, que reatribui tags para "Geral" (`V2/Core/Controllers/ConexaoTagCmsController.php:82-96`). Não há guarda de idempotência: reexecutar o `up()` viola o índice único de `slug`.

```php
// V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php:19-41 —
// cópia de dados legados por slug; $tagIdMap é preenchido e nunca lido; o pivô legado
// não é migrado.
$oldTags = DB::table('v2_associado_tags')->get();
$tagIdMap = [];

foreach ($oldTags as $oldTag) {
    $newId = v2_uuid();
    $tagIdMap[$oldTag->id] = $newId;

    $existing = DB::table('v2_conexao_tags')->where('slug', $oldTag->slug)->first();
    if ($existing) {
        $tagIdMap[$oldTag->id] = $existing->id;
        continue;
    }

    DB::table('v2_conexao_tags')->insert([
        'id'         => $newId,
        'name'       => $oldTag->name,
        'slug'       => $oldTag->slug,
        'is_reserved' => false,
        'created_at' => $oldTag->created_at ?? now(),
        'updated_at' => $oldTag->updated_at ?? now(),
    ]);
}
```

**O que mostra.** Migração de dados idempotente por `slug` (reexecutar não duplica) e **não** idempotente por identidade (o ID muda a cada execução, mas o `existing` evita nova linha). Depende de `v2_associado_tags` existir (criada em `2024_01_01_000016`) e de `v2_conexao_tags` já existir (migração anterior, `2026_08_18_000001`). O comentário `:43-45` confirma que os vínculos (`v2_associado_tag_pivot`) **não** são copiados: o sistema convive com dois catálogos de tags em paralelo.

```php
// V2/Modules/Conexoes/Associados/Migrations/2024_01_01_000004_create_v2_associado_instituicoes_table.php:56-66 —
// o padrão de rollback preservacionista: derruba índices e renomeia a tabela para backup_*.
$backupTable = 'backup_v2_associado_instituicoes_' . date('Y_m_d_His');

if (Schema::hasTable('v2_associado_instituicoes')) {
    Schema::table('v2_associado_instituicoes', function (Blueprint $table) {
        $table->dropIndex('idx_v2_asc_inst_active');
        $table->dropIndex('idx_v2_asc_inst_sort');
    });

    Schema::rename('v2_associado_instituicoes', $backupTable);

    Log::alert("[Migration] Tabela 'v2_associado_instituicoes' preservada como '{$backupTable}'.");
}
```

**O que mostra.** O `down()` preferido do corpus: nunca `DROP`. O efeito colateral é acumular `backup_*` no schema; não há rotina de limpeza. O nome do backup usa o relógio, então dois rollbacks no mesmo segundo colidem (o segundo `Schema::rename` falha e a transação reverte).

```php
// V2/Core/Traits/ApiResponse.php:10-34 — envelope único para sucesso e erro.
protected function success($data = null, string $message = 'Operação realizada com sucesso.', int $statusCode = 200): JsonResponse
{
    return response()->json([
        'ok'      => true,
        'data'    => $data,
        'message' => $message,
        'error'   => null,
    ], $statusCode);
}

protected function error(string $errorTitle, string $detail = '', int $statusCode = 400, string $type = 'about:blank'): JsonResponse
{
    return response()->json([
        'ok'      => false,
        'data'    => null,
        'message' => $detail ?: $errorTitle,
        'error'   => [
            'type'     => $type,
            'title'    => $errorTitle,
            'status'   => $statusCode,
            'detail'   => $detail,
            'instance' => request()->path(),
        ],
    ], $statusCode);
}
```

**O que mostra.** `message` é ambíguo por desenho: em erro, carrega `detail` quando existe e cai em `title` quando vazio. `error.status` repete o HTTP status no corpo, e `type` é um rótulo curto (`not_found`, `forbidden`, `invalid_token`) ou `about:blank`.

### 2.3 Decisões e restrições

- **UUID em vez de bigint** remove a coordenação de sequência entre ambientes, mas custa 16 bytes por PK e impede ordenação por inserção. O README chama isso de "UUIDv7" justamente para vender ordenação temporal — que o código não entrega.
- **ID gerado no PHP, não no banco.** O `DEFAULT v2_gen_uuid()` existe só para inserts fora do Eloquent. Como todo `INSERT` relevante passa `id` explícito (`V2/Services/Search/Services/PublicIndexerService.php:25-30`, `V2/Services/Search/Services/CMSIndexerService.php:25-29`), o defeito da função SQL é latente, não sintoma.
- **DDL transacional** funciona porque o alvo é PostgreSQL; o `lock_timeout` de 3 s evita que um `ALTER TABLE` bloqueie a aplicação indefinidamente. O `SET` é de sessão e **vaza** para os statements seguintes na mesma conexão — o migrator reutiliza a conexão, então todas as migrations seguintes herdam 3 s.
- **Rollback por renomeação** é a decisão conservadora do projeto: `migrate:rollback` não perde dados, mas deixa lixo. Um `migrate` subsequente recria a tabela original e as duas convivem.
- **Migrations podem conter dados**: seed de tag "Geral", categorias padrão, configs, flag de módulo, papel regional, página básica. Isso acopla `migrate` a regras de negócio — ver §6.
- **Migrations podem depender de código de aplicação**, inclusive models (ex.: `V2/Modules/Institucional/SobreNos/Migrations/2026_09_01_000001_normalize_sobre_nos_section_ids.php:22-29` usa `LayoutBaseConfig` e `SobreNosSectionNormalizer`). Reimplementar exige que a migration funcione contra o model **daquele momento**, não o atual.

### 2.4 Convenções de schema (o que toda tabela nova deve respeitar)

| Convenção | Regra observada | Exceções / evidência |
|---|---|---|
| Prefixo | `v2_` em 49 das 57 tabelas criadas | `users`, `persistences`, `activations`, `reminders`, `throttle`, `role_users`, `roles`, `activity_log` |
| PK | `uuid('id')->primary()` nas tabelas V2; `$incrementing=false` e `$keyType='string'` no model | `users` e Sentinel usam `increments('id')` (int) — `V2/Core/Migrations/2026_06_01_000002_create_users_table.php:21`; `V2/Core/Migrations/2026_06_01_000003_create_sentinel_tables.php:21` |
| Geração de ID | PHP `v2_uuid()` no hook `creating` | `DEFAULT v2_gen_uuid()` em 4 tabelas (RN-CORE-DATA-02) |
| Timestamps | `$table->timestamps()` e `created_at`/`updated_at` preenchidos pelo Eloquent ou por `now()` no insert cru | pivôs registram a data do vínculo no próprio pivô |
| Soft delete | `softDeletes()` em 32 migrations; o model **precisa** usar o trait | `v2_news_categories` tem `deleted_at` mas o model não usa `SoftDeletes` (`V2/Modules/Publicacoes/Noticias/Models/NewsCategory.php:7`) |
| FKs | apenas 8 constraints; o resto é referência lógica `*_id` | `2026_07_02_000001` e `2026_08_06_000001:11-17` |
| Índices | nome explícito `idx_<tabela>_<coluna>` / `pk_<...>` | quando não nomeado, vale o default `<tabela>_<coluna>_index` (usado nos `dropIndex` dos `down()`) |
| Pivôs | `<entidade>_<entidade>_pivot`, PK composta, sem `id`, `is_primary` opcional | `v2_parceiro_tag_pivot`, `v2_unidade_regional_tag_pivot`, `v2_news_tag_pivot`, `v2_associado_tag_pivot` |
| JSON | `jsonb` para `metadata`/payload flexível | `v2_news.metadata`, `v2_unidade_regionais.metadata` |
| Tipo de UUID em coluna | `uuid` no schema novo; `string(36)` em tabelas antigas, com migração de conversão posterior | `v2_news_tags`/`v2_news_categories` nascem `string(36)` e viram `uuid` em `2026_07_22_000005:17-31` |

## 3. Particularidades

### 3.1 Regras não óbvias

| ID | Regra | Evidência |
|---|---|---|
| RN-CORE-DATA-01 | O UUID do sistema é **v4**, não v7; `id` não é ordenável por tempo | `V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php:27-28`; `V2/Modules/README.md:13` |
| RN-CORE-DATA-02 | `v2_gen_uuid()` monta **18 bytes** (8+4+4+4+8+8 hex) e tenta `bytea::uuid`; é um gerador de fallback sem uso real | `V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php:24-33` |
| RN-CORE-DATA-03 | O slug da tag migrada é a chave natural da cópia: colisão com "geral" mantém a linha existente e descarta nome/`created_at` do legado | `V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php:27-31` |
| RN-CORE-DATA-04 | O `down()` da migração de tags é **no-op**: não remove o que copiou nem restaura o estado anterior | `V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php:56-61` |
| RN-CORE-DATA-05 | `ApiResponse::error()` sempre inclui `instance` = caminho da requisição, mesmo em erro 500 lógico | `V2/Core/Traits/ApiResponse.php:31` |
| RN-CORE-DATA-06 | `success()` usa `message` com default em português; `error()` nunca devolve `detail` fora de `error.detail` além do fallback em `message` | `V2/Core/Traits/ApiResponse.php:10,20,25` |
| RN-CORE-DATA-07 | Migrations usam `DB::beginTransaction()`/`commit()`/`rollBack()` explícitos; não há `$this->withinTransaction` (inexistente no Laravel 5.5) nem retry | `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:14-15` |
| RN-CORE-DATA-08 | `v2_conexao_tags` não tem `deleted_at`; exclusão é física e bloqueada só quando `is_reserved = true` | `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:18-24`; `V2/Core/Controllers/ConexaoTagCmsController.php:74-76` |

### 3.2 Casos-limite

- **Execução concorrente do `migrate`:** duas instâncias do migrator disputam a tabela de controle; o `lock_timeout` de 3 s faz a segunda falhar rápido em vez de esperar. Não há lock distribuído.
- **Partial failure em `up()`:** como o DDL é transacional, tabela + seed + pivô entram ou saem juntos. Já o `2026_08_18_000002` copia N linhas em uma transação; em volume alto, segura locks de linha até o commit.
- **Dados legados com `id` não-UUID:** `2026_07_22_000005` converte `varchar(36) → uuid USING id::uuid` (`V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000005_fix_tags_and_categories_uuid.php:17-31`). Qualquer linha com string fora do formato UUID aborta a migration inteira, e o `catch` relança.
- **`down()` de `2026_07_22_000004`** reduz as colunas do pivô para `varchar(36)` (`:37-40`), ou seja, a reversão **não** restaura o tipo original de fato, apenas um tipo compatível.
- **Reexecução de `2026_08_18_000001`** (histórico perdido) falha em `Schema::create` ou no unique de `slug=geral`; não é idempotente.
- **Tag reservada excluída por SQL direto:** o CMS reatribui vínculos para `geral`, mas se `geral` não existir, apenas faz `detach()` (`V2/Core/Controllers/ConexaoTagCmsController.php:82-100`) — vínculos são perdidos sem erro.
- **Uso de `ApiResponse` em rota não-HTTP** (comando artisan) faria `request()->path()` resolver o request de console; não há guarda.

### 3.3 Divergências e código morto

- `V2/Modules/README.md:13` ("UUIDv7") e `V2/Core/Providers/V2ServiceProvider.php:78` ("UUIDv7") vs. versão 4 real. `V2/README.md` e `V2/Core/README.md` **não** mencionam UUID; a única promessa de v7 está no README de módulos e no comentário do provider.
- `$tagIdMap` em `2026_08_18_000002` é escrito (`:24, :29`) e nunca lido — resíduo de uma migração de pivô que não foi concluída (`:43-45`).
- `v2_conexao_tags` e os dois pivôs foram criados para unificar tags de Parceiros e Unidades Regionais, mas **nenhum código anexa tags** a esses pivôs: só existem `detach()`/`syncWithoutDetaching()` no `destroy()` (`V2/Core/Controllers/ConexaoTagCmsController.php:85-96`).
- O seed de "Geral" usa `\Carbon\Carbon::now()` enquanto o restante do projeto usa `now()`; é o mesmo instante, mas revela que a migração foi escrita fora do padrão do arquivo vizinho.

## 4. Integrabilidade

### 4.1 Contrato para migrations de módulos

Toda migration nova de módulo/serviço deve:

1. Viver em `V2/Modules/<Grupo>/<Modulo>/Migrations/` ou `V2/Services/<Servico>/Migrations/` — o provider carrega esses diretórios automaticamente (`V2/Core/Providers/V2ServiceProvider.php:114-118`, `:178-182`).
2. Ter nome `YYYY_MM_DD_HHMMSS_verbo_objeto.php` e classe `StudlyCase` **única no corpus** (o migrator indexa por nome de arquivo e resolve por classe; duplicata de classe quebra o autoload).
3. Abrir transação, aplicar `SET lock_timeout = '3s'`, capturar `\Exception`, logar com prefixo `[Migration]` e relançar.
4. Guardar `Schema::hasTable` / `Schema::hasColumn` quando a operação for `ALTER` (idempotência defensiva).
5. Implementar `down()` preservacionista (`Schema::rename` para `backup_*`) ou no-op explícito; `Schema::dropIfExists` apenas para tabelas efêmeras/pivôs criados na própria migration.
6. Não assumir FKs: o corpus declara apenas **8 FKs** em 87 migrations (`$table->foreign(`), enquanto há dezenas de colunas `*_id` cuja integridade é só convenção de aplicação.
7. Não usar `v2_gen_uuid()` como default de coluna nova — use `v2_uuid()` no model ou nenhum default.

### 4.2 Como adicionar tabela

```php
// padrão mínimo aceitável para uma tabela de módulo
DB::beginTransaction();
DB::statement("SET lock_timeout = '3s';");
try {
    Schema::create('v2_minha_tabela', function (Blueprint $table) {
        $table->uuid('id')->primary();
        $table->string('slug', 255)->unique();
        $table->jsonb('metadata')->nullable();
        $table->timestamps();
        $table->softDeletes();
    });
    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    Log::error("[Migration] ERRO ao criar v2_minha_tabela: " . $e->getMessage());
    throw $e;
}
```

### 4.3 Anti-uso

- **`DB::table('v2_conexao_tags')->insert([...])` sem `id`** esperando o `DEFAULT`: a coluna não tem default; o insert falha por NOT NULL. O `DEFAULT`/função só existe em tabelas específicas.
- **Confiar em `down()` da migração de tags** para desfazer a cópia: ele só escreve log. Para reverter de verdade é preciso apagar as linhas por `slug` manualmente.
- **Reordenar migrations por timestamp** para "corrigir" dependências: o migrator ordena por nome de arquivo globalmente, então renomear um arquivo muda a ordem de **tudo** o que compartilha a data e pode rodar uma migração antes da tabela que ela altera (ex.: `2026_08_06_000001` referencia `v2_unidade_regionais`, criada em `2026_07_23_000001`).
- **Adicionar FK para tabela de módulo que ainda não existe** na ordem cronológica: a migration roda antes e falha; o padrão do projeto é FK condicionada a `Schema::hasTable` (`V2/Modules/Support/SupportConfigs/Migrations/2026_07_02_000001_add_v2_foreign_keys.php:16-50`).

### 4.4 Receitas de uso

```php
// 1. Gerar ID em qualquer model novo
protected static function boot()
{
    parent::boot();
    static::creating(function ($model) {
        if (empty($model->id)) {
            $model->id = v2_uuid();
        }
    });
}
```

```php
// 2. Devolver envelope padrão em controlador de API
use V2\Core\Traits\ApiResponse;

class MinhaApiController extends Controller
{
    use ApiResponse;

    public function store(Request $request)
    {
        if (!$request->filled('title')) {
            return $this->error('Título obrigatório', 'O campo title é obrigatório.', 422, 'validation_error');
        }

        return $this->success(['id' => $id], 'Criado com sucesso.', 201);
    }
}
```

```php
// 3. Migração de coluna idempotente
DB::beginTransaction();
DB::statement("SET lock_timeout = '3s';");
try {
    if (Schema::hasTable('v2_minha_tabela') && !Schema::hasColumn('v2_minha_tabela', 'novo_campo')) {
        Schema::table('v2_minha_tabela', function (Blueprint $table) {
            $table->boolean('novo_campo')->default(false);
        });
    }
    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    Log::error("[Migration] ERRO ao adicionar novo_campo: " . $e->getMessage());
    throw $e;
}
```

## 5. Inventário

| Arquivo | Papel | Efeito | Fonte |
|---|---|---|---|
| `V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php` | DDL | cria/substitui `v2_gen_uuid()`; `down()` faz `DROP FUNCTION IF EXISTS` | `:19-36`, `:53` |
| `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php` | DDL + seed | cria `v2_conexao_tags`, `v2_parceiro_tag_pivot`, `v2_unidade_regional_tag_pivot`; insere `geral` reservada | `:18-56` |
| `V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php` | DML | copia `v2_associado_tags` para `v2_conexao_tags` por slug; não migra pivôs; `down()` no-op | `:19-45`, `:56-61` |
| `V2/Core/Traits/ApiResponse.php` | trait | envelope JSON de sucesso/erro para 5 controladores | `:8-35` |

## 6. Riscos de reimplementação

1. **Trocar v4 por v7** "porque o README diz": muda a distribuição dos IDs, a ordenação por `id` e qualquer teste que compare formato. Se a decisão for v7, ela é uma **mudança de contrato**, não uma correção.
2. **Usar `gen_random_uuid()` como substituto direto**: funcionalmente equivalente ao PHP (v4), mas quebra a premissa de que o ID é atribuído antes do INSERT pelo model — fluxos que dependem de `$model->id` preenchido antes de `save()` deixam de funcionar.
3. **Implementar `down()` destrutivo** nas migrations de tabela: perde o histórico de dados que o projeto deliberadamente preserva.
4. **Migrations de dados sem chave natural**: a cópia de tags usa `slug`; qualquer reimplementação que copie por `id` vai duplicar catálogos quando o banco legado e o novo divergirem.
5. **`ApiResponse` como contrato global**: só cinco controladores o usam (`V2/Core/Controllers/SearchController.php:16`, `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalCmsController.php:25`, `.../UnidadeRegionalEmpresaApiController.php:20`, `.../UnidadeRegionalNewsApiController.php:23`, `V2/Services/MailService/Controllers/MailApiController.php:13`). O CMS responde JSON ad-hoc (`['success' => false, 'message' => ...]` em `V2/Core/Controllers/ConexaoTagCmsController.php:47,75`); não unifique sem migrar os consumidores.

## 7. Lacunas

- **LG-CORE-DATA-01** — Comportamento real de `RETURN bytes::uuid` com 18 bytes. Não há PostgreSQL disponível no ambiente de levantamento e o projeto não tem teste que chame `v2_gen_uuid()`. Impacto: se o cast falhar, qualquer INSERT que dependa do `DEFAULT` (4 tabelas: `v2_structural_portal_contents`, `v2_sle_assets`, `v2_cms_links_index`, `v2_public_search_index`) falha em runtime; hoje nenhum caminho depende disso. Onde confirmar: `SELECT v2_gen_uuid();` no banco de homologação.
- **LG-CORE-DATA-02** — Se `SG2048`/`random()` do PG17 muda de algoritmo entre versões, a distribuição dos bits gerados muda; não há teste de formato. Onde procurar: `pg_settings`/documentação da versão do servidor de produção.
- **LG-CORE-DATA-03** — Motivo histórico da divergência "UUIDv7" no README; não há issue/commit acessível em `.seeds/issues.jsonl` sobre a função SQL. Onde procurar: histórico git do arquivo (não disponível neste levantamento).

## 8. Checklist de paridade

- [ ] `PC-CORE-DATA-01` — `v2_uuid()` retorna string de 36 caracteres, versão `4` no 13º caractere e variante em `[89ab]` no 17º; 1 milhão de chamadas sem colisão. Fonte: `V2/Core/Helpers/helpers.php:63-68`.
- [ ] `PC-CORE-DATA-02` — `SELECT v2_gen_uuid()` executa sem erro e devolve UUID com nibble de versão `4` (ou documenta-se a falha do cast). Fonte: `V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php:27-33`.
- [ ] `PC-CORE-DATA-03` — `migrate:fresh` cria as 3 tabelas de tags e exatamente uma linha `slug='geral', is_reserved=true`. Fonte: `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:18-56`.
- [ ] `PC-CORE-DATA-04` — Com 3 tags legadas e 1 slug já existente, a migração de cópia produz 2 novas linhas e mantém a existente; `v2_associado_tag_pivot` permanece intacto. Fonte: `V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php:19-45`.
- [ ] `PC-CORE-DATA-05` — `migrate:rollback` da migração de cópia não altera `v2_conexao_tags`. Fonte: `:56-61`.
- [ ] `PC-CORE-DATA-06` — `success(['a'=>1])` → HTTP 200, corpo `{"ok":true,"data":{"a":1},"message":"Operação realizada com sucesso.","error":null}`. Fonte: `V2/Core/Traits/ApiResponse.php:10-18`.
- [ ] `PC-CORE-DATA-07` — `error('not_found','X.',404,'not_found')` → `message="X."`, `error.title="not_found"`, `error.detail="X."`, `error.instance` sem barra inicial. Fonte: `V2/Core/Traits/ApiResponse.php:20-34`.
- [ ] `PC-CORE-DATA-08` — Toda migration do corpus executa `SET lock_timeout = '3s'` e envolve o DDL em transação; script de verificação percorre os 87 arquivos. Fonte: `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:14-15`.

## 9. Evidências consultadas

- `V2/Core/Migrations/2026_06_01_000001_create_v2_uuid_function.php` (1–62).
- `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php` (1–85).
- `V2/Core/Migrations/2026_08_18_000002_migrate_associado_tags_to_conexao_tags.php` (1–62).
- `V2/Core/Traits/ApiResponse.php` (1–35).
- `V2/Core/Helpers/helpers.php` (54–70); `V2/Core/Providers/V2ServiceProvider.php` (78–82, 114–118, 178–182, 342–351).
- `V2/Core/Models/ConexaoTag.php` (1–52); `V2/Core/Controllers/ConexaoTagCmsController.php` (40–100).
- `V2/Services/Search/Services/PublicIndexerService.php` (21–45); `V2/Services/Search/Services/CMSIndexerService.php` (21–40).
- `V2/Modules/README.md` (13); `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000005_fix_tags_and_categories_uuid.php` (12–39); `V2/Modules/Support/SupportConfigs/Migrations/2026_07_02_000001_add_v2_foreign_keys.php` (14–50).
- `V2/Modules/Institucional/SobreNos/Migrations/2026_09_01_000001_normalize_sobre_nos_section_ids.php` (20–48).

## 10. Checklist de extração (A–M)

| Item | Resposta |
|---|---|
| A1 | PK `uuid` universal, migrations atômicas e rollback preservacionista; envelope de API fixo nos controladores que usam o trait. |
| A2 | `php artisan migrate`/`rollback`; `v2_uuid()`; `v2_gen_uuid()`; `$this->success()`/`$this->error()`. |
| A3 | Tabelas/índices; linhas de seed; `JsonResponse` com `ok/data/message/error`. |
| A4 | `Log::info` por migração, `Log::alert` no rollback, `Log::error` + rethrow; `SET lock_timeout` vaza na sessão. |
| A5 | `v2_uuid()` → UUID v4 válido; migration sem guarda em tabela existente → exceção relançada; `error()` → HTTP indicado. |
| A6 | Não há extensão de banco, não há geração de ID por sequência, não há `DROP` no rollback padrão, não há retry de migration. |
| A7 | Igual para público/CMS/API: o ID é o mesmo; a diferença é só quem chama `ApiResponse` (API) vs JSON ad-hoc (CMS). |
| B1 | Diagrama em §2.1; IDs em PHP + fallback SQL. |
| B2 | Hooks `creating` → `v2_uuid()`; migrations com transação/lock/guarda; `success`/`error`. |
| B3 | UUID v4 por simplicidade e compatibilidade com PHP 7.2/Laravel 5.5 (`V2/Core/Helpers/helpers.php:56-57`); rollback preservacionista `[I]` por prudência operacional. |
| B4 | Sete trechos em §2.2 (função SQL, helper, schema de tags, seed, cópia, rollback, trait). |
| B5 | Custo: UUID 16 B por PK; 2 queries por `newsCategoryFor`; cópia de tags é O(n) sob transação; `lock_timeout` 3 s limita espera. |
| B6 | Função criada no primeiro `migrate`; tabelas de tags criadas e semeadas uma vez; cópia roda uma vez e não tem volta destrutiva. |
| B7 | DDL+DML atômicos via transação; falha → rollback total + log + exceção; nenhum efeito best-effort nas migrations. |
| C1 | Fonte única do contrato de ID (`v2_uuid`) e do envelope de API (`ApiResponse`); fonte única das convenções de schema. |
| C2 | `v2_news_tags` é catálogo separado de Notícias e **não** usa a função SQL; `v2_conexao_tags` é o catálogo compartilhado de Parceiros/UR. |
| C3 | RN-CORE-DATA-01..08. |
| C4 | Concorrência do migrator; IDs legados inválidos abortam cast; reexecução de `up()` não idempotente; tag reservada ausente. |
| C5 | `down()` que não desfaz dados; tags duplicadas em dois catálogos; `$tagIdMap` morto; pivôs sem escritor. |
| C6 | Ordem por nome de arquivo; `lock_timeout` de sessão; `bytea` de 18 bytes; `instance` sem barra. |
| C7 | Divergência UUIDv7; no-op em `2026_08_18_000002::down`; `$tagIdMap` não usado; pivôs sem attach; `Carbon::now()` isolado. |
| D1 | `v2_uuid(): string`; `success($data=null,string $message='Operação realizada com sucesso.',int $statusCode=200): JsonResponse`; `error(string $errorTitle,string $detail='',int $statusCode=400,string $type='about:blank'): JsonResponse`; `v2_gen_uuid(): uuid` (SQL). |
| D2 | Helper carregado em `register()`; migrations core/módulo/serviço via `loadMigrationsFrom`; trait por `use` no controlador. |
| D3 | Após o autoload do Composer e o `register()` do provider; antes de qualquer model; `2026_06_01_000001` é a primeira migration do V2 em ordem de nome. |
| D4 | Não se aplica: sem Blade/JS. O contrato é HTTP/JSON de `ApiResponse`. |
| D5 | Três receitas em §4.4 (ID em model, envelope de API, migração idempotente). |
| D6 | Três anti-usos em §4.3. |
| D7 | `lock_timeout='3s'`; nenhum limite de tamanho de payload no trait; sem paginação própria. |
| D8 | 5 controladores usam `ApiResponse`; 4 tabelas usam `DEFAULT v2_gen_uuid()`; 80 de 87 migrations dependem do padrão transação (79 também aplicam `lock_timeout`). |
| D9 | Novo gerador: trocar `v2_uuid()` no helper; novo tipo de resposta: estender o trait (mas migrar os JSONs ad-hoc). |
| E1–E7 | Não se aplica: o documento não declara rotas. HTTP aparece apenas como contrato de status em `ApiResponse`. |
| F1 | Tabelas/index/colunas em `_indices/parcial-CORE-DATA/tabelas.csv`. |
| F2 | UUID v4 via PHP; `v2_gen_uuid()` como fallback; `timestamps()` em todas; `softDeletes()` em 32 migrations; ordem global por nome. |
| F3 | Models relevantes citados: `ConexaoTag` (`$table`, `$fillable`, `$casts`, `$incrementing=false`, `$keyType='string'`). |
| F4 | Hook `creating` gera `id`; `ConexaoTag` não tem observer; migrations não disparam eventos Eloquent quando usam `DB::table`. |
| F5 | `id` escrito por model/migration; `slug` da tag escrito pelo controlador via `Str::slug`; `is_reserved` só na seed/migration. |
| F6 | Cardinalidade baixa (tags: dezenas); cópia de tags O(n) em uma transação; nenhuma consulta crítica própria. |
| G1–G6 | Não se aplica: sem telas neste documento (o CRUD de tags está em CORE-CMS). |
| H1–H6 | Não se aplica: sem páginas de portal. |
| I1–I8 | Não se aplica: sem componente JS. |
| J1 | Nenhuma chave de config própria; ver `config.csv` da unidade para chaves regionais. |
| J2 | Não se aplica: sem permissões. |
| J3 | Não se aplica: sem `ModuleProvider`. |
| J4 | Não se aplica: sem flag de módulo. |
| J5 | Não se aplica: sem feature toggle. |
| K1 | Canais: log padrão da aplicação; mensagens com prefixo `[Migration]`/`ERRO NA MIGRAÇÃO`; `Log::alert` no rollback. |
| K2 | Nenhuma auditoria de schema; `ActivityLogger` é acionado pelo CMS de tags, não pelas migrations. |
| K3 | Migration falha → exceção relançada (fail-closed); `ApiResponse` devolve o status informado. |
| K4 | `lock_timeout=3s`; sem limites de payload/paginação no trait. |
| K5 | Nenhum segredo manipulado; a senha de e-mail é cifrada por outra unidade (`2026_07_06_000001`). |
| L1 | Seeds dentro de migrations: `geral` (`2026_08_18_000001:49-56`); demais seeds pertencem a outras unidades. |
| L2 | Chave natural `slug='geral'`; `is_reserved=true`. |
| L3 | Papel regional, categorias padrão, configs e flag de módulo são semeados por migration em outras unidades — ver `12-migracoes-inventario.md`. |
| M1 | PC-CORE-DATA-01..08. |
| M2 | LG-CORE-DATA-01..03. |
| M3 | §9. |
| M4 | Lint e índices parciais emitidos; consolidação é do coordenador. |
