# Módulo de Regras Genérico — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Criar o Sinal `Plataforma/sinais/Regras` — um repositório central de regras de negócio configuráveis por cliente, consultável por qualquer módulo via `ConsultarRegraLaboratorio`.

**Architecture:** Sinal V2 em `Plataforma/sinais/Regras/` com Sonda, Registry, Laboratorio e Validator. Regras são armazenadas como `(modulo, regra, acao, configuracao_json)` por customerControl — o Sinal Regras não interpreta o JSON, só armazena e entrega. Módulos consumidores registram suas situações no `RegrasRegistry` (para a UI popular dropdowns) e consultam via `ConsultarRegraLaboratorio::consultarAcao()`. CRUD exposto via `/api/v1/regras` (Handlers em `Api/Handlers/Regras/`). UI admin em `html/regras.php`.

**Tech Stack:** PHP 8.1, PostgreSQL (gx_goldie), CoffeeCode Router, ModelInteligencia (Sonda V2), JWT auth (internal API).

---

## File Map

**Novos — Sinal Regras:**
- `componentes/Missoes/Plataforma/sinais/Regras/Migrations/001_regras_table.sql`
- `componentes/Missoes/Plataforma/sinais/Regras/Sondas/RegraSonda.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Registry/RegrasRegistry.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Validators/SalvarRegraValidator.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Laboratorios/ConsultarRegraLaboratorio.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Constantes/AcaoRegra.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Regras.php`
- `componentes/Missoes/Plataforma/sinais/Regras/html/regras.php`
- `componentes/Missoes/Plataforma/sinais/Regras/html/regras.js`

**Novos — API Handlers:**
- `componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/ListarHandler.php`
- `componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/SalvarHandler.php`
- `componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/RemoverHandler.php`

**Novos — Testes:**
- `componentes/Missoes/Plataforma/sinais/Regras/Testes/Registry/RegrasRegistryTddTest.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Testes/Validators/SalvarRegraValidatorTddTest.php`
- `componentes/Missoes/Plataforma/sinais/Regras/Testes/Laboratorios/ConsultarRegraLaboratorioTddTest.php`

**Modificados:**
- `componentes/Missoes/Plataforma/sinais/Api/Routes.php` (+ 3 rotas)
- `componentes/Missoes/Plataforma/sinais/Api/Api.php` (+ 3 use + 3 métodos)

---

## Task 1: Migration — tabela `regras`

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Migrations/001_regras_table.sql`

- [ ] **Step 1: Criar arquivo de migration**

```sql
-- =====================================================
-- Migration: Módulo de Regras Genérico (Plataforma/Regras)
-- Data: 2026-05-18
-- =====================================================
--
-- Armazena regras de negócio configuráveis por cliente.
-- O campo `configuracao` é JSONB livre — interpretado pelo módulo consumidor.
-- O campo `customerControl` identifica o cliente dono da regra.
-- companyControl=0 significa "aplica a todas as filiais".
-- Block columns são padrão Galaxia — não declarar DEFAULT nem inserir manualmente.
-- =====================================================

CREATE TABLE regras (
    id             SERIAL PRIMARY KEY,

    modulo         VARCHAR(100) NOT NULL,
    regra          VARCHAR(100) NOT NULL,
    acao           VARCHAR(100) NOT NULL,
    configuracao   JSONB        NOT NULL,

    ativo          SMALLINT     NOT NULL DEFAULT 1,

    customerControl INTEGER NOT NULL,
    companyControl  INTEGER NOT NULL DEFAULT 0,

    -- Block padrão Galaxia (preenchido pelo framework)
    data_cadastro       TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
    block_filial_id     INTEGER,
    block_usuario_id    INTEGER,
    usuario_cadastro    VARCHAR(255),
    data_atualizacao    TIMESTAMP WITHOUT TIME ZONE,
    usuario_atualizacao VARCHAR(255),
    block_data          TIMESTAMP WITHOUT TIME ZONE,
    block_quem          VARCHAR(255),
    block               INTEGER,
    block_empresa_id    INTEGER,

    CONSTRAINT uk_regra UNIQUE (modulo, regra, acao, customerControl, companyControl)
);

CREATE INDEX idx_regras_lookup   ON regras (modulo, regra, customerControl, ativo);
CREATE INDEX idx_regras_customer ON regras (customerControl);
```

- [ ] **Step 2: Executar no banco de desenvolvimento**

```bash
psql -U postgres -d dev_gx_goldie -f componentes/Missoes/Plataforma/sinais/Regras/Migrations/001_regras_table.sql
```

Esperado: `CREATE TABLE` e `CREATE INDEX` sem erros.

- [ ] **Step 3: Verificar que a tabela existe**

```bash
psql -U postgres -d dev_gx_goldie -c "\d regras"
```

Esperado: listagem das colunas `id, modulo, regra, acao, configuracao, ativo, customerControl, companyControl, ...`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Migrations/001_regras_table.sql
git commit -m "feat(regras): add migration for regras table"
```

---

## Task 2: RegraSonda — model V2

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Sondas/RegraSonda.php`

- [ ] **Step 1: Criar a Sonda**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Regras\Sondas;

use Galaxia\_Controladores\ModelInteligencia;
use Base_config\Helpers\Message;

class RegraSonda extends ModelInteligencia
{
    public static $entity;
    public static $protected;
    public static $required;
    public static $fields;
    public static $propeties;

    public $message;

    public function __construct()
    {
        $this->message = new Message();
        $this->init();
    }

    public function __call($method, $arguments)
    {
        if (method_exists($this, $method)) {
            $this->init();
            return call_user_func_array([$this, $method], $arguments);
        }
    }

    private function init(): void
    {
        self::$entity    = 'regras';
        self::$protected = ['id'];
        self::$required  = [];
        self::$fields    = [];
        self::$propeties = [
            'customerControl' => false,
            'db'              => 'gx_goldie',
            'companyControl'  => false,
            'userControl'     => false,
            'sectorControl'   => false,
        ];
        parent::__construct(
            self::$entity,
            self::$protected,
            self::$required,
            self::$fields,
            self::$propeties
        );
    }

    public function buscarAcao(
        string $modulo,
        string $regra,
        string $acao,
        int $customerControl
    ): ?object {
        $criterios = 'modulo = :m AND regra = :r AND acao = :a AND customerControl = :c AND ativo = 1';
        $params    = http_build_query(['m' => $modulo, 'r' => $regra, 'a' => $acao, 'c' => $customerControl]);
        $resultado = $this->find($criterios, $params)->fetch(false);
        return $resultado ?: null;
    }

    public function buscarPorModuloRegra(
        string $modulo,
        string $regra,
        int $customerControl
    ): array {
        $criterios = 'modulo = :m AND regra = :r AND customerControl = :c AND ativo = 1';
        $params    = http_build_query(['m' => $modulo, 'r' => $regra, 'c' => $customerControl]);
        $resultado = $this->find($criterios, $params)->fetch(true);
        return is_array($resultado) ? $resultado : [];
    }

    public function listarPorCustomer(int $customerControl): array
    {
        $criterios = 'customerControl = :c AND ativo = 1';
        $params    = 'c=' . $customerControl;
        $resultado = $this->find($criterios, $params)
            ->order('modulo ASC, regra ASC, acao ASC')
            ->fetch(true);
        return is_array($resultado) ? $resultado : [];
    }

    public function salvar(array $data): bool
    {
        if (isset($data['configuracao']) && is_array($data['configuracao'])) {
            $data['configuracao'] = json_encode($data['configuracao']);
        }

        $this->data = (object) $data;
        $id         = $data['id'] ?? null;

        if (!empty($id)) {
            $this->update($this->safe(), 'id = :id', "id={$id}");
        } else {
            $id             = $this->create($this->safe());
            $this->data->id = $id;
        }

        if ($this->fail()) {
            $erro = $this->fail()->errorInfo[2] ?? json_encode($this->fail());
            $this->message->error("Erro ao salvar regra: {$erro}");
            return false;
        }
        return true;
    }

    public function desativar(int $id): bool
    {
        $this->update(['ativo' => 0], 'id = :id', "id={$id}");
        if ($this->fail()) {
            $this->message->error('Erro ao remover regra.');
            return false;
        }
        return true;
    }
}
```

- [ ] **Step 2: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Sondas/RegraSonda.php
git commit -m "feat(regras): add RegraSonda V2"
```

---

## Task 3: RegrasRegistry + teste

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Registry/RegrasRegistry.php`
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Testes/Registry/RegrasRegistryTddTest.php`

- [ ] **Step 1: Escrever o teste**

```php
<?php

require_once __DIR__ . '/../../Registry/RegrasRegistry.php';

use GalaxiaMissoes\Plataforma\sinais\Regras\Registry\RegrasRegistry;

class AssertRegistry
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function true(bool $cond, string $msg): void
    {
        if ($cond) {
            self::$passou++;
            echo "✅ [PASS] {$msg}\n";
        } else {
            self::$falhas++;
            echo "❌ [FAIL] {$msg}\n";
        }
    }
}

echo "====================================================\n";
echo "TDD — RegrasRegistry\n";
echo "====================================================\n\n";

RegrasRegistry::reset();

AssertRegistry::true(
    RegrasRegistry::listar() === [],
    'Registry começa vazio'
);

RegrasRegistry::registrar('Central', 'tempo_pedido_no_mapa', 'Tempo de pedido no mapa');

AssertRegistry::true(
    RegrasRegistry::temRegistro('Central', 'tempo_pedido_no_mapa'),
    'Reconhece situação registrada'
);

AssertRegistry::true(
    !RegrasRegistry::temRegistro('Central', 'inexistente'),
    'Rejeita situação não registrada'
);

RegrasRegistry::registrar('Central', 'pedido_sem_cubagem', 'Pedido sem cubagem');
RegrasRegistry::registrar('Missao', 'pedido_sem_movimento', 'Pedido parado');

$modulos = RegrasRegistry::listarModulos();
AssertRegistry::true(
    in_array('Central', $modulos) && in_array('Missao', $modulos),
    'listarModulos retorna todos os módulos registrados'
);

$situacoesCentral = RegrasRegistry::listarSituacoes('Central');
AssertRegistry::true(
    count($situacoesCentral) === 2,
    'listarSituacoes retorna apenas situações do módulo Central'
);

AssertRegistry::true(
    $situacoesCentral['tempo_pedido_no_mapa'] === 'Tempo de pedido no mapa',
    'Label da situação está correto'
);

$todos = RegrasRegistry::listar();
AssertRegistry::true(
    isset($todos['Central']) && isset($todos['Missao']),
    'listar retorna mapa agrupado por módulo'
);

echo "\n====================================================\n";
printf("Resultado: %d passou, %d falhou\n", AssertRegistry::$passou, AssertRegistry::$falhas);
echo "====================================================\n";
exit(AssertRegistry::$falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar o teste para confirmar que falha**

```bash
cd /home/goldie/www/genesis
php componentes/Missoes/Plataforma/sinais/Regras/Testes/Registry/RegrasRegistryTddTest.php
```

Esperado: erro de classe não encontrada.

- [ ] **Step 3: Implementar RegrasRegistry**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Regras\Registry;

class RegrasRegistry
{
    /** @var array<string, array<string, string>> modulo => [regra => label] */
    private static array $registro = [];

    public static function registrar(string $modulo, string $regra, string $label): void
    {
        self::$registro[$modulo][$regra] = $label;
    }

    public static function temRegistro(string $modulo, string $regra): bool
    {
        return isset(self::$registro[$modulo][$regra]);
    }

    /** @return string[] */
    public static function listarModulos(): array
    {
        return array_keys(self::$registro);
    }

    /** @return array<string, string> regra => label */
    public static function listarSituacoes(string $modulo): array
    {
        return self::$registro[$modulo] ?? [];
    }

    /** @return array<string, array<string, string>> modulo => [regra => label] */
    public static function listar(): array
    {
        return self::$registro;
    }

    public static function reset(): void
    {
        self::$registro = [];
    }
}
```

- [ ] **Step 4: Rodar o teste para confirmar que passa**

```bash
php componentes/Missoes/Plataforma/sinais/Regras/Testes/Registry/RegrasRegistryTddTest.php
```

Esperado: `8 passou, 0 falhou`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Registry/RegrasRegistry.php \
        componentes/Missoes/Plataforma/sinais/Regras/Testes/Registry/RegrasRegistryTddTest.php
git commit -m "feat(regras): add RegrasRegistry with TDD test"
```

---

## Task 4: SalvarRegraValidator + teste

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Validators/SalvarRegraValidator.php`
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Testes/Validators/SalvarRegraValidatorTddTest.php`

- [ ] **Step 1: Escrever o teste**

```php
<?php

require_once __DIR__ . '/../../Validators/SalvarRegraValidator.php';

use GalaxiaMissoes\Plataforma\sinais\Regras\Validators\SalvarRegraValidator;

class AssertValidator
{
    public static int $passou = 0;
    public static int $falhas = 0;

    public static function true(bool $cond, string $msg): void
    {
        if ($cond) {
            self::$passou++;
            echo "✅ [PASS] {$msg}\n";
        } else {
            self::$falhas++;
            echo "❌ [FAIL] {$msg}\n";
        }
    }
}

echo "====================================================\n";
echo "TDD — SalvarRegraValidator\n";
echo "====================================================\n\n";

$v = new SalvarRegraValidator();

$erros = $v->validar([]);
AssertValidator::true(!empty($erros), 'Bloqueia payload vazio');

$erros = $v->validar(['regra' => 'r', 'acao' => 'a', 'configuracao' => []]);
AssertValidator::true(
    !empty($erros) && (bool) array_filter($erros, fn($e) => str_contains($e, 'modulo')),
    'Exige modulo'
);

$erros = $v->validar(['modulo' => 'm', 'acao' => 'a', 'configuracao' => []]);
AssertValidator::true(
    !empty($erros) && (bool) array_filter($erros, fn($e) => str_contains($e, 'regra')),
    'Exige regra'
);

$erros = $v->validar(['modulo' => 'm', 'regra' => 'r', 'configuracao' => []]);
AssertValidator::true(
    !empty($erros) && (bool) array_filter($erros, fn($e) => str_contains($e, 'acao')),
    'Exige acao'
);

$erros = $v->validar(['modulo' => 'm', 'regra' => 'r', 'acao' => 'a']);
AssertValidator::true(
    !empty($erros) && (bool) array_filter($erros, fn($e) => str_contains($e, 'configuracao')),
    'Exige configuracao'
);

$erros = $v->validar(['modulo' => str_repeat('x', 101), 'regra' => 'r', 'acao' => 'a', 'configuracao' => []]);
AssertValidator::true(
    !empty($erros) && (bool) array_filter($erros, fn($e) => str_contains($e, '100')),
    'Rejeita modulo > 100 chars'
);

$erros = $v->validar([
    'modulo'       => 'Central',
    'regra'        => 'tempo_pedido_no_mapa',
    'acao'         => 'alterar_cor',
    'configuracao' => ['15' => 'success', '30' => 'warning', '60' => 'danger'],
]);
AssertValidator::true(empty($erros), 'Aceita payload válido completo');

$erros = $v->validar([
    'modulo'         => 'Central',
    'regra'          => 'tempo_pedido_no_mapa',
    'acao'           => 'alterar_cor',
    'configuracao'   => ['threshold' => 30],
    'companyControl' => 5,
]);
AssertValidator::true(empty($erros), 'Aceita companyControl opcional como inteiro');

echo "\n====================================================\n";
printf("Resultado: %d passou, %d falhou\n", AssertValidator::$passou, AssertValidator::$falhas);
echo "====================================================\n";
exit(AssertValidator::$falhas > 0 ? 1 : 0);
```

- [ ] **Step 2: Rodar para confirmar falha**

```bash
php componentes/Missoes/Plataforma/sinais/Regras/Testes/Validators/SalvarRegraValidatorTddTest.php
```

Esperado: erro de classe não encontrada.

- [ ] **Step 3: Implementar o Validator**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Regras\Validators;

class SalvarRegraValidator
{
    public function validar(array $entrada): array
    {
        $erros = [];

        if (empty($entrada['modulo'])) {
            $erros[] = 'Campo modulo é obrigatório.';
        } elseif (strlen($entrada['modulo']) > 100) {
            $erros[] = 'Campo modulo deve ter no máximo 100 caracteres.';
        }

        if (empty($entrada['regra'])) {
            $erros[] = 'Campo regra é obrigatório.';
        } elseif (strlen($entrada['regra']) > 100) {
            $erros[] = 'Campo regra deve ter no máximo 100 caracteres.';
        }

        if (empty($entrada['acao'])) {
            $erros[] = 'Campo acao é obrigatório.';
        } elseif (strlen($entrada['acao']) > 100) {
            $erros[] = 'Campo acao deve ter no máximo 100 caracteres.';
        }

        if (!array_key_exists('configuracao', $entrada)) {
            $erros[] = 'Campo configuracao é obrigatório.';
        } elseif (!is_array($entrada['configuracao']) && !is_object($entrada['configuracao'])) {
            $erros[] = 'Campo configuracao deve ser um objeto/array.';
        }

        if (isset($entrada['companyControl']) && !is_int($entrada['companyControl'])) {
            $erros[] = 'Campo companyControl deve ser um inteiro.';
        }

        return $erros;
    }
}
```

- [ ] **Step 4: Rodar para confirmar que passa**

```bash
php componentes/Missoes/Plataforma/sinais/Regras/Testes/Validators/SalvarRegraValidatorTddTest.php
```

Esperado: `8 passou, 0 falhou`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Validators/SalvarRegraValidator.php \
        componentes/Missoes/Plataforma/sinais/Regras/Testes/Validators/SalvarRegraValidatorTddTest.php
git commit -m "feat(regras): add SalvarRegraValidator with TDD test"
```

---

## Task 5: ConsultarRegraLaboratorio + teste

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Laboratorios/ConsultarRegraLaboratorio.php`
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Testes/Laboratorios/ConsultarRegraLaboratorioTddTest.php`

- [ ] **Step 1: Escrever o teste (unit com Sonda stub)**

```php
<?php

/**
 * TDD — ConsultarRegraLaboratorio (unit, sem DB)
 *
 * Usa stub da RegraSonda para testar o Laboratorio isoladamente.
 * Standalone: php Testes/Laboratorios/ConsultarRegraLaboratorioTddTest.php
 */

// ---- Stub RegraSonda ----
namespace GalaxiaMissoes\Plataforma\sinais\Regras\Sondas {

    class RegraSonda
    {
        private array $fixture = [];

        public function seedFixture(array $rows): void
        {
            $this->fixture = $rows;
        }

        public function buscarAcao(
            string $modulo,
            string $regra,
            string $acao,
            int $customerControl
        ): ?object {
            foreach ($this->fixture as $row) {
                if (
                    $row['modulo'] === $modulo &&
                    $row['regra']  === $regra  &&
                    $row['acao']   === $acao   &&
                    $row['customerControl'] === $customerControl
                ) {
                    return (object) $row;
                }
            }
            return null;
        }

        public function buscarPorModuloRegra(
            string $modulo,
            string $regra,
            int $customerControl
        ): array {
            return array_values(array_filter(
                $this->fixture,
                fn($r) => $r['modulo'] === $modulo &&
                          $r['regra']  === $regra  &&
                          $r['customerControl'] === $customerControl
            ));
        }
    }
}

// ---- Laboratorio + teste ----
namespace {

    require_once __DIR__ . '/../../Laboratorios/ConsultarRegraLaboratorio.php';

    use GalaxiaMissoes\Plataforma\sinais\Regras\Laboratorios\ConsultarRegraLaboratorio;
    use GalaxiaMissoes\Plataforma\sinais\Regras\Sondas\RegraSonda;

    class AssertLab
    {
        public static int $passou = 0;
        public static int $falhas = 0;

        public static function true(bool $cond, string $msg): void
        {
            if ($cond) {
                self::$passou++;
                echo "✅ [PASS] {$msg}\n";
            } else {
                self::$falhas++;
                echo "❌ [FAIL] {$msg}\n";
            }
        }
    }

    echo "====================================================\n";
    echo "TDD — ConsultarRegraLaboratorio\n";
    echo "====================================================\n\n";

    $fixture = [
        [
            'modulo'          => 'Central',
            'regra'           => 'tempo_pedido_no_mapa',
            'acao'            => 'alterar_cor',
            'configuracao'    => '{"15":"success","30":"warning","60":"danger"}',
            'customerControl' => 7,
        ],
        [
            'modulo'          => 'Central',
            'regra'           => 'tempo_pedido_no_mapa',
            'acao'            => 'enviar_alerta',
            'configuracao'    => '{"threshold":60}',
            'customerControl' => 7,
        ],
    ];

    $sonda = new RegraSonda();
    $sonda->seedFixture($fixture);
    $lab = new ConsultarRegraLaboratorio($sonda);

    // consultarAcao — encontra regra existente
    $resultado = $lab->consultarAcao('Central', 'tempo_pedido_no_mapa', 'alterar_cor', 7);
    AssertLab::true($resultado !== null, 'consultarAcao retorna objeto para regra existente');
    AssertLab::true(
        is_array($resultado->configuracao) && ($resultado->configuracao['15'] ?? '') === 'success',
        'configuracao é decodificada como array pelo Laboratorio'
    );

    // consultarAcao — retorna null para acao inexistente
    $resultado = $lab->consultarAcao('Central', 'tempo_pedido_no_mapa', 'inexistente', 7);
    AssertLab::true($resultado === null, 'consultarAcao retorna null para acao inexistente');

    // consultarAcao — retorna null para customer errado
    $resultado = $lab->consultarAcao('Central', 'tempo_pedido_no_mapa', 'alterar_cor', 99);
    AssertLab::true($resultado === null, 'consultarAcao retorna null para customer diferente');

    // consultar — retorna todas as acoes
    $acoes = $lab->consultar('Central', 'tempo_pedido_no_mapa', 7);
    AssertLab::true(count($acoes) === 2, 'consultar retorna todas as acoes da regra');

    // consultar — retorna array vazio para regra inexistente
    $acoes = $lab->consultar('Central', 'inexistente', 7);
    AssertLab::true($acoes === [], 'consultar retorna array vazio para regra inexistente');

    echo "\n====================================================\n";
    printf("Resultado: %d passou, %d falhou\n", AssertLab::$passou, AssertLab::$falhas);
    echo "====================================================\n";
    exit(AssertLab::$falhas > 0 ? 1 : 0);
}
```

- [ ] **Step 2: Rodar para confirmar falha**

```bash
php componentes/Missoes/Plataforma/sinais/Regras/Testes/Laboratorios/ConsultarRegraLaboratorioTddTest.php
```

Esperado: erro de classe não encontrada.

- [ ] **Step 3: Implementar ConsultarRegraLaboratorio**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Regras\Laboratorios;

use GalaxiaMissoes\Plataforma\sinais\Regras\Sondas\RegraSonda;

class ConsultarRegraLaboratorio
{
    private RegraSonda $sonda;

    public function __construct(?RegraSonda $sonda = null)
    {
        $this->sonda = $sonda ?? new RegraSonda();
    }

    /**
     * Retorna a configuração de uma ação específica, ou null se não configurada.
     * O campo configuracao é decodificado como array associativo.
     */
    public function consultarAcao(
        string $modulo,
        string $regra,
        string $acao,
        int $customerControl
    ): ?object {
        $resultado = $this->sonda->buscarAcao($modulo, $regra, $acao, $customerControl);
        if ($resultado === null) {
            return null;
        }
        return $this->decodificarConfiguracao($resultado);
    }

    /**
     * Retorna todas as ações configuradas para (modulo, regra).
     * Cada item tem configuracao decodificado como array.
     */
    public function consultar(
        string $modulo,
        string $regra,
        int $customerControl
    ): array {
        $resultados = $this->sonda->buscarPorModuloRegra($modulo, $regra, $customerControl);
        return array_map(fn($r) => $this->decodificarConfiguracao((object) $r), $resultados);
    }

    private function decodificarConfiguracao(object $regra): object
    {
        if (is_string($regra->configuracao)) {
            $regra->configuracao = json_decode($regra->configuracao, true) ?? [];
        }
        return $regra;
    }
}
```

- [ ] **Step 4: Rodar para confirmar que passa**

```bash
php componentes/Missoes/Plataforma/sinais/Regras/Testes/Laboratorios/ConsultarRegraLaboratorioTddTest.php
```

Esperado: `5 passou, 0 falhou`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Laboratorios/ConsultarRegraLaboratorio.php \
        componentes/Missoes/Plataforma/sinais/Regras/Testes/Laboratorios/ConsultarRegraLaboratorioTddTest.php
git commit -m "feat(regras): add ConsultarRegraLaboratorio with TDD test"
```

---

## Task 6: AcaoRegra — classe de constantes

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Constantes/AcaoRegra.php`

- [ ] **Step 1: Criar a classe**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Regras\Constantes;

class AcaoRegra
{
    const ALTERAR_COR   = 'alterar_cor';
    const ENVIAR_ALERTA = 'enviar_alerta';
    const BLOQUEAR      = 'bloquear';
    const VALOR         = 'valor';
}
```

- [ ] **Step 2: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Constantes/AcaoRegra.php
git commit -m "feat(regras): add AcaoRegra constants class"
```

---

## Task 7: API Handlers (Listar, Salvar, Remover)

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/ListarHandler.php`
- Create: `componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/SalvarHandler.php`
- Create: `componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/RemoverHandler.php`

- [ ] **Step 1: Criar ListarHandler**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Api\Handlers\Regras;

use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\BaseHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Request;
use GalaxiaMissoes\Plataforma\sinais\Api\Response;
use GalaxiaMissoes\Plataforma\sinais\Regras\Registry\RegrasRegistry;
use GalaxiaMissoes\Plataforma\sinais\Regras\Sondas\RegraSonda;

/**
 * GET /api/v1/regras
 * GET /api/v1/regras?modulo=Central
 *
 * Lista as regras do cliente autenticado, opcionalmente filtradas por módulo.
 * Também retorna o registry de situações disponíveis para alimentar a UI.
 */
class ListarHandler extends BaseHandler
{
    public function handle(Request $req, Response $res): void
    {
        $customerControl = (int) ($req->jwtClaims['customer'] ?? 0);
        if ($customerControl === 0) {
            [$body, $status] = $this->erro($req, 'unauthorized', 'Token inválido ou sem customer.', 401);
            $res->json($body, $status);
            return;
        }

        $sonda  = new RegraSonda();
        $regras = $sonda->listarPorCustomer($customerControl);

        $moduloFiltro = $req->query['modulo'] ?? null;
        if ($moduloFiltro !== null) {
            $regras = array_values(array_filter(
                $regras,
                fn($r) => ($r->modulo ?? $r['modulo'] ?? '') === $moduloFiltro
            ));
        }

        $regrasNormalizadas = array_map(function ($r) {
            $r = is_object($r) ? (array) $r : $r;
            if (is_string($r['configuracao'] ?? null)) {
                $r['configuracao'] = json_decode($r['configuracao'], true) ?? [];
            }
            return $r;
        }, $regras);

        [$body, $status] = $this->ok($req, [
            'regras'   => $regrasNormalizadas,
            'registry' => RegrasRegistry::listar(),
        ]);
        $res->json($body, $status);
    }
}
```

- [ ] **Step 2: Criar SalvarHandler**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Api\Handlers\Regras;

use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\BaseHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Request;
use GalaxiaMissoes\Plataforma\sinais\Api\Response;
use GalaxiaMissoes\Plataforma\sinais\Regras\Sondas\RegraSonda;
use GalaxiaMissoes\Plataforma\sinais\Regras\Validators\SalvarRegraValidator;

/**
 * POST /api/v1/regras
 *
 * Cria ou atualiza uma regra para o cliente autenticado.
 * Body: { modulo, regra, acao, configuracao, companyControl? (int, default 0), id? (int, para update) }
 */
class SalvarHandler extends BaseHandler
{
    public function handle(Request $req, Response $res): void
    {
        $customerControl = (int) ($req->jwtClaims['customer'] ?? 0);
        if ($customerControl === 0) {
            [$body, $status] = $this->erro($req, 'unauthorized', 'Token inválido ou sem customer.', 401);
            $res->json($body, $status);
            return;
        }

        $body  = $req->body;
        $erros = (new SalvarRegraValidator())->validar($body);
        if (!empty($erros)) {
            [$resposta, $status] = $this->erro($req, 'invalid_input', implode(' ', $erros), 422);
            $res->json($resposta, $status);
            return;
        }

        $data = [
            'id'             => !empty($body['id']) ? (int) $body['id'] : null,
            'modulo'         => $body['modulo'],
            'regra'          => $body['regra'],
            'acao'           => $body['acao'],
            'configuracao'   => $body['configuracao'],
            'ativo'          => 1,
            'customerControl' => $customerControl,
            'companyControl'  => isset($body['companyControl']) ? (int) $body['companyControl'] : 0,
        ];

        $sonda = new RegraSonda();
        $ok    = $sonda->salvar($data);

        if (!$ok) {
            [$resposta, $status] = $this->erro($req, 'operation_failed', 'Erro ao salvar regra.', 400);
            $res->json($resposta, $status);
            return;
        }

        [$resposta, $status] = $this->ok($req, ['id' => $sonda->data->id ?? null], 200);
        $res->json($resposta, $status);
    }
}
```

- [ ] **Step 3: Criar RemoverHandler**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Api\Handlers\Regras;

use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\BaseHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Request;
use GalaxiaMissoes\Plataforma\sinais\Api\Response;
use GalaxiaMissoes\Plataforma\sinais\Regras\Sondas\RegraSonda;

/**
 * DELETE /api/v1/regras/{id}
 *
 * Desativa (soft-delete) uma regra do cliente autenticado.
 */
class RemoverHandler extends BaseHandler
{
    public function handle(Request $req, Response $res): void
    {
        $customerControl = (int) ($req->jwtClaims['customer'] ?? 0);
        if ($customerControl === 0) {
            [$body, $status] = $this->erro($req, 'unauthorized', 'Token inválido ou sem customer.', 401);
            $res->json($body, $status);
            return;
        }

        $id = (int) ($req->routeParams['id'] ?? 0);
        if ($id === 0) {
            [$body, $status] = $this->erro($req, 'invalid_input', 'ID inválido.', 422);
            $res->json($body, $status);
            return;
        }

        $sonda  = new RegraSonda();
        $regras = $sonda->listarPorCustomer($customerControl);
        $existe = (bool) array_filter(
            $regras,
            fn($r) => ((int) (is_object($r) ? $r->id : $r['id'])) === $id
        );

        if (!$existe) {
            [$body, $status] = $this->erro($req, 'not_found', 'Regra não encontrada.', 404);
            $res->json($body, $status);
            return;
        }

        $ok = $sonda->desativar($id);
        if (!$ok) {
            [$body, $status] = $this->erro($req, 'operation_failed', 'Erro ao remover regra.', 400);
            $res->json($body, $status);
            return;
        }

        [$body, $status] = $this->ok($req, ['removed' => true]);
        $res->json($body, $status);
    }
}
```

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Api/Handlers/Regras/
git commit -m "feat(regras): add Listar/Salvar/Remover API handlers"
```

---

## Task 8: Registrar rotas — Routes.php + Api.php

**Files:**
- Modify: `componentes/Missoes/Plataforma/sinais/Api/Routes.php`
- Modify: `componentes/Missoes/Plataforma/sinais/Api/Api.php`

- [ ] **Step 1: Adicionar rotas em Routes.php**

Localizar o bloco de comentário de rotas (ex: logo após as rotas de Dashboard) e adicionar:

```php
// Regras
$route->get   ('/regras',     'Api:regrasListar');
$route->post  ('/regras',     'Api:regrasSalvar');
$route->delete('/regras/{id}','Api:regrasRemover');
```

- [ ] **Step 2: Adicionar `use` statements em Api.php**

No bloco de `use` statements, adicionar:

```php
use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\Regras\ListarHandler as RegrasListarHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\Regras\SalvarHandler as RegrasSalvarHandler;
use GalaxiaMissoes\Plataforma\sinais\Api\Handlers\Regras\RemoverHandler as RegrasRemoverHandler;
```

- [ ] **Step 3: Adicionar métodos de dispatch em Api.php**

Localizar onde estão os outros métodos de dispatch (ex: próximo ao bloco de Dashboard) e adicionar:

```php
public function regrasListar(?array $data = []): void
{
    $this->dispatcher()->run('GET', '/api/v1/regras', RegrasListarHandler::class, $data ?? []);
}

public function regrasSalvar(?array $data = []): void
{
    $this->dispatcher()->run('POST', '/api/v1/regras', RegrasSalvarHandler::class, $data ?? []);
}

public function regrasRemover(?array $data = []): void
{
    $this->dispatcher()->run('DELETE', '/api/v1/regras/{id}', RegrasRemoverHandler::class, $data ?? []);
}
```

- [ ] **Step 4: Verificar sintaxe**

```bash
php -l componentes/Missoes/Plataforma/sinais/Api/Routes.php
php -l componentes/Missoes/Plataforma/sinais/Api/Api.php
```

Esperado: `No syntax errors detected`.

- [ ] **Step 5: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Api/Routes.php \
        componentes/Missoes/Plataforma/sinais/Api/Api.php
git commit -m "feat(regras): wire API routes for regras endpoints"
```

---

## Task 9: Controller Regras.php (dispatcher fino)

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/Regras.php`

- [ ] **Step 1: Criar o controller**

```php
<?php

namespace GalaxiaMissoes\Plataforma\sinais\Regras;

use GalaxiaMissoes\Plataforma\sinais\Regras\Registry\RegrasRegistry;

class Regras
{
    public $galaxiaRoute;

    public function __construct(object $galaxia, ?array $data = [])
    {
        $this->galaxiaRoute = $galaxia;
    }

    public function GalaxiaAutoload(?array $available = [])
    {
        return [];
    }

    public function start(?array $data = []): void
    {
        // View renderizada pelo GalaxiaRoute via html/regras.php
    }
}
```

- [ ] **Step 2: Verificar sintaxe**

```bash
php -l componentes/Missoes/Plataforma/sinais/Regras/Regras.php
```

Esperado: `No syntax errors detected`.

- [ ] **Step 3: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/Regras.php
git commit -m "feat(regras): add Regras controller"
```

---

## Task 10: View admin — html/regras.php + html/regras.js

**Files:**
- Create: `componentes/Missoes/Plataforma/sinais/Regras/html/regras.php`
- Create: `componentes/Missoes/Plataforma/sinais/Regras/html/regras.js`

- [ ] **Step 1: Criar a view html/regras.php**

```php
<?php
/**
 * Admin UI — Módulo de Regras Genérico
 * CRUD de regras de negócio configuráveis por cliente.
 * Toda a lógica de dados está em regras.js (consumindo /api/v1/regras).
 */
?>
<div class="container-fluid px-4 py-3">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h3 class="mb-0"><?= show('regras.titulo', 'Regras de Negócio') ?></h3>
        <button class="btn btn-primary btn-sm" id="btn-nova-regra">
            <i class="bi bi-plus-lg me-1"></i><?= show('regras.nova_regra', 'Nova Regra') ?>
        </button>
    </div>

    <!-- Filtro por módulo -->
    <div class="mb-3">
        <select class="form-select form-select-sm w-auto" id="filtro-modulo">
            <option value=""><?= show('regras.todos_modulos', 'Todos os módulos') ?></option>
        </select>
    </div>

    <!-- Tabela de regras -->
    <div class="card">
        <div class="card-body p-0">
            <table class="table table-hover mb-0" id="tabela-regras">
                <thead class="table-light">
                    <tr>
                        <th><?= show('regras.col_modulo', 'Módulo') ?></th>
                        <th><?= show('regras.col_regra', 'Regra') ?></th>
                        <th><?= show('regras.col_acao', 'Ação') ?></th>
                        <th><?= show('regras.col_configuracao', 'Configuração') ?></th>
                        <th><?= show('regras.col_acoes', 'Ações') ?></th>
                    </tr>
                </thead>
                <tbody id="corpo-tabela-regras">
                    <tr>
                        <td colspan="5" class="text-center py-3 text-muted">
                            <?= show('regras.carregando', 'Carregando...') ?>
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>

<!-- Modal nova/editar regra -->
<div class="modal fade" id="modal-regra" tabindex="-1">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="modal-regra-titulo">
                    <?= show('regras.nova_regra', 'Nova Regra') ?>
                </h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
            </div>
            <div class="modal-body">
                <input type="hidden" id="regra-id">
                <div class="mb-3">
                    <label class="form-label"><?= show('regras.col_modulo', 'Módulo') ?></label>
                    <select class="form-select" id="regra-modulo"></select>
                </div>
                <div class="mb-3">
                    <label class="form-label"><?= show('regras.col_regra', 'Regra') ?></label>
                    <select class="form-select" id="regra-regra"></select>
                </div>
                <div class="mb-3">
                    <label class="form-label"><?= show('regras.col_acao', 'Ação') ?></label>
                    <input type="text" class="form-control" id="regra-acao"
                           placeholder="ex: alterar_cor, enviar_alerta, bloquear">
                    <div class="form-text" id="sugestoes-acao"></div>
                </div>
                <div class="mb-3">
                    <label class="form-label"><?= show('regras.col_configuracao', 'Configuração (JSON)') ?></label>
                    <textarea class="form-control font-monospace" id="regra-configuracao"
                              rows="5" placeholder='{"threshold":30}'></textarea>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
                    <?= show('regras.cancelar', 'Cancelar') ?>
                </button>
                <button type="button" class="btn btn-primary" id="btn-salvar-regra">
                    <?= show('regras.salvar', 'Salvar') ?>
                </button>
            </div>
        </div>
    </div>
</div>

<script src="<?= $galaxia->asset('Plataforma', 'Regras', 'regras.js') ?>"></script>
```

- [ ] **Step 2: Criar regras.js**

```javascript
(function () {
    'use strict';

    const API = '/api/v1/regras';
    let registry = {};
    let modalEl;
    let bsModal;

    const ACOES_SUGERIDAS = ['alterar_cor', 'enviar_alerta', 'bloquear', 'valor'];

    async function apiFetch(url, opts = {}) {
        const res = await fetch(url, {
            headers: { 'Content-Type': 'application/json', ...((opts.headers) || {}) },
            ...opts,
        });
        return res.json();
    }

    async function carregar(moduloFiltro = '') {
        const url = moduloFiltro ? `${API}?modulo=${encodeURIComponent(moduloFiltro)}` : API;
        const json = await apiFetch(url);
        if (json.error) { galaxiaToast('error', json.error.message); return; }

        registry = json.data.registry || {};
        renderizarFiltro();
        renderizarTabela(json.data.regras || []);
    }

    function renderizarFiltro() {
        const sel = document.getElementById('filtro-modulo');
        const atual = sel.value;
        while (sel.options.length > 1) sel.remove(1);
        Object.keys(registry).forEach(mod => {
            const opt = document.createElement('option');
            opt.value = mod;
            opt.textContent = mod;
            if (mod === atual) opt.selected = true;
            sel.appendChild(opt);
        });
    }

    function renderizarTabela(regras) {
        const tbody = document.getElementById('corpo-tabela-regras');
        if (!regras.length) {
            tbody.innerHTML = '<tr><td colspan="5" class="text-center py-3 text-muted">Nenhuma regra cadastrada.</td></tr>';
            return;
        }
        tbody.innerHTML = regras.map(r => `
            <tr>
                <td><span class="badge bg-secondary">${escHtml(r.modulo)}</span></td>
                <td>${escHtml(r.regra)}</td>
                <td><code>${escHtml(r.acao)}</code></td>
                <td><pre class="mb-0 small">${escHtml(JSON.stringify(r.configuracao, null, 2))}</pre></td>
                <td>
                    <button class="btn btn-sm btn-outline-primary me-1" onclick="editarRegra(${r.id})">
                        <i class="bi bi-pencil"></i>
                    </button>
                    <button class="btn btn-sm btn-outline-danger" onclick="removerRegra(${r.id})">
                        <i class="bi bi-trash"></i>
                    </button>
                </td>
            </tr>
        `).join('');
    }

    function abrirModalNova() {
        document.getElementById('regra-id').value = '';
        document.getElementById('regra-acao').value = '';
        document.getElementById('regra-configuracao').value = '';
        document.getElementById('modal-regra-titulo').textContent = 'Nova Regra';
        popularSelectModulo();
        bsModal.show();
    }

    function popularSelectModulo(moduloSelecionado = '', regraSelecionada = '') {
        const selMod = document.getElementById('regra-modulo');
        selMod.innerHTML = '<option value="">-- Selecione o módulo --</option>';
        Object.keys(registry).forEach(mod => {
            const opt = document.createElement('option');
            opt.value = mod;
            opt.textContent = mod;
            if (mod === moduloSelecionado) opt.selected = true;
            selMod.appendChild(opt);
        });
        popularSelectRegra(moduloSelecionado, regraSelecionada);
    }

    function popularSelectRegra(modulo, regraSelecionada = '') {
        const selReg = document.getElementById('regra-regra');
        selReg.innerHTML = '<option value="">-- Selecione a situação --</option>';
        const situacoes = registry[modulo] || {};
        Object.entries(situacoes).forEach(([chave, label]) => {
            const opt = document.createElement('option');
            opt.value = chave;
            opt.textContent = label;
            if (chave === regraSelecionada) opt.selected = true;
            selReg.appendChild(opt);
        });
    }

    window.editarRegra = function (id) {
        apiFetch(API).then(json => {
            const regra = (json.data.regras || []).find(r => r.id === id);
            if (!regra) return;
            document.getElementById('regra-id').value = regra.id;
            document.getElementById('regra-acao').value = regra.acao;
            document.getElementById('regra-configuracao').value = JSON.stringify(regra.configuracao, null, 2);
            document.getElementById('modal-regra-titulo').textContent = 'Editar Regra';
            popularSelectModulo(regra.modulo, regra.regra);
            bsModal.show();
        });
    };

    window.removerRegra = function (id) {
        if (!confirm('Remover esta regra?')) return;
        apiFetch(`${API}/${id}`, { method: 'DELETE' }).then(json => {
            if (json.error) { galaxiaToast('error', json.error.message); return; }
            galaxiaToast('success', 'Regra removida.');
            carregar(document.getElementById('filtro-modulo').value);
        });
    };

    async function salvar() {
        const id            = document.getElementById('regra-id').value;
        const modulo        = document.getElementById('regra-modulo').value;
        const regra         = document.getElementById('regra-regra').value;
        const acao          = document.getElementById('regra-acao').value.trim();
        const configRaw     = document.getElementById('regra-configuracao').value.trim();

        if (!modulo || !regra || !acao || !configRaw) {
            galaxiaToast('warning', 'Preencha todos os campos.');
            return;
        }

        let configuracao;
        try { configuracao = JSON.parse(configRaw); }
        catch { galaxiaToast('error', 'Configuração não é um JSON válido.'); return; }

        const payload = { modulo, regra, acao, configuracao };
        if (id) payload.id = parseInt(id, 10);

        const json = await apiFetch(API, { method: 'POST', body: JSON.stringify(payload) });
        if (json.error) { galaxiaToast('error', json.error.message); return; }

        bsModal.hide();
        galaxiaToast('success', 'Regra salva com sucesso.');
        carregar(document.getElementById('filtro-modulo').value);
    }

    function escHtml(str) {
        return String(str ?? '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    document.addEventListener('DOMContentLoaded', function () {
        modalEl = document.getElementById('modal-regra');
        bsModal = new bootstrap.Modal(modalEl);

        document.getElementById('btn-nova-regra').addEventListener('click', abrirModalNova);
        document.getElementById('btn-salvar-regra').addEventListener('click', salvar);

        document.getElementById('regra-modulo').addEventListener('change', function () {
            popularSelectRegra(this.value);
        });

        document.getElementById('filtro-modulo').addEventListener('change', function () {
            carregar(this.value);
        });

        document.getElementById('regra-acao').addEventListener('focus', function () {
            document.getElementById('sugestoes-acao').textContent =
                'Sugestões: ' + ACOES_SUGERIDAS.join(', ');
        });

        carregar();
    });
})();
```

- [ ] **Step 3: Verificar sintaxe PHP**

```bash
php -l componentes/Missoes/Plataforma/sinais/Regras/html/regras.php
```

Esperado: `No syntax errors detected`.

- [ ] **Step 4: Commit**

```bash
git add componentes/Missoes/Plataforma/sinais/Regras/html/
git commit -m "feat(regras): add admin UI view and JS for regras management"
```

---

## Checklist de auto-revisão

- [ ] Tabela `regras` criada e indexada no banco
- [ ] `RegraSonda` V2 com `customerControl: false` (gestão explícita)
- [ ] `RegrasRegistry::registrar()` disponível para módulos consumidores usarem no boot
- [ ] `ConsultarRegraLaboratorio::consultarAcao()` decodifica JSONB → array
- [ ] `SalvarRegraValidator` protege todos os campos obrigatórios
- [ ] `RemoverHandler` verifica ownership (só remove regras do próprio customer)
- [ ] 3 testes TDD passando (Registry, Validator, Laboratorio)
- [ ] Rotas registradas: GET/POST `/api/v1/regras` e DELETE `/api/v1/regras/{id}`
- [ ] UI exibe registry de situações nos dropdowns (sem preenchimento manual de strings)
