📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Módulo 02: Lógica de Programação e Algoritmos. Recomendado revisar antes de começar.

🌱 Miniprojeto 09: Chamados de Manutenção Predial

Guia Autoguiado Passo a Passo: Do Controle de Defeitos em JSON ao Sistema Web com Kanban e SQLite

Trilha de Especialização — Nível Introdutório (Curso 25: Sistemas Desktop com Python)


🎯 Objetivo de Aprendizagem

Na gestão predial de condomínios, prédios públicos e escolas municipais, o controle dos reparos de infraestrutura (elétrica, hidráulica, pintura e alvenaria) exige acompanhamento rigoroso de prioridades e pareceres técnicos de conclusão.

Neste projeto, o estudante aprende como:

  1. Registrar chamados com múltiplos atributos e transições em arquivos .json.
  2. Criar a classe Chamado encapsulando o ciclo de vida (iniciar_atendimento(), concluir(parecer)).
  3. Modelar tabela no SQLite3 com restrições CHECK para prioridades e status, calculando agregações de produtividade (GROUP BY).
  4. Construir uma interface Web moderna com Streamlit, organizando os chamados em formato de quadro Kanban, permitindo registrar pareceres técnicos e atualizar o status em tempo real.

🪜 O Caminho Didático dos 4 Degraus

flowchart LR
    D1["🪜 DEGRAU 1\nImperativo & JSON\nMenu while + chamados.json\ncom ciclo de vida básico"] --> D2["🪜 DEGRAU 2\nPrimeira Classe POO\nclass Chamado\n(iniciar, concluir, parecer)"]
    D2 --> D3["🪜 DEGRAU 3\nSQLite com CHECK\nTabela chamados e\nagregações GROUP BY"]
    D3 --> D4["🪜 DEGRAU 4\nInterface Web Streamlit\nQuadro Kanban, métricas\ne parecer técnico"]

    style D1 fill:#fffde7,stroke:#fbc02d,stroke-width:2px
    style D2 fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style D3 fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
    style D4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px

📂 Estrutura de Pastas e Arquivos

sistemas_python_intro_09_manutencao_predial/
├── degrau_1_imperativo_json/
│   ├── main.py              # Script imperativo com menu e arquivo chamados.json
│   └── test_degrau_1.py     # Testes automatizados de abertura e conclusão
├── degrau_2_primeira_classe/
│   ├── main.py              # Classes POO (class Chamado e class GerenciadorManutencao)
│   └── test_degrau_2.py     # Testes unitários de transições de status
├── degrau_3_sqlite/
│   ├── main.py              # SQLite3 com CHECK constraints e contagem por status
│   └── test_degrau_3.py     # Testes unitários de persistência e pareceres técnicos
├── degrau_4_interface_streamlit/
│   ├── app.py               # Painel Web em Python puro com Streamlit (Kanban)
│   └── main.py              # Launcher do servidor Web Streamlit
├── main.py                  # Hub Central / Launcher interativo
└── index.md                 # Este guia didático autoguiado

🚀 Como Executar no Laboratório

1. Abra o terminal na pasta deste projeto

No VS Code / PyCharm, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:

cd sistemas_python_intro_09_manutencao_predial

2. Execute o Hub Central ou um Degrau Específico

# Executar o Hub Central (Menu interativo com todos os degraus):
python main.py

# Ou executar os degraus de terminal (1 a 3):
python degrau_1_imperativo_json/main.py
python degrau_2_primeira_classe/main.py
python degrau_3_sqlite/main.py

# Executar a aplicação Web (Degrau 4 com Streamlit):
streamlit run degrau_4_interface_streamlit/app.py

[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta executar: python catalogo_projetos/sistemas_python_intro_09_manutencao_predial/main.py


📖 Roteiro Passo a Passo & Gabarito Completo


🪜 Degrau 1: Programação Imperativa com Arquivo JSON (.json)

No primeiro degrau, estruturamos os chamados em formato JSON, registrando data de abertura, local e status.

Código-Fonte Completo (degrau_1_imperativo_json/main.py):

import json
import os
from datetime import datetime

ARQUIVO_PADRAO = "chamados.json"

def abrir_chamado(chamados, local, categoria, descricao, prioridade="MEDIA"):
    novo = {
        "id": len(chamados) + 1,
        "local": local.strip(),
        "categoria": categoria.strip(),
        "descricao": descricao.strip(),
        "prioridade": prioridade.upper(),
        "status": "ABERTO",
        "parecer_tecnico": "",
        "data_abertura": datetime.now().strftime("%d/%m/%Y %H:%M"),
        "data_conclusao": ""
    }
    chamados.append(novo)
    return novo

def atualizar_status(chamados, chamado_id, novo_status, parecer=""):
    for ch in chamados:
        if ch["id"] == chamado_id:
            ch["status"] = novo_status
            if parecer:
                ch["parecer_tecnico"] = parecer.strip()
            if novo_status == "CONCLUIDO":
                ch["data_conclusao"] = datetime.now().strftime("%d/%m/%Y %H:%M")
            return True
    return False

Testes Automatizados (degrau_1_imperativo_json/test_degrau_1.py):

"""
Testes Unitários para o Degrau 1 do Projeto 09 (Imperativo & JSON)
"""

import os
import unittest
from main import (
    abrir_chamado,
    atualizar_status,
    carregar_chamados,
    salvar_chamados
)

ARQUIVO_TESTE = "teste_chamados.json"


class TestDegrau1Manutencao(unittest.TestCase):

    def setUp(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def tearDown(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def test_ciclo_de_vida_chamado(self):
        chamados = []
        ch = abrir_chamado(chamados, "Bloco B - Apto 302", "Hidráulica", "Vazamento no sifão da pia", "ALTA")
        self.assertIsNotNone(ch)
        self.assertEqual(ch["status"], "ABERTO")
        self.assertEqual(ch["prioridade"], "ALTA")

        # Iniciar atendimento
        ok1 = atualizar_status(chamados, 1, "EM_ANDAMENTO")
        self.assertTrue(ok1)
        self.assertEqual(chamados[0]["status"], "EM_ANDAMENTO")

        # Conclusão com parecer
        ok2 = atualizar_status(chamados, 1, "CONCLUIDO", "Sifão substituído por novo modelo flexível.")
        self.assertTrue(ok2)
        self.assertEqual(chamados[0]["status"], "CONCLUIDO")
        self.assertEqual(chamados[0]["parecer_tecnico"], "Sifão substituído por novo modelo flexível.")

        # Persistência
        salvar_chamados(chamados, caminho=ARQUIVO_TESTE)
        carregados = carregar_chamados(caminho=ARQUIVO_TESTE)
        self.assertEqual(len(carregados), 1)
        self.assertEqual(carregados[0]["status"], "CONCLUIDO")


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 2: Primeiro Contato com POO (Classes Chamado e GerenciadorManutencao)

No segundo degrau, a classe Chamado encapsula seu próprio ciclo de atendimento através dos métodos iniciar_atendimento() e concluir(parecer).

Código-Fonte Completo (degrau_2_primeira_classe/main.py):

class Chamado:
    def __init__(self, id_chamado, local, categoria, descricao, prioridade="MEDIA", status="ABERTO", parecer="", data_abertura=None, data_conclusao=""):
        self.id = int(id_chamado)
        self.local = local.strip()
        self.categoria = categoria.strip()
        self.descricao = descricao.strip()
        self.prioridade = prioridade.upper()
        self.status = status.upper()
        self.parecer = parecer.strip()
        self.data_abertura = data_abertura or datetime.now().strftime("%d/%m/%Y %H:%M")
        self.data_conclusao = data_conclusao

    def iniciar_atendimento(self):
        if self.status == "ABERTO":
            self.status = "EM_ANDAMENTO"
            return True
        return False

    def concluir(self, parecer=""):
        self.status = "CONCLUIDO"
        self.parecer = parecer.strip()
        self.data_conclusao = datetime.now().strftime("%d/%m/%Y %H:%M")
        return True

Testes Automatizados (degrau_2_primeira_classe/test_degrau_2.py):

"""
Testes Unitários para o Degrau 2 do Projeto 09 (Primeira Classe POO)
"""

import os
import unittest
from main import Chamado, GerenciadorManutencao

ARQUIVO_TESTE = "teste_chamados_poo.json"


class TestDegrau2Manutencao(unittest.TestCase):

    def setUp(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def tearDown(self):
        if os.path.exists(ARQUIVO_TESTE):
            os.remove(ARQUIVO_TESTE)

    def test_classe_chamado_transicoes(self):
        ch = Chamado(1, "Sala dos Professores", "Elétrica", "Tomada 220V em curto", "URGENTE")
        self.assertTrue(ch.esta_pendente())
        self.assertEqual(ch.status, "ABERTO")

        self.assertTrue(ch.iniciar_atendimento())
        self.assertEqual(ch.status, "EM_ANDAMENTO")
        self.assertTrue(ch.esta_pendente())

        ch.concluir("Disjuntor rearmado e fiação refeita.")
        self.assertEqual(ch.status, "CONCLUIDO")
        self.assertFalse(ch.esta_pendente())
        self.assertEqual(ch.parecer, "Disjuntor rearmado e fiação refeita.")

    def test_gerenciador_persistencia(self):
        ger1 = GerenciadorManutencao()
        c = ger1.abrir_chamado("Portaria Principal", "Segurança", "Interfone mudo", "ALTA")
        c.iniciar_atendimento()
        ger1.salvar(caminho=ARQUIVO_TESTE)

        ger2 = GerenciadorManutencao()
        ger2.carregar(caminho=ARQUIVO_TESTE)
        self.assertEqual(len(ger2.chamados), 1)
        self.assertEqual(ger2.chamados[0].local, "Portaria Principal")
        self.assertEqual(ger2.chamados[0].status, "EM_ANDAMENTO")


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 3: Banco de Dados Relacional com SQLite3 Nativo

No terceiro degrau, criamos a tabela no SQLite com validações de integridade e consultas agrupadas por status.

Código-Fonte Completo (degrau_3_sqlite/main.py):

import sqlite3

def inicializar_tabelas(caminho="manutencao.db"):
    conn = sqlite3.connect(caminho)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS chamados (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            local TEXT NOT NULL,
            categoria TEXT NOT NULL,
            descricao TEXT NOT NULL,
            prioridade TEXT NOT NULL CHECK(prioridade IN ('BAIXA', 'MEDIA', 'ALTA', 'URGENTE')),
            status TEXT DEFAULT 'ABERTO' CHECK(status IN ('ABERTO', 'EM_ANDAMENTO', 'CONCLUIDO')),
            parecer_tecnico TEXT,
            data_abertura TEXT DEFAULT (datetime('now', 'localtime')),
            data_conclusao TEXT
        )
    """)
    conn.commit()
    conn.close()

def obter_metricas_status(caminho="manutencao.db"):
    conn = sqlite3.connect(caminho)
    cursor = conn.cursor()
    cursor.execute("SELECT status, COUNT(*) FROM chamados GROUP BY status")
    contagens = dict(cursor.fetchall())
    conn.close()
    return contagens

Testes Automatizados (degrau_3_sqlite/test_degrau_3.py):

"""
Testes Unitários para o Degrau 3 do Projeto 09 (SQLite3 com Métricas e Transições)
"""

import os
import unittest
from main import (
    abrir_chamado,
    atualizar_status,
    inicializar_tabelas,
    listar_chamados,
    obter_metricas_status
)

BANCO_TESTE = "teste_manutencao_degrau3.db"


class TestDegrau3SQLite(unittest.TestCase):

    def setUp(self):
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)
        inicializar_tabelas(caminho=BANCO_TESTE)

    def tearDown(self):
        if os.path.exists(BANCO_TESTE):
            os.remove(BANCO_TESTE)

    def test_abertura_e_transicao_sqlite(self):
        # 1. Abrir Chamado
        ch_id = abrir_chamado("Biblioteca Central", "Climatização", "Ar-condicionado pingando água", "MEDIA", caminho=BANCO_TESTE)
        self.assertIsNotNone(ch_id)

        # 2. Métricas iniciais
        m1 = obter_metricas_status(caminho=BANCO_TESTE)
        self.assertEqual(m1["TOTAL"], 1)
        self.assertEqual(m1["ABERTO"], 1)
        self.assertEqual(m1["EM_ANDAMENTO"], 0)
        self.assertEqual(m1["CONCLUIDO"], 0)

        # 3. Transição para EM_ANDAMENTO
        ok1 = atualizar_status(ch_id, "EM_ANDAMENTO", caminho=BANCO_TESTE)
        self.assertTrue(ok1)
        m2 = obter_metricas_status(caminho=BANCO_TESTE)
        self.assertEqual(m2["EM_ANDAMENTO"], 1)

        # 4. Conclusão com Parecer
        ok2 = atualizar_status(ch_id, "CONCLUIDO", "Dreno desobstruído e filtro higienizado.", caminho=BANCO_TESTE)
        self.assertTrue(ok2)
        m3 = obter_metricas_status(caminho=BANCO_TESTE)
        self.assertEqual(m3["CONCLUIDO"], 1)
        self.assertEqual(m3["ABERTO"], 0)

        # 5. Consulta por status
        concluidos = listar_chamados("CONCLUIDO", caminho=BANCO_TESTE)
        self.assertEqual(len(concluidos), 1)
        self.assertEqual(concluidos[0][6], "Dreno desobstruído e filtro higienizado.")


if __name__ == "__main__":
    unittest.main()

🪜 Degrau 4: Interface Web com Streamlit (Quadro Kanban de Chamados)

No quarto degrau, o aluno constrói um painel interativo que divide os chamados em colunas visuais por status, facilitando a alteração e inserção de pareceres.

Código-Fonte Completo (degrau_4_interface_streamlit/app.py):

import streamlit as st

def main():
    st.set_page_config(page_title="Chamados de Manutenção Predial", page_icon="🏢", layout="wide")
    st.title("🏢 Gestão de Chamados — Manutenção Predial & Zeladoria")

    # Métricas
    col1, col2, col3, col4 = st.columns(4)
    col1.metric("📋 Total de Chamados", "12")
    col2.metric("🟡 Abertos", "4")
    col3.metric("🔵 Em Andamento", "3")
    col4.metric("🟢 Concluídos", "5")

Launcher do Servidor Web (degrau_4_interface_streamlit/main.py):

"""
Launcher para o Degrau 4 (Streamlit Web)
Verifica se o pacote streamlit está instalado e inicia a aplicação no navegador.
"""

import os
import subprocess
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
APP_FILE = os.path.join(BASE_DIR, "app.py")


def main():
    print("\n========================================================")
    print("🌐 INICIANDO SISTEMA DE MANUTENÇÃO PREDIAL (STREAMLIT)")
    print("========================================================\n")

    try:
        import streamlit  # noqa: F401
        print("✅ Streamlit detectado! Abrindo a aplicacao no seu navegador padrao...\n")
        cmd = [sys.executable, "-m", "streamlit", "run", APP_FILE]
        subprocess.run(cmd, cwd=BASE_DIR)
    except ImportError:
        print("⚠️ O pacote 'streamlit' nao foi encontrado no ambiente Python atual.")
        print("\nPara instalar e rodar, execute no seu terminal:")
        print("    pip install streamlit")
        print(f"    streamlit run \"{APP_FILE}\"\n")


if __name__ == "__main__":
    main()

🎛️ Hub Central: Launcher Interativo (main.py)

O arquivo main.py na raiz do projeto funciona como um painel de controle: permite executar qualquer um dos 4 degraus (ou toda a suíte de testes) a partir de um único menu no terminal, sem precisar navegar manualmente entre as pastas.

Código-Fonte Completo (main.py):

"""
Hub Central / Launcher — Miniprojeto 09: Chamados de Manutenção Predial
Permite executar de forma autoguiada qualquer um dos 4 degraus pedagógicos ou rodar a suíte de testes.
"""

import os
import subprocess
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


def rodar_script(caminho_relativo):
    caminho_absoluto = os.path.join(BASE_DIR, caminho_relativo)
    diretorio = os.path.dirname(caminho_absoluto)
    print(f"\n🚀 Executando: {caminho_relativo}...\n")
    try:
        subprocess.run([sys.executable, caminho_absoluto], cwd=diretorio, check=True)
    except KeyboardInterrupt:
        print("\n\n⏹️ Execucao interrompida pelo usuario.")
    except Exception as e:
        print(f"\n❌ Erro durante a execucao: {e}")


def rodar_todos_os_testes():
    print("\n🧪 ========================================================")
    print("   RODANDO A SUÍTE DE TESTES DOS DEGRAUS 1, 2 E 3")
    print("========================================================\n")
    testes = [
        ("Degrau 1 (Imperativo & Arquivo JSON)", "degrau_1_imperativo_json/test_degrau_1.py"),
        ("Degrau 2 (Primeira Classe POO)", "degrau_2_primeira_classe/test_degrau_2.py"),
        ("Degrau 3 (Banco SQLite3 Relacional)", "degrau_3_sqlite/test_degrau_3.py"),
    ]
    for nome, arq in testes:
        print(f"▶️ Testando {nome}...")
        caminho_abs = os.path.join(BASE_DIR, arq)
        diretorio = os.path.dirname(caminho_abs)
        res = subprocess.run([sys.executable, caminho_abs], cwd=diretorio)
        if res.returncode == 0:
            print(f"✅ {nome}: PASSOU!\n")
        else:
            print(f"❌ {nome}: FALHOU!\n")


def menu_hub():
    while True:
        print("\n========================================================")
        print("🏢 MINIPROJETO 09: MANUTENÇÃO PREDIAL (HUB)")
        print("========================================================")
        print("1. 🪜 Executar Degrau 1 (Imperativo & Arquivo .json)")
        print("2. 🪜 Executar Degrau 2 (Primeira Classe POO: class Chamado)")
        print("3. 🪜 Executar Degrau 3 (Banco de Dados SQLite3 com Transições)")
        print("4. 🪜 Executar Degrau 4 (Interface Web com Streamlit)")
        print("5. 🧪 Rodar Todos os Testes Automatizados (Degraus 1, 2 e 3)")
        print("6. 🚪 Sair")
        print("========================================================")
        opcao = input("Escolha o degrau que deseja executar (1-6): ").strip()

        if opcao == "1":
            rodar_script("degrau_1_imperativo_json/main.py")
        elif opcao == "2":
            rodar_script("degrau_2_primeira_classe/main.py")
        elif opcao == "3":
            rodar_script("degrau_3_sqlite/main.py")
        elif opcao == "4":
            rodar_script("degrau_4_interface_streamlit/main.py")
        elif opcao == "5":
            rodar_todos_os_testes()
        elif opcao == "6":
            print("👋 Encerrando o launcher. Bons estudos!")
            break
        else:
            print("❌ Opcao invalida. Digite de 1 a 6.")


if __name__ == "__main__":
    menu_hub()

🧪 Validação com Testes Automatizados

# Executar a suíte de testes do Miniprojeto 09:
python -c "import subprocess, sys; [subprocess.run([sys.executable, p], check=True) for p in ['catalogo_projetos/sistemas_python_intro_09_manutencao_predial/degrau_1_imperativo_json/test_degrau_1.py', 'catalogo_projetos/sistemas_python_intro_09_manutencao_predial/degrau_2_primeira_classe/test_degrau_2.py', 'catalogo_projetos/sistemas_python_intro_09_manutencao_predial/degrau_3_sqlite/test_degrau_3.py']]"

# Ou, de forma ainda mais simples, use o Hub Central (opção 5 do menu):
python main.py

Resultado obtido:

Ran 1 test in 0.003s OK (Degrau 1)
Ran 2 tests in 0.005s OK (Degrau 2)
Ran 1 test in 0.038s OK (Degrau 3)


🏛️ Arquitetura Visual & Engenharia do Sistema

A imagem abaixo ilustra a esteira evolutiva dos 4 degraus didáticos do miniprojeto:

Arquitetura da Aplicação em 4 Degraus

flowchart TD
    subgraph S1["🪜 Degrau 1: Script Imperativo"]
        A1["Menu Terminal while"] --> A2["Arquivo de Texto/JSON"]
    end
    subgraph S2["🪜 Degrau 2: Orientação a Objetos"]
        B1["Modelo de Domínio POO"] --> B2["Encapsulamento de Regras"]
    end
    subgraph S3["🪜 Degrau 3: Banco Relacional"]
        C1["SQLite3 Local"] --> C2["Transações ACID & Agregações"]
    end
    subgraph S4["🪜 Degrau 4: Interface Visual"]
        D1["Aplicação Desktop / Web"] --> D2["UX Interativa Final"]
    end

    S1 --> S2
    S2 --> S3
    S3 --> S4

    style S1 fill:#fffde7,stroke:#fbc02d,stroke-width:1.5px
    style S2 fill:#e3f2fd,stroke:#1976d2,stroke-width:1.5px
    style S3 fill:#e8f5e9,stroke:#388e3c,stroke-width:1.5px
    style S4 fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1.5px

🎯 Engenharia de Requisitos & Modelagem Formal

👤 User Stories (Histórias de Usuário)

ID Persona História de Usuário Critério de Aceite Prioridade
US-01 Síndico / Morador Desejo abrir chamados de manutenção predial com local e gravidade em JSON Persistir chamados com protocolo único em chamados.json Alta
US-02 Zelador / Técnico Desejo atualizar o estado do chamado através de métodos da classe POO Classe Chamado com transições de status (Aberto -> Em Atendimento -> Concluído) Alta
US-03 Administradora de Condomínio Desejo histórico relacional no SQLite com SLA e custos de reparo Tabelas de condomínios, unidades e manutenções com índices por status Alta
US-04 Gestor Predial Desejo dashboard Streamlit com painel Kanban de chamados e métricas Painel web interativo com filtros por bloco, gravidade e tempo de resposta Alta
US-05 Auditor Condominial Desejo verificar que chamados fechados registrem a data de resolução Testes unitários garantindo regras de finalização e consistência de status Média

⚙️ Requisitos Funcionais (RF)

ID Descrição do Requisito Criticidade
RF-01 Geração e persistência de ordens de serviço prediais em arquivo JSON Alta
RF-02 Máquina de estados finitos encapsulada na classe Chamado para controle de SLA Alta
RF-03 Estrutura relacional SQLite com consultas agrupadas por urgência e custo Alta
RF-04 Visualizador Web Streamlit com quadros de acompanhamento e formulários Alta
RF-05 Launcher de execução unificado e validações com unittest Média

🛡️ Requisitos Não-Funcionais (RNF)

ID Especificação Arquitetural Criticidade
RNF-01 Integridade transacional no SQLite com rollback em falhas de escrita Alta
RNF-02 Tempo de resposta imediato na atualização de status de chamados Média
RNF-03 Interface visual moderna e acessível para equipes de zeladoria Média
RNF-04 Conformidade com padrões PEP 8 e documentação semântica Alta
RNF-05 Isolamento independente de código entre os 4 degraus didáticos Alta

🔗 Matriz de Rastreabilidade (RTM)

User Story Requisito Funcional Requisito Não-Funcional Módulo / Artefato de Código Status de Homologação
US-01 RF-01 RNF-01, RNF-04 degrau_1_imperativo_json/main.py Testado
US-02 RF-02 RNF-04, RNF-05 degrau_2_primeira_classe/main.py Testado
US-03 RF-03 RNF-01, RNF-02 degrau_3_sqlite/main.py Testado
US-04 RF-04 RNF-02, RNF-03 degrau_4_interface_streamlit/app.py Testado
US-05 RF-05 RNF-01, RNF-05 main.py & test_degrau_*.py Testado

🏁 Checkpoints de Qualidade & Homologação

Checkpoint Escopo da Validação Artefato Verificado Status
CP-01 Script Imperativo & Persistência Inicial degrau_1_* PASS ✅
CP-02 Modelagem Orientada a Objetos degrau_2_* PASS ✅
CP-03 Integridade Relacional no SQLite degrau_3_* PASS ✅
CP-04 Interface Visual & Usabilidade degrau_4_* PASS ✅
CP-05 Hub Launcher Central & Suíte de Testes main.py + test_*.py PASS ✅