diff --git a/.gitignore b/.gitignore index ca4a880..0a3d2e3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,16 @@ build/ deploy_release/ foton_system/config/settings.json + +.obsidian/ + +# Test Artifacts & Temporary Files +test_*.txt +test_*.xlsx +test.xlsx +*.bak +GERADO_* +tmp/ +.pytest_cache/ +.coverage +htmlcov/ diff --git a/README.md b/README.md index 2e743a4..c48e04b 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,22 @@ -# FOTON System 💡 +# 💡 FOTON System > **Transforme o Caos de Arquivos em uma Máquina de Gestão.** O FOTON System organiza, sincroniza e automatiza seu escritório de arquitetura, eliminando o tempo perdido procurando arquivos e gerando documentos. ---- - -## 📚 Navegação Rápida (Obsidian Vault) - -### 🎯 **[👉 COMECE AQUI: DOCUMENTATION_INDEX.md](DOCUMENTATION_INDEX.md)** ← Mapa completo de tudo! - -### 🎯 Para Começar - -- [[UserGuide|📖 Guia do Usuário]] - Manual completo (Modo Visual & Turbo TUI) -- [[deployment_guide|🚀 Guia de Instalação]] - Instale o Executável (.exe) -- [[DEPLOYMENT_USER_GUIDE|💾 Implantação e Backup]] - Ferramenta nova! Base de dados inteligente -- [[mcp_guide|🤖 Integração com IA]] - Controle por voz/texto (Claude/Cursor) +## 📚 Documentação (Acesso Rápido) -### 🧠 Entendendo o Sistema +### 🏛️ Para Agentes de IA +- [[LlmProtocol|📜 Protocolo de Documentação]] - **LEITURA OBRIGATÓRIA PARA AGENTES** +- [[Index|🗺️ Mapa de Conteúdo (MOC)]] - Navegação por domínios +- [[LlmContext|🧠 Contexto Geral para LLMs]] - Identidade do sistema +- [[SystemManifest|📋 Manifesto do Sistema]] - Visão geral técnica -- [[Pipelines|🔄 Como a Mágica Acontece]] - Fluxo de dados simplificado -- [[concepts|🏗️ Arquitetura do Sistema]] - Conceitos técnicos (Hexagonal) -- [[DataModel|📊 Modelo de Dados]] - Estrutura de arquivos e DB - -### 👨‍💻 Para Desenvolvedores - -- [[AI_INTEGRATION_REPORT|🤖 Relatório de IA]] - Como a IA se integra -- [[AGENTIC_SPRINT_PLAN|📋 Planejamento Agentic]] - Sprints e roadmap -- [[workplan|📅 Plano de Trabalho]] - Tarefas e milestones +### 🎯 Para o Arquiteto (Usuário) +- [[UserGuide|📖 Guia do Usuário]] - Manual completo +- [[DeploymentUserGuide|💾 Implantação e Backup]] - Guia de segurança de dados +- [[TuiGuide|📟 Guia do Modo Terminal]] - Produtividade turbo +- [[QuickReference|📑 Referência Rápida]] - Comandos e atalhos --- @@ -43,11 +32,11 @@ Um dia, você precisa gerar 5 propostas urgentes. Você abre a pasta do cliente ### A Solução -Você instala o FOTON. (Veja [[deployment_guide|como instalar]]) +Você instala o FOTON. (Veja [[DeploymentGuide|como instalar]]) -1. **Sincronização Mágica**: Com um clique, o FOTON lê suas pastas e arruma seu Excel. "J. Silva" e "João Silva" viram a mesma pessoa. ([[Pipelines#Sincronização|Como funciona]]) +1. **Sincronização Mágica**: Com um clique, o FOTON lê suas pastas e arruma seu Excel. "J. Silva" e "João Silva" viram a mesma pessoa. ([[Pipelines|Como funciona]]) 2. **Centros de Verdade**: O FOTON cria um arquivo `INFO-CLIENTE.md` dentro da pasta do João. Agora, os dados moram onde o projeto mora. ([[DataModel|Entenda a estrutura]]) -3. **Automação**: Para gerar as 5 propostas, você só digita o valor. O FOTON puxa o nome, endereço e CPF do João automaticamente e gera o PDF. Sem erro de digitação. ([[UserGuide#Geração de Documentos|Veja como]]) +3. **Automação**: Para gerar as 5 propostas, você só digita o valor. O FOTON puxa o nome, endereço e CPF do João automaticamente e gera o PDF. Sem erro de digitação. ([[UserGuide|Veja como]]) ### O Retorno a Produtividade @@ -61,29 +50,29 @@ Você gastou 10 minutos no que levaria 2 horas. Seus arquivos estão organizados > "O Fim do 'Onde Salvei?'" -- **Sincronização Bidirecional**: O que está na pasta vai para o Excel, e vice-versa. ([[Pipelines#Sincronização|Veja o fluxo]]) -- **Banco de Dados Distribuído**: Seus dados vivem nas pastas, em arquivos de texto simples (`INFO-*.md`). Leves, seguros e fáceis de editar. ([[DataModel#Centros de Verdade|Saiba mais]]) +- **Sincronização Bidirecional**: O que está na pasta vai para o Excel, e vice-versa. ([[Pipelines|Veja o fluxo]]) +- **Banco de Dados Distribuído**: Seus dados vivem nas pastas, em arquivos de texto simples (`INFO-*.md`). Leves, seguros e fáceis de editar. ([[DataModel|Saiba mais]]) ### 2. Geração de Documentos > "Adeus, Ctrl+C Ctrl+V" -- **Context-Aware**: O sistema sabe quem é o cliente pela pasta onde você está. ([[concepts#Context-Aware Engine|Entenda a lógica]]) -- **Templates Inteligentes**: Use seus modelos de Word e PowerPoint. O sistema preenche as lacunas (`@nome`, `@valor`) para você. ([[UserGuide#Geração de Documentos|Tutorial completo]]) +- **Context-Aware**: O sistema sabe quem é o cliente pela pasta onde você está. ([[Concepts|Entenda a lógica]]) +- **Templates Inteligentes**: Use seus modelos de Word e PowerPoint. O sistema preenche as lacunas (`@nome`, `@valor`) para você. ([[UserGuide|Tutorial completo]]) ### 3. Integração com IA > "Seu assistente que nunca esquece nada" -- **Controle por Voz/Texto**: Use Claude ou Cursor para gerenciar o escritório em linguagem natural. ([[mcp_guide|Configure em 2 minutos]]) -- **Memória Vetorial (RAG)**: Pergunte "O que sabemos sobre projetos residenciais?" e a IA busca em todos os seus documentos. ([[AI_INTEGRATION_REPORT|Como funciona]]) +- **Controle por Voz/Texto**: Use Claude ou Cursor para gerenciar o escritório em linguagem natural. ([[McpGuide|Configure em 2 minutos]]) +- **Memória Vetorial (RAG)**: Pergunte "O que sabemos sobre projetos residenciais?" e a IA busca em todos os seus documentos. ([[AiIntegrationReport|Como funciona]]) ### 4. Modo Avançado (Ferramentas Administrativas) > "Para quando você precisa de super poderes" -- **Refatoração de Dados**: Mudou o nome de uma variável? O sistema atualiza todos os seus arquivos de uma vez. ([[UserGuide#Schema Manager|Veja como]]) -- **Diagnóstico**: Um "Check-up" completo para garantir que nenhuma pasta está perdida ou sem dono. ([[UserGuide#Diagnóstico|Entenda]]) +- **Refatoração de Dados**: Mudou o nome de uma variável? O sistema atualiza todos os seus arquivos de uma vez. ([[UserGuide|Veja como]]) +- **Diagnóstico**: Um "Check-up" completo para garantir que nenhuma pasta está perdida ou sem dono. ([[UserGuide|Entenda]]) --- @@ -97,8 +86,8 @@ Baixe o instalador na aba **Releases** do GitHub e rode. Pronto! ```bash pip install -r requirements.txt -python foton_system/interfaces/cli/main.py --tui # Modo Turbo (Terminal) -python foton_system/interfaces/cli/main.py --gui # Modo Visual (Janelas) +python -m foton_system.main --tui # Modo Turbo (Terminal) +python -m foton_system.main --gui # Modo Visual (Janelas) ``` Use `foton --info` para ver onde seus dados estão salvos. @@ -112,13 +101,13 @@ graph TD README[📄 README] --> UserGuide[📖 User Guide] README --> Pipelines[🔄 Pipelines] README --> deployment[🚀 Deploy Guide] - + UserGuide --> TUI[📟 TUI Guide] UserGuide --> mcp[🤖 MCP Guide] - + Pipelines --> concepts[🏗️ Concepts] concepts --> MCPServices[⚡ MCP Services Layer] - + deployment --> workplan[📅 Work Plan] ``` @@ -126,11 +115,11 @@ graph TD ## 📖 Leia Também -- [[concepts|Conceitos de Arquitetura]] - Entenda a Arquitetura Hexagonal +- [[Concepts|Conceitos de Arquitetura]] - Entenda a Arquitetura Hexagonal - [[Pipelines|Pipelines do Sistema]] - Visualize o fluxo de dados - [[DataModel|Modelo de Dados]] - Como os dados estão organizados -- [[AI_INTEGRATION_REPORT|IA no FOTON]] - Como a inteligência artificial ajuda -- [[workplan|Plano de Trabalho]] - Roadmap e funcionalidades planejadas +- [[AiIntegrationReport|IA no FOTON]] - Como a inteligência artificial ajuda +- [[WorkPlan|Plano de Trabalho]] - Roadmap e funcionalidades planejadas --- diff --git a/docs/00_META/ADR/ADR001_ParaZettelkastenDoc.md b/docs/00_META/ADR/ADR001_ParaZettelkastenDoc.md new file mode 100644 index 0000000..14e2b84 --- /dev/null +++ b/docs/00_META/ADR/ADR001_ParaZettelkastenDoc.md @@ -0,0 +1,27 @@ +--- +type: adr +domain: core +status: accepted +date: 2026-05-11 +--- +# ADR001: Adoção do Modelo PARA + Zettelkasten para Documentação + +## Status +Aceito + +## Contexto +A documentação anterior do FOTON System estava dispersa, com links quebrados e sem uma estrutura clara de evolução. Isso dificultava tanto o uso por humanos quanto a navegação por agentes de IA, aumentando a entropia do repositório. + +## Decisão +Adotar uma estrutura híbrida baseada em: +1. **PARA (Projects, Areas, Resources, Archives):** Para categorizar o ciclo de vida da informação. +2. **Zettelkasten:** Para criar um grafo de conhecimento interligado por links bi-direcionais (`[[link]]`) e metadados (Frontmatter YAML). + +## Consequências +- **Positivas:** Maior rastreabilidade, facilidade de onboarding para IAs, histórico de sprints preservado. +- **Negativas:** Requer rigor na manutenção dos metadados e na localização dos arquivos. + +--- +## 🔗 Links Relacionados +- Índice de ADRs: [[Index]] +- Protocolo: [[LlmProtocol]] diff --git a/docs/00_META/ADR/ADR002_PascalCaseNaming.md b/docs/00_META/ADR/ADR002_PascalCaseNaming.md new file mode 100644 index 0000000..753fa72 --- /dev/null +++ b/docs/00_META/ADR/ADR002_PascalCaseNaming.md @@ -0,0 +1,26 @@ +--- +type: adr +domain: core +status: accepted +date: 2026-05-11 +--- +# ADR002: Padronização de Nomenclatura PascalCase para Documentos + +## Status +Aceito + +## Contexto +Arquivos Markdown na pasta `docs/` seguiam múltiplos padrões (snake_case, UPPER_CASE, camelCase), o que gerava inconsistências nos links bi-direcionais e dificultava a predição de caminhos por scripts e IAs. + +## Decisão +Padronizar todos os nomes de arquivos de documentação para **PascalCase** (ex: `UserGuide.md`). +Pastas meta e de projeto mantêm o prefixo numérico ou descritivo em snake_case para ordenação no sistema de arquivos. + +## Consequências +- **Positivas:** Coesão visual, links previsíveis, conformidade com padrões de "Wikis" modernas. +- **Negativas:** Necessidade de renomear arquivos existentes e atualizar todas as referências internas. + +--- +## 🔗 Links Relacionados +- Protocolo: [[LlmProtocol]] +- Índice: [[Index]] diff --git a/docs/00_META/ADR/ADR003_SandboxTestIsolation.md b/docs/00_META/ADR/ADR003_SandboxTestIsolation.md new file mode 100644 index 0000000..54bfdae --- /dev/null +++ b/docs/00_META/ADR/ADR003_SandboxTestIsolation.md @@ -0,0 +1,20 @@ +# ADR 003: Isolamento de Testes via Modo Sandbox + +## Status +Proposto + +## Contexto +O Foton System manipula dados críticos de escritórios de arquitetura (Excel, arquivos Markdown de clientes e documentos legais). A execução de testes (Unitários, Integração ou E2E) no ambiente de desenvolvimento corre o risco de corromper ou misturar dados de teste com dados reais, especialmente em máquinas onde o sistema está instalado para uso produtivo. + +## Decisão +Fica estabelecido que **todos os testes automatizados** devem obrigatoriamente operar em **Modo Sandbox**. + +1. **Ativação Obrigatória:** Todo `setUpClass` de suítes de teste de integração ou E2E deve chamar `PathManager.set_sandbox_mode(True)`. +2. **Isolamento de Diretório:** Os testes devem utilizar um subdiretório exclusivo dentro da pasta temporária do sistema (ex: `%TEMP%/foton_tests_XXXX`) para garantir que execuções paralelas não interfiram entre si. +3. **Limpeza Automática:** O `tearDownClass` deve ser responsável por limpar o diretório temporário e desativar o modo sandbox (`set_sandbox_mode(False)`). +4. **Configuração Volátil:** Os testes não devem ler o `settings.json` real do usuário. O `Config()` deve ser reinicializado ou sobreposto com caminhos apontando para o sandbox. + +## Consequências +- **Segurança:** Risco zero de deleção ou modificação acidental de dados reais do usuário durante o desenvolvimento. +* **Reprodutibilidade:** Testes rodam em um ambiente "limpo" e controlado, independente da máquina onde estão sendo executados. +* **Complexidade:** Requer um boilerplate consistente em todos os arquivos de teste para garantir que o `PathManager` e o `Config` estejam devidamente isolados. diff --git a/CONTRIBUTING.md b/docs/00_META/Contributing.md similarity index 90% rename from CONTRIBUTING.md rename to docs/00_META/Contributing.md index a95efdb..ecff665 100644 --- a/CONTRIBUTING.md +++ b/docs/00_META/Contributing.md @@ -1,4 +1,10 @@ -# Contribuindo para o FOTON System +--- +type: guide +domain: core +status: active +tags: [contributing, development, collaboration] +--- +# Contribuindo para o FOTON System (Contributing) Obrigado pelo interesse em contribuir para o FOTON System! 🎉 Este documento define as diretrizes para garantir que a colaboração seja produtiva e organizada. @@ -85,3 +91,9 @@ Ao abrir uma Issue, por favor inclua: * Screenshots ou logs de erro. Obrigado por ajudar a construir o FOTON System! 🚀 + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Manifesto: [[SystemManifest]] +- Protocolo: [[LlmProtocol]] diff --git a/docs/00_META/Dictionary.md b/docs/00_META/Dictionary.md new file mode 100644 index 0000000..203dc26 --- /dev/null +++ b/docs/00_META/Dictionary.md @@ -0,0 +1,35 @@ +--- +type: concept +domain: core +status: active +tags: [meta, dictionary, ddd, terms] +--- +# 📖 Dicionário de Domínios e Termos (Dictionary) + +Este documento serve como a **Ubiquitous Language** (DDD) do FOTON System. Aqui definimos o que cada termo significa para garantir que Humanos e IAs falem a mesma língua. + +## 🏛️ Termos de Arquitetura + +- **Centro de Verdade (Center of Truth):** Arquivo `.md` (INFO-CLIENTE, INFO-SERVICO) que detém a autoridade sobre os dados de um projeto. +- **RalphLoop:** Ciclo agêntico de "Pesquisa -> Plano -> Ação -> Validação". +- **Hexagonal Architecture:** Padrão que isola a lógica de negócio (Core) de implementações externas (Adapters). +- **Zettelkasten + PARA:** Sistema de organização de notas interligadas por grafos e esferas de responsabilidade. + +## 👥 Termos de Negócio (Escritório) + +- **Cliente:** Pessoa ou entidade que contrata os serviços de arquitetura. +- **Serviço (Service):** Um sub-projeto ou demanda específica vinculada a um cliente (ex: Projeto Executivo, Consultoria). +- **Template:** Modelo de documento (Word/PowerPoint) com variáveis `@tags`. +- **CUB (Custo Unitário Básico):** Índice usado para estimativas de custos de construção. + +## 🤖 Termos Técnicos + +- **MCP (Model Context Protocol):** Protocolo de comunicação entre o Foton e Assistentes de IA. +- **TUI (Terminal User Interface):** Interface de navegação via teclado no terminal. +- **Frontmatter:** Bloco de metadados YAML no início dos arquivos Markdown. + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Protocolo: [[LlmProtocol]] +- Contexto LLM: [[LlmContext]] diff --git a/docs/00_META/Index.md b/docs/00_META/Index.md new file mode 100644 index 0000000..111a7d6 --- /dev/null +++ b/docs/00_META/Index.md @@ -0,0 +1,60 @@ +--- +type: index +domain: core +status: active +tags: [map, index, toc] +--- +# 🗺️ Mapa de Conteúdo (Index) + +Bem-vindo ao centro nervoso da documentação do **FOTON System**. Este índice organiza o conhecimento do sistema seguindo a metodologia PARA + Zettelkasten. + +## 🏛️ Esferas de Conhecimento + +### 00_META - Metadados e Protocolos +- [[LlmProtocol]] - Regras para IAs e Agentes. +- [[LlmContext]] - Contexto geral para LLMs e identidade do sistema. +- [[SystemManifest]] - Visão técnica e manifesto do sistema. +- [[Contributing]] - Guia de contribuição para o projeto. +- [[Dictionary]] - Glossário de termos técnicos e de negócio. + +#### ADRs (Architectural Decision Records) +- [[ADR001_ParaZettelkastenDoc]] - Adoção do PARA + Zettelkasten. +- [[ADR002_PascalCaseNaming]] - Padronização PascalCase. + +### 01_PROJECTS - Sprints e Esforços Ativos +- [[PlanDocRefactor]] - **Sprint Atual:** Refatoração da Documentação. +- [[SprintSandbox]] - Testes e experimentações em ambiente controlado. +- [[AgenticSprintPlan]] - Planejamento original da evolução agêntica. +- [[WorkPlan]] - Plano de masterização de frameworks. + +### 02_AREAS - Domínios e Regras Perenes (DDD) +- [[Concepts]] - Arquitetura Hexagonal e padrões. +- [[Pipelines]] - Fluxos de dados do sistema. +- [[DataModel]] - Modelo de dados e Centros de Verdade. +- [[BackupStrategySummary]] - Estratégia de backup e resiliência de dados. +- [[DatabaseFlowDiagram]] - Diagramas de inicialização de base. +- [[DatabaseInitializationSolution]] - Soluções para base ausente. + +### 03_RESOURCES - Manuais e Recursos +- [[UserGuide]] - Guia do Usuário (Arquiteto). +- [[QuickReference]] - Atalhos e comandos rápidos. +- [[TuiGuide]] - Guia do Modo Terminal. +- [[McpGuide]] - Guia de Integração com IA via MCP. +- [[DocsMcp]] - Documentação técnica específica para MCP. +- [[DeploymentGuide]] - Guia de Deploy (Devs). +- [[DeploymentUserGuide]] - Guia de Implantação para o Usuário. +- [[TestQualityReport]] - Relatório de qualidade de testes. + +### 04_ARCHIVES - Histórico e Releases +- [[Release_v1.1.0]] - Notas da versão 1.1.0. +- [[AiIntegrationReport]] - Relatório inicial de integração IA. +- [[DocumentationAudit]] - Auditoria inicial da estrutura de documentação. + +--- +## 🏷️ Navegação Rápida por Tags +- #architecture | #feature | #mcp | #guide | #backup + +--- +**Links Relacionados:** +- [[README]] +- [[LlmProtocol]] diff --git a/LLM_CONTEXT.md b/docs/00_META/LlmContext.md similarity index 79% rename from LLM_CONTEXT.md rename to docs/00_META/LlmContext.md index 6dfffdb..b027f83 100644 Binary files a/LLM_CONTEXT.md and b/docs/00_META/LlmContext.md differ diff --git a/docs/00_META/LlmProtocol.md b/docs/00_META/LlmProtocol.md new file mode 100644 index 0000000..7c85ebf --- /dev/null +++ b/docs/00_META/LlmProtocol.md @@ -0,0 +1,61 @@ +--- +type: guide +domain: core +status: active +tags: [meta, protocol, llm, documentation] +--- +# 📜 Protocolo de Documentação Agêntica (LlmProtocol) + +Este documento define as regras de engajamento para Humanos e IAs (Agentes) ao interagir com o repositório **FOTON System**. O objetivo é garantir a integridade do conhecimento, a rastreabilidade das decisões e a resiliência do ecossistema. + +## 🏛️ Arquitetura PARA + Zettelkasten + +A documentação é organizada em quatro esferas principais (Método PARA) e interligada por grafos (Zettelkasten). + +1. **00_META:** Regras do sistema, índices (MOCs) e glossários. +2. **01_PROJECTS:** Esforços ativos (Sprints). Contém o "como e quando" as coisas foram feitas. **Imutável após o fechamento da Sprint.** +3. **02_AREAS:** Conhecimento perene e regras de domínio (DDD). Contém o "o que é" o sistema hoje. **Evolutivo.** +4. **03_RESOURCES:** Manuais, guias, templates e materiais de referência. +5. **04_ARCHIVES:** Histórico de releases e documentos depreciados. + +## 🤖 Regras para Agentes (LLMs) + +### 1. RalphLoop & Spec-Driven Development +Antes de iniciar qualquer tarefa de código, a IA deve: +- Consultar a Sprint ativa em `01_PROJECTS/`. +- Validar se o `PLAN_SXX.md` possui os Specs necessários. +- Atualizar o Checklist conforme avança. +- Registrar falhas e sucessos de testes no `REPORT_SXX.md`. + +### 2. Frontmatter Obrigatório +Todo arquivo `.md` na pasta `docs/` **DEVE** começar com um bloco YAML: +```yaml +--- +type: concept | spec | plan | report | guide +domain: core | clients | documents | finance | infra +status: draft | active | deprecated +tags: [tag1, tag2] +--- +``` + +### 3. Navegação por Hiperlinks +- Use `[[NomeDoArquivo]]` para links internos (padrão Obsidian/Wiki). +- No final de cada documento de Área ou Recurso, adicione uma seção `## 🔗 Links Relacionados`. + +### 4. Manutenção de Baixa Entropia +- **NUNCA** refatore arquivos de Sprints passadas. Eles são o log histórico. +- Ao atualizar uma regra de negócio, altere o arquivo correspondente em `02_AREAS/`. +- Se um documento se tornar obsoleto, mova-o para `04_ARCHIVES/Deprecated/` em vez de deletar. + +### 5. Padronização de Nomenclatura +- **Pastas:** `00_SNAKE_CASE` (prefixo numérico + caixa alta). +- **Arquivos de Documentação:** `PascalCase.md` (ex: `UserGuide.md`, `ClientDataModel.md`). +- **Arquivos de Código:** Seguir PEP8 (snake_case.py). +- **Documentos Gerados (Saída):** `02-COD_DOC_TIPO_VER_REV_NOME.ext`. + +> [!DIDACTIC:META] Ordem sobre o Caos: A padronização de nomes (PascalCase) e pastas (PARA) não é estética. Ela permite que Agentes de IA encontrem o contexto correto em milissegundos, economizando tokens e evitando alucinações. + +--- +## 🔗 Links Relacionados +- Índice Principal: [[Index]] +- Contexto LLM: [[LlmContext]] diff --git a/SYSTEM_MANIFEST.md b/docs/00_META/SystemManifest.md similarity index 57% rename from SYSTEM_MANIFEST.md rename to docs/00_META/SystemManifest.md index 8c3bbb5..4c71625 100644 --- a/SYSTEM_MANIFEST.md +++ b/docs/00_META/SystemManifest.md @@ -18,22 +18,45 @@ O Agente **NÃO** deve manipular DOCX/PPTX diretamente. O Agente deve manipular ## 2. Formatação de Dados (IMPORTANTE) -O sistema possui um **Middleware de Formatação Automática**. -O Agente deve fornecer **NÚMEROS PUROS** ou **DATA ISO** sempre que possível. O sistema formata para o padrão brasileiro (R$ X.XXX,XX) automaticamente. +O sistema possui um **Middleware de Formatação Automática**. +O Agente deve fornecer **NÚMEROS PUROS** para cálculos e **TEXTO ENTRE ASPAS** para literais que não devem ser formatados (como anos ou códigos). -### Regras de Tipagem +### Regras de Tipagem e Bypass +> [!DIDACTIC:FORMATACAO] Dica de Ouro: Use aspas para anos e códigos (ex: "2026"). Isso evita que o sistema coloque vírgulas e pontos decimais em números que não são medidas. + +> [!DIDACTIC:IA] Transparência: O Foton não "chuta" dados. Se uma variável @ não estiver no arquivo INFO, o sistema deixará o campo em branco ou exibirá um aviso. Mantenha seu INFO completo! + +> [!DIDACTIC:SSOT] Camadas de Dados: O sistema busca dados primeiro no arquivo do documento, depois na pasta do Serviço e por fim na do Cliente. Isso permite sobrescrever @emails ou @telefones específicos para um contrato sem mudar o cadastro global do cliente. + +* **Números Decimais (Default):** Qualquer sequência de números puros (ex: `2026`) será interpretada como valor decimal e formatada (ex: `2.026,00`). Use para áreas, valores e quantidades. +* **Bypass Literal (Aspas):** Use aspas para que o sistema ignore a formatação decimal. + * *Input:* `@anoProjeto: "2026"` -> *Output:* `2026` + * *Input:* `@numeroProposta: "001"` -> *Output:* `001` * **Dinheiro:** Se a chave contiver `valor`, `custo`, `total`, `preco`, `cub`, `exec` -> O sistema adiciona `R$` e formata. * *Input:* `@valorProposta: 5000.50` * *Output no Doc:* `R$ 5.000,50` * **Áreas:** Se a chave contiver `area`, `aceqv` -> O sistema formata com pontos e vírgulas. * *Input:* `@areaTotal: 1234.5` * *Output no Doc:* `1.234,50` +* **Percentuais:** Chaves terminadas em `%` convertem 0.14 para 14,00%. + * *Input:* `@Honorarios%: 0.05` + * *Output no Doc:* `5,00%` * **Texto:** Texto é mantido como está. --- -## 3. Variáveis de Sistema (Automáticas) +## 3. Motor de Interface (TUILayout) + +O Foton utiliza um motor de renderização dinâmico em `foton_system/interfaces/cli/views/tui_layout.py` para garantir uma experiência coesa e profissional. + +* **Responsividade:** A largura da interface se adapta automaticamente à janela do terminal (40-100 colunas). +* **Visible Width Logic:** Compensação para Emojis (2 colunas) e limpeza de ANSI (0 colunas) para manter bordas sólidas. +* **Ensino Contextual:** Integração com o `TipService` para exibir dicas `[!DIDACTIC]` específicas para cada etapa do fluxo. + +--- + +## 4. Variáveis de Sistema (Automáticas) Não é necessário preencher estas variáveis manualmente. O sistema injeta automaticamente: @@ -43,6 +66,8 @@ Não é necessário preencher estas variáveis manualmente. O sistema injeta aut | `@LinkCUB` | Link direto para o PDF do CUB do mês anterior | `.../cub-dezembro-2025.pdf` | | `@ReferenciaCUB` | Rótulo do CUB utilizado | `Dezembro/2025` | +> [!DIDACTIC:GERAL] Automação de Datas: A @DataAtual é gerada no momento da emissão do documento. Você nunca mais enviará uma proposta com a data de ontem! + --- ## 4. Estrutura de Templates (KIT DOC) diff --git a/docs/AGENTIC_SPRINT_PLAN.md b/docs/01_PROJECTS/AgenticSprintPlan.md similarity index 90% rename from docs/AGENTIC_SPRINT_PLAN.md rename to docs/01_PROJECTS/AgenticSprintPlan.md index 7faa77a..fc4a6f4 100644 --- a/docs/AGENTIC_SPRINT_PLAN.md +++ b/docs/01_PROJECTS/AgenticSprintPlan.md @@ -1,4 +1,10 @@ -# 🚀 FOTON System: Plano de Evolução Agêntica (v2) +--- +type: plan +domain: core +status: active +tags: [agentic, roadmap, future] +--- +# 🚀 FOTON System: Plano de Evolução Agêntica (v2) (AgenticSprintPlan) Este documento estabelece a arquitetura para a transição do FOTON de um sistema de gestão para um **Ecossistema Agêntico** de alta performance, operando em três níveis de profundidade. @@ -56,3 +62,9 @@ O objetivo desta sprint é dar "consciência" ao sistema sobre os dados disperso 1. **Prioridade ROS:** Sempre preferir POPs (`core/ops`) para ações. 2. **Escaping de Paths:** Todas as saídas de configuração devem usar strings seguras para JSON (Escape de barras `\\`). 3. **Privacidade AEC:** Dados sensíveis de arquitetura nunca saem da máquina do usuário. + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Contexto: [[LlmContext]] +- Protocolo: [[LlmProtocol]] diff --git a/docs/01_PROJECTS/SprintSandbox.md b/docs/01_PROJECTS/SprintSandbox.md new file mode 100644 index 0000000..7319103 --- /dev/null +++ b/docs/01_PROJECTS/SprintSandbox.md @@ -0,0 +1,35 @@ +# SPRINT: Desenvolvimento do Modo Sandbox (RalphLoop) + +## 🎯 Objetivo +Implementar um ambiente volátil e seguro que permita aos usuários e agentes de IA explorarem as funcionalidades do Foton System sem afetar os dados reais de produção (OneDrive). + +## 🛠️ Arquitetura Proposta +- **Ativação:** Via flag CLI `--sandbox` ou variável de ambiente `FOTON_SANDBOX=1`. +- **Isolamento:** Redirecionamento de todos os caminhos do `PathManager` para uma pasta temporária do SO. +- **Seeding:** Cópia automática de dados mínimos (templates de exemplo, clientes fictícios) para o ambiente temporário. +- **Feedback:** Alertas visuais na TUI e metadados no MCP/info_sistema. + +## 📋 Backlog de Tarefas (RalphLoop) + +### Fase 1: Fundação e TDD (Vermelho) +- [x] Criar teste unitário para `PathManager` validando redirecionamento em modo Sandbox. `test_path_manager_sandbox.py` +- [x] Criar teste de integração para o ciclo de vida do Sandbox (Início -> Redirecionamento -> Limpeza). + +### Fase 2: Implementação (Verde) +- [x] Modificar `PathManager` para suportar estado global `SANDBOX_MODE`. +- [x] Implementar `SandboxService` para gerenciar a criação e limpeza do diretório temporário. +- [x] Implementar lógica de "Seeding" (Cópia de recursos básicos). + +### Fase 3: Refatoração e Feedback (Azul) +- [x] Atualizar `info_sistema` para reportar o modo ativo. +- [x] Garantir que logs também sejam redirecionados no sandbox. + +--- + +## 🚀 Progresso Atual +- [x] Base limpa (Git verificado) +- [x] Documento de Sprint criado +- [x] Fase 1: Fundação e TDD (Vermelho -> Verde) +- [x] Fase 2: SandboxService e Seeding (Implementado) +- [x] Fase 3: Refatoração e Feedback (Concluído) +- [x] Todos os 132 testes passando (Regressão zero) diff --git a/docs/01_PROJECTS/Sprint_AgnosticOS/PlanAgnosticOS.md b/docs/01_PROJECTS/Sprint_AgnosticOS/PlanAgnosticOS.md new file mode 100644 index 0000000..407016f --- /dev/null +++ b/docs/01_PROJECTS/Sprint_AgnosticOS/PlanAgnosticOS.md @@ -0,0 +1,80 @@ +--- +type: plan +domain: core +status: active +tags: [sprint, architecture, refactor, cross-platform] +--- +# 🏃‍♂️ Sprint Plan: Arquitetura Agnóstica (OS/Environment) + +## 🎯 Objetivo +Transformar o **FOTON System** em um sistema verdadeiramente cross-platform e multi-ambiente. O sistema deve rodar perfeitamente como um microsserviço (Docker/Ubuntu Server) ou como uma aplicação Desktop rica (Windows/Mac), ativando ou ocultando recursos dinamicamente através do padrão Adapter e de um "Porteiro" de ambiente, sem falhas de importação (`ImportError`). + +## 🛠️ Especificações Técnicas (Specs) + +### Fase 1: O Porteiro (Environment Porter) +Criar um módulo central que identifica as capacidades reais do ambiente de execução. +* **Arquivo:** `foton_system/modules/shared/infrastructure/services/environment_porter.py` +* **Lógica:** + * Detectar SO (`os.name`, `platform.system()`). + * Detectar GUI (presença de DISPLAY e existência de socket X11 /tmp/.X11-unix/X0 ou execução no Windows/Mac Desktop). + * Detectar Docker (`os.path.exists('/.dockerenv')` /.dockerenv, /.dockerinit, e variáveis como DOCKER_HOST). + * Detectar MCP (`--mcp` via `sys.argv`). +* **Perfis Mapeados (SystemProfile):** `SERVER_HEADLESS`, `DESKTOP_GUI`, `DESKTOP_WSL`, DESKTOP_TUI`. +* **Método Chave:** `can_use_feature(feature_name: str) -> bool` (ex: can_use_native_dialogs validando presença do zenity). + +### Fase 2: Menus Dinâmicos e Condicionais +Refatorar a CLI para usar uma estrutura de dados de roteamento baseada em `SystemProfile`. +* **Arquivo:** `foton_system/interfaces/cli/menus.py` +* **Lógica:** + * O `MenuSystem` deve injetar o `EnvironmentPorter`. + * Substituir os menus "hardcoded" por dicionários/listas mapeados. + * Ocultar automaticamente a opção de "Preencher Ficha (Interface)" se `can_use_feature("webview")` for `False`. + * Ocultar opções de "Criar Atalhos" se `can_use_feature("shortcuts")` for `False`. + +### Fase 3: Padrões Adapter ("Porta dos Fundos") +Isolar e encapsular bibliotecas problemáticas (pywin32, pywebview, tkinter). Nenhuma dessas libs deve ser importada no escopo global. + +1. **IFormInterface (Web/GUI):** + * *Abstract:* Interface para captura rica de dados. + * *Adapters:* + * `WebViewFormFiller` (Tenta abrir o `webview`. Falha capturada faz fallback elegante para o Browser.), + * `BrowserFormFiller` (Usa webbrowser nativo do Python sendo o fallback) + * `TuiFormFiller` (modo terminal). + * *Desacoplamento:* O import do `webview` fica apenas dentro do `WebViewFormFiller`. +2. **IFileSelector (Buscas/Caminhos):** + * *Abstract:* Diálogos para salvar/abrir. + * *Adapters:* + * `CrossFileDialogSelector` (diálogos nativos limpos, ex: usando a lib leve `crossfiledialog` se adicionada e condicional se GUI disponível e zenity/kdialog presentes ou fallback pra tkinter encapsulado localmente) + * `TuiFileSelector` (terminal fallback absoluto). +3. **ISystemIntegrator (SO):** + * *Abstract:* Integrações específicas do SO (atalhos, registro). + * *Adapters:* + * WindowsIntegrator: Cria atalhos com winshell. + * LinuxIntegrator: Cria atalhos .desktop em ~/.local/share/applications/ usando xdg-desktop-menu. + * NullIntegrator: Ignora a integração (para Server/Docker). +### Fase 4: Gestão Otimizada de Dependências e Build Multi-OS +O sistema de dependências não deve punir instalações servidoras com "lixo" gráfico. + +1. **Requirements Modulares:** + * `requirements-core.txt`: `pandas`, `python-docx`, `python-pptx`, `mcp`, `chromadb`, etc. + * `requirements-desktop.txt`: `pywebview`, `pythonnet`, `pywin32` (instalado on-demand ou durante o build de desktop). +2. **Estratégia de Build (`build.py`):** + * Refatorar o pipeline para aceitar flags `--target=linux-server`, `--target=windows-desktop`. + * Opção de Pipelines duplos: Target Server (limpo) e Target Desktop (com hooks e hidden-imports explícitos como webview, winshell, crossfiledialog). + * Configurar o `PyInstaller` para empacotar apenas o necessário para cada target, criando bundles independentes em `dist/`. + * No Linux, usar script Bash nativo ou empacotamento `.tar.gz`. + +## 📦 Checklist de Execução + +- [ ] Criar o `EnvironmentPorter` (Fase 1). +- [ ] Criar estrutura de `requirements-*.txt` dividida (Fase 4). +- [ ] Implementar interfaces e classes Adapter para SO (`ISystemIntegrator`) e Forms (`IFormInterface`) (Fase 3). +- [ ] Refatorar os adapters UI (`IFileSelector` e provedores de UI) (Fase 3). +- [ ] Refatorar o `MenuSystem` para usar o Porteiro dinamicamente (Fase 2). +- [ ] Ajustar o `build.py` para suportar cross-platform e dependências condicionais (Fase 4). +- [ ] Executar suite de testes para garantir que quebras de import (`ImportError`) não afetem o core. + +--- +## 🔗 Links Relacionados +- Meta: [[LlmProtocol]] +- Arquitetura: [[LlmContext]] diff --git a/docs/01_PROJECTS/Sprint_AgnosticOS/ReportAgnosticOS.md b/docs/01_PROJECTS/Sprint_AgnosticOS/ReportAgnosticOS.md new file mode 100644 index 0000000..e56abd0 --- /dev/null +++ b/docs/01_PROJECTS/Sprint_AgnosticOS/ReportAgnosticOS.md @@ -0,0 +1,60 @@ +--- +type: report +domain: core +status: active +tags: [sprint, architecture, report, agnostic-os] +--- +# 📝 Report: Sprint Arquitetura Agnóstica (OS/Environment) + +## 📊 Status da Sprint +- **Início:** 14/05/2026 +- **Branch:** `feat/agnostic-architecture` +- **Progresso:** 100% + +## 🚀 RalphLoop - Ciclo de Desenvolvimento + +### Ciclo 1: EnvironmentPorter (Fase 1) +- **Status:** Concluído ✅ + +### Ciclo 2: Menus Dinâmicos (Fase 2) +- **Status:** Concluído ✅ + +### Ciclo 3: Padrões Adapter (Fase 3) +- **Status:** Concluído ✅ + +### Ciclo 4: Dependências e Build (Fase 4) +- **Status:** Concluído ✅ +- **Checklist:** + - [x] Criação de requirements-core.txt (Server) + - [x] Criação de requirements-desktop.txt (Desktop) + - [x] Refatoração do build.py com suporte a --target + - [x] Hidden-imports explícitos para adapters dinâmicos + +## 🧪 Registro de Testes + +| Data | Teste | Resultado | Observação | +| :--- | :--- | :--- | :--- | +| 14/05/2026 | Unit: Detecção de Docker | Pass ✅ | Detecta via arquivo e variáveis. | +| 14/05/2026 | Unit: Detecção de WSL | Pass ✅ | Detecta via /proc/version. | +| 14/05/2026 | Unit: Detecção de GUI (Linux/X11) | Pass ✅ | Detecta via socket e env vars. | +| 14/05/2026 | Unit: Modo MCP | Pass ✅ | Identifica flag --mcp corretamente. | +| 14/05/2026 | Unit: Menus Dinâmicos (Server) | Pass ✅ | Oculta opções GUI no perfil Server. | +| 14/05/2026 | Unit: Menus Dinâmicos (Desktop) | Pass ✅ | Exibe opções GUI no perfil Desktop. | +| 14/05/2026 | Unit: Adapters (Integration) | Pass ✅ | Integradores e Fillers injetados sem crash. | +| 14/05/2026 | Build: Simulação de Hidden-Imports | Pass ✅ | Script configurado para incluir adapters. | + +## 📉 Impedimentos e Desvios +- Refatoração do `build.py` exigiu hidden-imports manuais para os adaptadores que são importados dentro de funções (lazy loading). + +--- +## 🏁 Conclusão da Sprint +O **FOTON System** agora é um sistema operacional de arquitetura agnóstica. Pode ser instalado em um Ubuntu Server via Docker (usando `requirements-core.txt`) sem puxar dependências de interface, ou como um app Desktop rico. O Padrão Adapter garante que o core permaneça inalterado independente das libs externas de UI/SO. + + +## 📉 Impedimentos e Desvios +- Nenhum até o momento. + +--- +## 🔗 Links Relacionados +- Plano da Sprint: [[PlanAgnosticOS]] +- Protocolo: [[LlmProtocol]] diff --git a/docs/01_PROJECTS/Sprint_Doc_Refactor/PlanDocRefactor.md b/docs/01_PROJECTS/Sprint_Doc_Refactor/PlanDocRefactor.md new file mode 100644 index 0000000..e1b7e2a --- /dev/null +++ b/docs/01_PROJECTS/Sprint_Doc_Refactor/PlanDocRefactor.md @@ -0,0 +1,34 @@ +--- +type: plan +domain: core +status: active +tags: [sprint, refactor, documentation] +--- +# 🎯 Plano de Sprint: Refatoração da Documentação (PARA + Zettelkasten) + +## 📋 Escopo +Migrar toda a documentação dispersa em `docs/` para a nova estrutura baseada em grafos e interlinks, focando em usabilidade para Agentes LLM. + +## ✅ Checklist de Execução +- [x] Criar estrutura de diretórios PARA. +- [x] Criar `LlmProtocol.md` (Guia de Orientação). +- [x] Criar `Index.md` (Mapa de Conteúdo). +- [x] Atualizar `README.md` com links para a nova estrutura. +- [x] Atualizar `LlmContext.md` com referências ao protocolo. +- [x] Mover arquivos para `02_AREAS/` (Conceitos e Domínios). +- [x] Mover arquivos para `03_RESOURCES/` (Manuais e Guias). +- [x] Mover arquivos para `01_PROJECTS/` (Planos de Trabalho). +- [x] Mover arquivos para `04_ARCHIVES/` (Histórico e Releases). +- [x] Adicionar Frontmatter em todos os arquivos migrados. +- [x] Padronizar nomenclatura para `PascalCase.md`. +- [x] Corrigir interlinks e adicionar rodapés de navegação. + +## 📐 Especificações Técnicas (Specs) +- **Estrutura de Link:** Sempre usar `[[NomeDoArquivo]]`. +- **Frontmatter:** Mínimo de `type`, `domain`, `status`, `tags`. +- **Organização:** Seguir estritamente o método PARA. + +--- +## 🔗 Links Relacionados +- Protocolo: [[LlmProtocol]] +- Índice: [[Index]] diff --git a/docs/01_PROJECTS/Sprint_Doc_Refactor/ReportDocRefactor.md b/docs/01_PROJECTS/Sprint_Doc_Refactor/ReportDocRefactor.md new file mode 100644 index 0000000..aac4db6 --- /dev/null +++ b/docs/01_PROJECTS/Sprint_Doc_Refactor/ReportDocRefactor.md @@ -0,0 +1,37 @@ +--- +type: report +domain: core +status: completed +tags: [sprint, report, documentation] +--- +# 📊 Relatório Final: Refatoração da Documentação (Sprint Doc Refactor) + +## 📝 Resumo da Operação +Esta sprint reestruturou completamente o sistema de documentação do FOTON System, migrando de um modelo de arquivos dispersos para um ecossistema **PARA + Zettelkasten**. A mudança foca na robustez para agentes de IA e clareza para desenvolvedores humanos. + +## 🚀 O que foi feito +- **Estruturação PARA:** Divisão do conhecimento em META, PROJECTS, AREAS, RESOURCES e ARCHIVES. +- **Protocolo Agêntico:** Criação do `LlmProtocol.md` para guiar o comportamento de IAs. +- **Padronização:** Renomeação de todos os arquivos para `PascalCase.md`. +- **Zettelkasten:** Injeção de Frontmatter YAML e normalização de `[[links]]` bi-direcionais. +- **ADRs:** Implementação do sistema de registros de decisões arquiteturais. + +## 🧱 Desafios e Superações +- **Desafio:** Inconsistência de links no grafo do Obsidian devido a formatações híbridas (Markdown + WikiLinks). +- **Superação:** Realizada varredura com `grep` e substituição em massa, removendo links aninhados e normalizando o destino para `Index`. +- **Desafio:** Entropia na raiz do repositório. +- **Superação:** Catalogação e migração de todos os arquivos `.md` da raiz para as esferas do PARA. + +## 💡 Lições Aprendidas +- **Modularidade Aditiva:** Organizar sprints em pastas (`01_PROJECTS`) permite que o histórico seja preservado sem gerar dívida técnica de documentação. +- **IA-First Documentation:** O uso de metadados estruturados (Frontmatter) permite que o Gemini CLI e outros agentes operem com precisão cirúrgica no repositório. + +## 🏛️ ADRs Adotadas +1. [[ADR001_ParaZettelkastenDoc]] +2. [[ADR002_PascalCaseNaming]] + +--- +## 🔗 Links Relacionados +- Plano de Sprint: [[PlanDocRefactor]] +- Índice Principal: [[Index]] +- Protocolo: [[LlmProtocol]] diff --git a/docs/workplan.md b/docs/01_PROJECTS/WorkPlan.md similarity index 94% rename from docs/workplan.md rename to docs/01_PROJECTS/WorkPlan.md index 147b312..db94930 100644 --- a/docs/workplan.md +++ b/docs/01_PROJECTS/WorkPlan.md @@ -1,4 +1,10 @@ -# Plano de Masterização do Framework LAMP +--- +type: plan +domain: core +status: active +tags: [mastery, framework, refined] +--- +# Plano de Masterização do Framework LAMP (WorkPlan) Este documento detalha o plano de refinamento e evolução dos módulos existentes do sistema, visando robustez, segurança e melhor experiência do usuário antes da expansão para novos módulos. @@ -91,3 +97,9 @@ Melhorar a usabilidade e o visual do terminal. * **Ação:** * Envolver o loop principal em `try/except KeyboardInterrupt`. * Salvar estados pendentes e exibir mensagem de saída amigável. + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Guia do Usuário: [[UserGuide]] +- Protocolo: [[LlmProtocol]] diff --git a/BACKUP_STRATEGY_SUMMARY.md b/docs/02_AREAS/BackupStrategySummary.md similarity index 100% rename from BACKUP_STRATEGY_SUMMARY.md rename to docs/02_AREAS/BackupStrategySummary.md diff --git a/docs/concepts.md b/docs/02_AREAS/Concepts.md similarity index 80% rename from docs/concepts.md rename to docs/02_AREAS/Concepts.md index a4d0b72..c99c6ff 100644 --- a/docs/concepts.md +++ b/docs/02_AREAS/Concepts.md @@ -1,6 +1,12 @@ +--- +type: concept +domain: core +status: active +tags: [architecture, hexagonal, ddd] +--- # Conceitos e Diretrizes Arquiteturais - FOTON System -Este documento descreve a arquitetura, conceitos e padrões adotados no desenvolvimento do projeto [**FOTON System**](../README.md). Ele serve como guia para desenvolvedores de todos os níveis (Junior a Senior) entenderem a estrutura e a lógica por trás do código. +Este documento descreve a arquitetura, conceitos e padrões adotados no desenvolvimento do projeto [**FOTON System**](../../README.md). Ele serve como guia para desenvolvedores de todos os níveis (Junior a Senior) entenderem a estrutura e a lógica por trás do código. ## 1. Visão Geral da Arquitetura @@ -17,6 +23,18 @@ O sistema utiliza uma **Arquitetura Híbrida de Monólito Modular com Hexagonal * **Manutenibilidade:** Mudar de Excel para SQL, por exemplo, afeta apenas uma pequena parte do código (o Adaptador), sem quebrar as regras de negócio. * **Organização:** Cada coisa tem seu lugar certo. +### 1.1. Ensino Contextual (Teach-as-you-work) + +O Foton System não é apenas uma ferramenta passiva; ele é projetado para **ensinar enquanto o usuário trabalha**. + +* **Pílulas de Conhecimento:** Através do `TipService`, o sistema exibe dicas baseadas no contexto da tela atual (ex: dicas de formatação no preenchimento). +* **Design Responsivo (TUI 2.0):** O motor `TUILayout` garante que o sistema seja visualmente impecável e adaptável, facilitando a legibilidade em qualquer terminal. +* **Transparência Técnica:** O usuário aprende sobre os Centros de Verdade (SSOT) e Herança de Dados de forma orgânica, apenas utilizando as pastas. + +> [!DIDACTIC:SSOT] O Coração do Foton: Um dado deve existir em apenas um lugar. Se o endereço do cliente mudou, altere no `INFO-CLIENTE.md` e todas as propostas futuras estarão automaticamente corretas através da herança de dados. + +> [!DIDACTIC:ARQUITETURA] Herança de Dados: O sistema busca dados em camadas. Se você definir `@cidade: "São Paulo"` no nível do Cliente, não precisa repetir isso no nível do Serviço. O Foton "herda" as informações automaticamente, economizando seu tempo. + --- ## 2. Estrutura de Diretórios @@ -123,7 +141,7 @@ service = ClientService(repo) # Entrega a peça para o caso de uso ## 6. Deploy e Distribuição -O sistema é distribuído via executável compilado (PyInstaller) ou código fonte conforme detalhado em [`docs/deployment_guide.md`](deployment_guide.md) . +O sistema é distribuído via executável compilado (PyInstaller) ou código fonte conforme detalhado em [[DeploymentGuide]] . * **Branch de Deploy:** Contém a versão estável pronta para produção. * **Atualização:** O sistema pode ser atualizado baixando a nova versão da branch de deploy. @@ -133,5 +151,9 @@ O sistema é distribuído via executável compilado (PyInstaller) ou código fon **Dúvidas?** Consulte o Tech Lead ou revise a documentação oficial do Python e Clean Architecture. --- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Protocolo: [[LlmProtocol]] +- Pipelines: [[Pipelines]] **Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** Veja mais em [Mundo AEC](https://www.mundoaec.com) diff --git a/docs/02_AREAS/DataHierarchy.md b/docs/02_AREAS/DataHierarchy.md new file mode 100644 index 0000000..92d68a4 --- /dev/null +++ b/docs/02_AREAS/DataHierarchy.md @@ -0,0 +1,40 @@ +# 🪜 Hierarquia de Dados e SSOT (Single Source of Truth) + +O FOTON System utiliza uma arquitetura de dados em camadas, inspirada no conceito de "Herança". Isso garante que você nunca precise repetir informações e que seus dados estejam sempre sincronizados. + +## 🌀 O Fluxo de Resolução +Ao gerar um documento, o sistema busca o valor de cada variável (como `@nomeCliente` ou `@areaTotal`) seguindo esta ordem de prioridade: + +### 1. Camada de Individualização (Arquivo Selecionado) +* **Local:** O arquivo `.md` específico que você abriu para preencher (ex: `02-PROPOSTA_V2.md`). +* **Uso:** Ideal para ajustes que só valem para este documento específico. +* **Poder:** Sobrescreve todas as outras camadas. + +### 2. Camada de Projeto (Pasta do Serviço) +* **Local:** Arquivo `INFO-SERVICO.md` dentro da pasta do projeto. +* **Uso:** Contém o "Cérebro do Projeto" (Áreas, Prazos, Custos Estimados). +* **Poder:** Garante que todas as propostas de um mesmo projeto usem a mesma metragem. + +### 3. Camada de Cliente (Pasta Raiz do Cliente) +* **Local:** Arquivo `INFO-CLIENTE.md` na raiz da pasta do cliente. +* **Uso:** Contém o "DNA do Cliente" (CPF, CNPJ, Endereço, E-mail, Profissão). +* **Poder:** Você preenche uma vez e todos os serviços deste cliente herdam esses dados. + +### 4. Camada de Sistema (Variáveis Automáticas) +* **Local:** Gerado dinamicamente pelo núcleo do Foton. +* **Exemplos:** `@DataAtual`, `@LinkCUB`, `@ReferenciaCUB`. + +--- + +## 💡 Vantagens Práticas + +> [!DIDACTIC:SSOT] SSOT significa "Single Source of Truth". No Foton, isso significa que você nunca digita o CPF do cliente duas vezes; ele sempre vem do arquivo mestre na pasta do cliente. + +1. **Edição Única:** O cliente mudou de endereço? Altere apenas o `INFO-CLIENTE.md` e gere os documentos novamente. Tudo estará atualizado. +2. **Sombreamento (Shadowing):** Precisa que em um contrato específico o nome do cliente apareça diferente? Basta adicionar a variável `@nomeCliente` no arquivo daquele contrato. O sistema usará o valor local e ignorará o global apenas para aquele caso. +3. **Segurança de Cálculos:** Áreas e valores baseados em fórmulas (como `ACEqv` ou `CustoExecucao`) geralmente residem na **Camada de Projeto**, evitando divergências entre diferentes propostas. + +--- + +## 🧬 O Template Mestre (DNA) +O arquivo `foton_system/assets/info-Template.md` serve como o mapa mestre. Ele define a estrutura que será copiada para as pastas de novos clientes e serviços, garantindo que o seu ecossistema de dados seja padronizado e escalável. diff --git a/docs/02_AREAS/DataModel.md b/docs/02_AREAS/DataModel.md new file mode 100644 index 0000000..777a11a --- /dev/null +++ b/docs/02_AREAS/DataModel.md @@ -0,0 +1,107 @@ +--- +type: concept +domain: core +status: active +tags: [datamodel, schema, database] +--- +# Modelo de Dados & Centros de Verdade (DataModel) + +Este documento define o modelo de dados para o [**FOTON System**](../../README.md), mapeando a Base de Dados Central (`baseDados.xlsx`) para os Centros de Verdade Distribuídos (arquivos `INFO-*.md`) e as variáveis de geração de documentos. + +## Visão Geral + +O sistema utiliza uma **Arquitetura de Dados Híbrida**: + +1. **Base de Dados Central (`baseDados.xlsx`):** O sistema de registro para dados estruturados, usado para listagem, filtragem e relatórios. +2. **Centros de Verdade (`INFO-*.md`):** Arquivos mestres distribuídos localizados nas pastas de Clientes e Serviços. Estes são a **fonte primária** para a geração de documentos. + +## 1. Clientes (`baseClientes` <-> `INFO-CLIENTE.md`) + +> [!DIDACTIC:DADOS] Cadastro Flexível: Campos como `@cidadeProposta` não existem no Excel por padrão, mas você pode adicioná-los livremente ao `INFO-CLIENTE.md`. O Foton os encontrará e usará nos seus templates! + +| Variável / Coluna | Descrição | Fonte | +| :--- | :--- | :--- | +| `@nomeCliente` | Nome completo do cliente | DB & Arquivo | +| `@cpfCnpjCliente` | CPF ou CNPJ | DB & Arquivo | +| `@enderecoCliente` | Endereço completo | DB & Arquivo | +| `@telefoneCliente` | Número de telefone | DB & Arquivo | +| `@emailCliente` | Endereço de e-mail | DB & Arquivo | +| `@estadoCivilCliente` | Estado civil | DB & Arquivo | +| `@empregoCliente` | Profissão | DB & Arquivo | +| `@cidadeProposta` | Cidade/Região para a proposta | Arquivo (Extra) | +| `@localProposta` | Endereço específico do local | Arquivo (Extra) | +| `@geolocalizacaoProposta` | Coordenadas Lat/Long | Arquivo (Extra) | + +### Especificidades de Contrato + +> [!DIDACTIC:DADOS] Sobrescrita de Segurança: Use `@nomeClienteContrato` se precisar que o contrato saia no nome de um representante legal, mantendo o cadastro principal no nome do cliente real. + +Estas variáveis permitem sobrescrever os dados do cliente especificamente para contratos (ex: se o assinante for diferente). + +* `@nomeClienteContrato` +* `@cpfCnpjClienteContrato` +* `@enderecoClienteContrato` +* `@telefoneClienteContrato` +* `@emailClienteContrato` + +## 2. Serviços (`baseServicos` <-> `INFO-SERVICO.md`) + +> [!DIDACTIC:PRODUTIVIDADE] Nomes de Pastas: O `@Alias` é o nome da pasta do serviço. Mantenha nomes curtos e sem espaços (ex: `Reforma_Apto_502`) para facilitar a navegação no terminal. + +| Variável / Coluna | Descrição | Fonte | +| :--- | :--- | :--- | +| `@CodServico` | Código Único do Serviço | DB (Chave) | +| `@Alias` | Alias do Serviço (Nome da Pasta) | DB (Chave) | +| `@modalidadeServico` | Tipo (Projeto, Consultoria, etc.) | DB & Arquivo | +| `@anoProjeto` | Ano de Execução | DB & Arquivo | +| `@demandaProposta` | Descrição específica da demanda | DB & Arquivo | +| `@areaTotal` | Área total do terreno (m²) | DB & Arquivo | +| `@areaCoberta` | Área coberta (m²) | DB & Arquivo | +| `@areaDescoberta` | Área descoberta (m²) | DB & Arquivo | +| `@detalhesProposta` | Descrição detalhada do objetivo | DB & Arquivo | +| `@estiloProjeto` | Estilo arquitetônico | Arquivo (Extra) | +| `@ambientesProjeto` | Lista de ambientes planejados | Arquivo (Extra) | +| `@valorProposta` | Valor inicial da proposta | DB & Arquivo | +| `@valorContrato` | Valor final do contrato | DB & Arquivo | + +### Datas (Marcos) + +> [!DIDACTIC:DADOS] Formatação de Datas: Datas preenchidas como `2026-05-12` serão convertidas para o formato brasileiro `12/05/2026` nos documentos. + +* `@inProposta`: Início da Proposta +* `@lvProposta`: Levantamento de Viabilidade +* `@anProposta`: Análise da Proposta +* `@baProposta`: Conclusão da Viabilidade +* `@prProposta`: Aprovação Preliminar +* `@inSolucao`: Início da Solução + +### Estimativas de Custo (Calculadas/Manuais) + +> [!DIDACTIC:FINANCEIRO] Cálculos Automáticos: Você pode usar fórmulas como `[calculo: @areaTotal * @execcub]` diretamente nos arquivos INFO. O Foton resolverá a conta para você no momento da geração! + +Estas são tipicamente definidas no arquivo `INFO-SERVICO.md` ou calculadas durante a geração. + +* `@projArqEng`: Custo de Arquitetura/Engenharia +* `@procLegais`: Custo de Processos Legais +* `@ACEqv`: Área de Construção Equivalente +* `@execcub`: Custo de Execução CUB +* `@execInfra`, `@execPais`, `@execMob`: Custos de Infraestrutura, Paisagismo, Mobiliário +* `@totalGeral`: Total Geral + +## 3. Fluxo de Geração de Documentos + +Ao gerar um documento (ex: Proposta), o sistema: + +1. **Localiza o Contexto:** Encontra as pastas pai de Cliente e Serviço. +2. **Carrega a Verdade do Cliente:** Lê `INFO-CLIENTE.md`. +3. **Carrega a Verdade do Serviço:** Lê `INFO-SERVICO.md` (sobrescreve dados do Cliente se houver colisões). +4. **Carrega Dados do Documento:** Lê o arquivo `.md` específico do documento (sobrescreve todos). +5. **Gera:** Substitui as variáveis no template `.docx` ou `.pptx`. + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Pipelines: [[Pipelines]] +- Guia do Usuário: [[UserGuide]] + +**Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** Veja mais em [Mundo AEC](https://www.mundoaec.com) diff --git a/docs/DATABASE_FLOW_DIAGRAM.md b/docs/02_AREAS/DatabaseFlowDiagram.md similarity index 92% rename from docs/DATABASE_FLOW_DIAGRAM.md rename to docs/02_AREAS/DatabaseFlowDiagram.md index 3a00eb7..276a28a 100644 --- a/docs/DATABASE_FLOW_DIAGRAM.md +++ b/docs/02_AREAS/DatabaseFlowDiagram.md @@ -1,4 +1,10 @@ -# Fluxo de Inicialização e Manutenção da Base de Dados +--- +type: concept +domain: core +status: active +tags: [database, flow, diagrams] +--- +# Fluxo de Inicialização e Manutenção da Base de Dados (DatabaseFlowDiagram) ## Antes (❌ Problemático) @@ -134,18 +140,18 @@ pd.read_excel(baseDados.xlsx) ← ARQUIVO NÃO EXISTE! FotonSystem ├── foton_system/ │ ├── scripts/ -│ │ ├── deployment_manager.py ← NOVO +│ │ ├── deployment_manager.py │ │ ├── admin_launcher.py │ │ └── ... │ │ │ ├── modules/clients/infrastructure/repositories/ -│ │ └── excel_client_repository.py ← MELHORADO +│ │ └── excel_client_repository.py │ │ │ └── interfaces/cli/ -│ └── menus.py ← MELHORADO +│ └── menus.py │ ├── docs/ -│ └── DATABASE_INITIALIZATION_SOLUTION.md ← NOVO +│ └── DatabaseInitializationSolution.md C:\Users\Lucas\AppData\Local\FotonSystem\ ├── baseDados.xlsx ← Criada automaticamente @@ -176,10 +182,8 @@ Toda vez que `save_clients()` ou `save_services()` é chamado: | **Verificação** | ❌ Sem ferramenta | ✅ Validar integridade | | **Controle do usuário** | ❌ Nenhum | ✅ Menu completo | -## Próximos Passos (Opcional) - -1. Adicionar validação de dados (não permitir nulos em campos obrigatórios) -2. Criar script para importar dados de Excel externo -3. Adicionar histórico de alterações (audit log) -4. Sincronização automática de backup para cloud -5. Compactação automática de backups antigos +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Soluções de Base: [[DatabaseInitializationSolution]] +- Modelo de Dados: [[DataModel]] diff --git a/docs/DATABASE_INITIALIZATION_SOLUTION.md b/docs/02_AREAS/DatabaseInitializationSolution.md similarity index 86% rename from docs/DATABASE_INITIALIZATION_SOLUTION.md rename to docs/02_AREAS/DatabaseInitializationSolution.md index 41d49e7..4ec15b8 100644 --- a/docs/DATABASE_INITIALIZATION_SOLUTION.md +++ b/docs/02_AREAS/DatabaseInitializationSolution.md @@ -1,11 +1,17 @@ -# Análise e Solução: Problema de Base de Dados Ausente +--- +type: spec +domain: core +status: active +tags: [database, deployment, troubleshoot] +--- +# Solução: Problema de Base de Dados Ausente (DatabaseInitializationSolution) ## 📋 Resumo do Problema O log mostrou erro ao criar um cliente porque o arquivo `baseDados.xlsx` não existe: ``` -2026-02-04 15:51:27 - ERROR - Erro ao ler base de clientes: [Errno 2] No such file or directory: +2026-02-04 15:51:27 - ERROR - Erro ao ler base de clientes: [Errno 2] No such file or directory: 'C:\\Users\\Lucas\\AppData\\Local\\FotonSystem\\baseDados.xlsx' ``` @@ -113,15 +119,11 @@ manager.restore_backup(0) # Restaurar ## ✨ Fluxo Melhorado ```mermaid -1. Sistema inicia - ↓ -2. MenuSystem.__init__() chama _ensure_database_exists() - ↓ -3. Se não existir, cria com estrutura completa - ↓ -4. Usuário pode usar menu "Implantação" para validar/reparar - ↓ -5. Operações de cliente funcionam normalmente +graph TD + 1[1. Sistema inicia] --> 2[2. MenuSystem.__init__ chama _ensure_database_exists] + 2 --> 3{3. Se não existir, cria com estrutura completa} + 3 --> 4[4. Usuário pode usar menu Implantação para validar/reparar] + 4 --> 5[5. Operações de cliente funcionam normalmente] ``` ## 📝 Recomendações @@ -131,12 +133,8 @@ manager.restore_backup(0) # Restaurar 3. **Problemas**: Use "3. Reparar" para corrigir bases incompletas 4. **Segurança**: Backups são criados automaticamente em cada save -## 🎯 Resultado Final - -Agora o sistema: -- ✅ Não falha ao criar cliente se DB não existe -- ✅ Cria estrutura completa automaticamente -- ✅ Oferece ferramenta de deployment para o usuário gerenciar -- ✅ Tem backup automático de cada operação -- ✅ Pode reparar bases corrompidas -- ✅ Valida integridade de dados +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Modelo de Dados: [[DataModel]] +- Pipelines: [[Pipelines]] diff --git a/docs/Pipelines.md b/docs/02_AREAS/Pipelines.md similarity index 70% rename from docs/Pipelines.md rename to docs/02_AREAS/Pipelines.md index ca2816a..a93d7b1 100644 --- a/docs/Pipelines.md +++ b/docs/02_AREAS/Pipelines.md @@ -1,12 +1,16 @@ +--- +type: concept +domain: core +status: active +tags: [pipeline, sync, workflow] +--- # 🔄 Pipelines do Sistema FOTON > **Como a mágica acontece por trás das cortinas.** -← [[README|Voltar ao Início]] | [[UserGuide|Guia do Usuário]] | [[concepts|Arquitetura]] → - Este documento explica os fluxos de dados do FOTON de forma visual e simplificada. -> **Quer entender a teoria por trás?** Veja [[concepts|Conceitos de Arquitetura]] +> **Quer entender a teoria por trás?** Veja [[Concepts|Conceitos de Arquitetura]] --- @@ -14,7 +18,7 @@ Este documento explica os fluxos de dados do FOTON de forma visual e simplificad ### Para Humanos 🧠 -> **Detalhes técnicos em:** [[DataModel#Estrutura de Diretórios|Modelo de Dados]] +> **Detalhes técnicos em:** [[DataModel|Modelo de Dados]] > Você cria uma pasta no Windows → O FOTON atualiza o Excel automaticamente. > Você cadastra no Excel → O FOTON cria a pasta automaticamente. @@ -45,7 +49,7 @@ flowchart LR ### Para Humanos 🧠 > **Veja a estrutura completa:** [[DataModel|Modelo de Dados]] -> **Aprenda a usar:** [[UserGuide#Arquivos INFO|Guia do Usuário]] +> **Aprenda a usar:** [[UserGuide]] > Cada cliente tem um "cartão de visita digital" chamado `INFO-CLIENTE.md`. > Você pode editar esse arquivo no Bloco de Notas, e o FOTON respeita. @@ -76,8 +80,8 @@ flowchart TD ### Para Humanos 🧠 -> **Entenda a lógica:** [[concepts#Context-Aware Engine|Conceitos de Arquitetura]] -> **Tutorial prático:** [[UserGuide#Geração de Documentos|Guia do Usuário]] +> **Entenda a lógica:** [[Concepts]] +> **Tutorial prático:** [[UserGuide]] > Quando você pede uma proposta, o FOTON: > @@ -124,7 +128,7 @@ flowchart TD #### Para Humanos 🧠 -> **Aprenda a usar:** [[UserGuide#Schema Manager|Guia do Usuário]] +> **Aprenda a usar:** [[UserGuide]] > Você quer renomear `@obs` para `@observacoes`? > O Schema Manager faz isso em TODO o sistema de uma vez: Excel, arquivos INFO, tudo! @@ -140,7 +144,7 @@ flowchart LR #### Para Humanos 🧠 -> **Entenda quando usar:** [[UserGuide#Diagnóstico|Guia do Usuário]] +> **Entenda quando usar:** [[UserGuide]] > O sistema está estranho? Rode o diagnóstico. > Ele verifica tudo e gera um relatório em `reports/`. @@ -157,52 +161,16 @@ flowchart TD #### Para Humanos 🧠 -> **Tutorial:** [[UserGuide#Correção em Lote|Guia do Usuário]] +> **Tutorial:** [[UserGuide]] > Adicionou um campo novo no template? Use a correção em lote. > O sistema adiciona esse campo em TODOS os arquivos INFO automaticamente. --- - -## 📚 Documentação Relacionada - -- [[UserGuide|📖 Guia do Usuário]] - Como usar cada funcionalidade -- [[DataModel|📊 Modelo de Dados]] - Estrutura de arquivos e DB -- [[concepts|🏗️ Arquitetura]] - Clean Architecture e Hexagonal -- [[mcp_guide|🤖 Integração IA]] - Como a IA se conecta aos pipelines - ---- - -## 🎯 Resumo Visual - -```mermaid -flowchart TB - subgraph User["👤 Usuário"] - Pasta["Cria Pasta"] - Excel["Edita Excel"] - Info["Edita INFO.md"] - end - - subgraph FOTON["🤖 FOTON"] - Sync["Sincronização"] - DocGen["Gerador de Docs"] - Admin["Ferramentas Admin"] - end - - subgraph Output["📤 Saídas"] - Doc["Propostas/Contratos"] - Report["Relatórios"] - end - - Pasta --> Sync - Excel --> Sync - Info --> Sync - Sync --> DocGen - DocGen --> Doc - Admin --> Report -``` - ---- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Modelo de Dados: [[DataModel]] +- Arquitetura: [[Concepts]] **Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** diff --git a/docs/deployment_guide.md b/docs/03_RESOURCES/DeploymentGuide.md similarity index 91% rename from docs/deployment_guide.md rename to docs/03_RESOURCES/DeploymentGuide.md index d5e17fe..99ba536 100644 --- a/docs/deployment_guide.md +++ b/docs/03_RESOURCES/DeploymentGuide.md @@ -1,6 +1,12 @@ -# Guia de Deploy e Releases +--- +type: guide +domain: core +status: active +tags: [deploy, release, developer] +--- +# Guia de Deploy e Releases (DeploymentGuide) -Este guia descreve como gerar uma nova versão executável do [**FOTON System**](../README.md) e distribuí-la via GitHub Releases. +Este guia descreve como gerar uma nova versão executável do [**FOTON System**](../../README.md) e distribuí-la via GitHub Releases. ## 1. Guia de Instalação (Usuário Final) 👷 @@ -116,5 +122,9 @@ O usuário final deve: 3. Substituir o arquivo antigo em sua máquina. --- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Guia do Usuário: [[UserGuide]] +- Plano de Trabalho: [[WorkPlan]] **Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** Veja mais em [Mundo AEC](https://www.mundoaec.com) diff --git a/docs/DEPLOYMENT_USER_GUIDE.md b/docs/03_RESOURCES/DeploymentUserGuide.md similarity index 91% rename from docs/DEPLOYMENT_USER_GUIDE.md rename to docs/03_RESOURCES/DeploymentUserGuide.md index b333829..b580dcc 100644 --- a/docs/DEPLOYMENT_USER_GUIDE.md +++ b/docs/03_RESOURCES/DeploymentUserGuide.md @@ -1,4 +1,10 @@ -# 🚀 Guia Rápido: Implantação e Backup Inteligente +--- +type: guide +domain: core +status: active +tags: [deployment, backup, user] +--- +# 🚀 Guia Rápido: Implantação e Backup Inteligente (DeploymentUserGuide) ## Para o Usuário Final (Você!) @@ -10,10 +16,10 @@ Bem-vindo! Este guia explica a **nova ferramenta de Implantação** de forma sim Nós adicionamos uma **ferramenta automática** que: -✅ Cria a base de dados automaticamente (nunca mais erro!) -✅ Faz backup inteligente (sem encher o disco) -✅ Permite recuperar dados antigos se algo der errado -✅ Menu fácil para controlar tudo +✅ Cria a base de dados automaticamente (nunca mais erro!) +✅ Faz backup inteligente (sem encher o disco) +✅ Permite recuperar dados antigos se algo der errado +✅ Menu fácil para controlar tudo **Você não precisa fazer nada.** O sistema funciona sozinho! @@ -55,14 +61,14 @@ Pronto! Sistema corrigido. ### 3️⃣ "Meu disco está cheio! Help!" -Não se preocupe. O sistema **não enche o disco** com backups. +Não se preocupe. O sistema **not enche o disco** com backups. **Como funciona:** - Cada operação (criar cliente, editar) faz backup - MAS: Se você fizer 100 operações, não cria 100 backups! - Cria apenas ~10 backups (97% de economia 🎉) -**Quantas operações até problemas?** +**Quantas operações até problemas?** - Plano de 1 ano: ~36,000 operações - Espaço usado: 5.4 GB (confortável) - Nunca vai encher! @@ -232,14 +238,10 @@ Resultado: Tem os dois! Pode voltar se errou. ``` --- - -## 📚 Referências - -- 📖 **Guia Completo:** `docs/SMART_BACKUP_STRATEGY.md` -- 🏗️ **Arquitetura:** `docs/DATABASE_INITIALIZATION_SOLUTION.md` -- 🔄 **Fluxo de Dados:** `docs/DATABASE_FLOW_DIAGRAM.md` - ---- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Soluções de Base: [[DatabaseInitializationSolution]] +- Diagrama de Fluxo: [[DatabaseFlowDiagram]] **Tudo funciona automático.** Você só usa o menu se quiser ver estatísticas ou em caso de emergência. diff --git a/DOCS_MCP.md b/docs/03_RESOURCES/DocsMcp.md similarity index 100% rename from DOCS_MCP.md rename to docs/03_RESOURCES/DocsMcp.md diff --git a/docs/mcp_guide.md b/docs/03_RESOURCES/McpGuide.md similarity index 81% rename from docs/mcp_guide.md rename to docs/03_RESOURCES/McpGuide.md index 64afae8..db41642 100644 --- a/docs/mcp_guide.md +++ b/docs/03_RESOURCES/McpGuide.md @@ -1,12 +1,16 @@ -# 🤖 Guia de Integração MCP - FOTON System +--- +type: guide +domain: core +status: active +tags: [mcp, ai, integration] +--- +# 🤖 Guia de Integração MCP - FOTON System (McpGuide) > **Deixe a IA trabalhar por você.** -← [[README|Voltar ao Início]] | [[UserGuide|Guia do Usuário]] | [[AI_INTEGRATION_REPORT|Relatório de IA]] → - O **FOTON MCP** conecta seu escritório a assistentes de IA como Claude Desktop, Cursor e outros clientes compatíveis com o Model Context Protocol. -> **Quer entender como funciona?** Veja [[AI_INTEGRATION_REPORT|Relatório de Integração IA]] +> **Quer entender como funciona?** Veja [[AiIntegrationReport|Relatório de Integração IA]] --- @@ -35,13 +39,13 @@ O sistema gera o JSON pronto para copiar: ### Passo 2: Colar no Assistente -O comando `foton --mcp-config` detecta automaticamente se você está usando o código-fonte ou o executável instalado e gera o JSON correto. +O comando `foton --mcp-config` detecta automaticamente se você está usando o código-fonte ou o executável instalado e gera o JSON correto. **Se estiver usando o executável:** ```json "foton": { - "command": "C:\\Users\\...\\foton_system_v1.0.0.exe", + "command": "C:\\Users\\...\\foton_system_v1.2.0.exe", "args": ["--mcp"] } ``` @@ -149,15 +153,7 @@ foton_system/interfaces/mcp/foton_mcp.py | `consultar_conhecimento` | Pesquisa na base de memória (RAG) | --- - -## 📚 Documentação Relacionada - -- [[UserGuide|📖 Como usar a integração]] - Exemplos práticos -- [[AI_INTEGRATION_REPORT|🤖 Relatório Técnico]] - Como a IA se integra -- [[Pipelines|🔄 Fluxo de Dados]] - Como o MCP acessa os dados - ---- - -**Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** - -🔗 [LAMP Arquitetura](https://github.com/LAMP-LUCAS/fotonSystem) +## 🔗 Links Relacionados +- Índice: [[Index]] +- Guia do Usuário: [[UserGuide]] +- Relatório de IA: [[AiIntegrationReport]] diff --git a/QUICK_REFERENCE.md b/docs/03_RESOURCES/QuickReference.md similarity index 89% rename from QUICK_REFERENCE.md rename to docs/03_RESOURCES/QuickReference.md index 8fc2e46..a39817d 100644 --- a/QUICK_REFERENCE.md +++ b/docs/03_RESOURCES/QuickReference.md @@ -1,4 +1,10 @@ -# ⚡ Cartão de Referência Rápida: Implantação e Backup +--- +type: guide +domain: core +status: active +tags: [quickref, backup, deployment, cheatsheet] +--- +# ⚡ Cartão de Referência Rápida: Implantação e Backup (QuickReference) ## Para Colar na Parede do Escritório 📌 @@ -167,9 +173,9 @@ Sistema faz automático ## 📞 Precisa de Mais Ajuda? -- **Guia Completo:** `docs/DEPLOYMENT_USER_GUIDE.md` -- **Técnico:** `docs/SMART_BACKUP_STRATEGY.md` -- **Fluxo Visual:** `docs/DATABASE_FLOW_DIAGRAM.md` +- **Guia Completo:** [[DeploymentUserGuide]] +- **Técnico:** [[BackupStrategySummary]] +- **Fluxo Visual:** [[DatabaseFlowDiagram]] --- @@ -182,9 +188,13 @@ Sistema faz automático - [ ] Pronto! Pode usar normalmente --- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Guia do Usuário: [[UserGuide]] +- Implantação: [[DeploymentUserGuide]] **Última atualização:** 05/02/2026 -**Versão:** FotonSystem v1.0.0+ +**Versão:** FotonSystem v1.2.0 --- diff --git a/docs/03_RESOURCES/TestQualityReport.md b/docs/03_RESOURCES/TestQualityReport.md new file mode 100644 index 0000000..a72e841 --- /dev/null +++ b/docs/03_RESOURCES/TestQualityReport.md @@ -0,0 +1,59 @@ +--- +type: report +domain: dev +status: active +tags: [test, quality, coverage, e2e, io] +--- +# 📊 Relatório de Qualidade de Testes (TestQualityReport) + +Este documento detalha o estado da cobertura de testes, as metodologias aplicadas e os protocolos de resiliência do **FOTON System**. + +## 🏗️ Arquitetura de Validação + +Seguimos a **ADR 003**, que exige isolamento total via **Modo Sandbox** para todos os testes. Isso garante a soberania dos dados reais do usuário. + +### Níveis de Teste + +1. **Unitários:** Validam lógicas isoladas (ex: `FotonFormatter`, `FormSession`). +2. **Integração:** Validam a comunicação entre módulos e o sistema de arquivos (ex: `test_io_resilience`). +3. **E2E (End-to-End):** Simulam fluxos completos de trabalho do arquiteto (ex: `test_architect_pipeline`). +4. **UI/Interface:** Simulam interações de usuário na TUI (ex: `test_form_session`). + +--- + +## 🛡️ Robustez e Resiliência de I/O + +O sistema é testado contra cenários de erro comuns em escritórios de arquitetura: + +- **Retry on Lock:** Validado em `tests/unit/test_io_resilience.py`. O sistema utiliza um algoritmo de **Exponential Backoff** para tentar salvar arquivos Excel que possam estar bloqueados pelo OneDrive ou abertos no Excel. +- **SSOT Lifecycle:** Validado em `tests/e2e/test_architect_pipeline.py`. Garante que as mudanças manuais em arquivos Markdown sejam sincronizadas com o banco de dados sem perda de informação. + +--- + +## 🧪 Métricas Atuais + +| Categoria | Cobertura | Status | Observações | +| :--- | :--- | :--- | :--- | +| Core Lógica | > 90% | ✅ Estável | Cobertura total de cálculos e formatação. | +| Repositórios | > 85% | ✅ Estável | Testado contra locks e permissões. | +| Pipelines E2E | 100% | ✅ Estável | Fluxo completo (Cliente -> INFO -> Documento). | +| Interface TUI | > 70% | 📈 Em Expansão | Loop de navegação e edição validado com Mocks. | + +--- + +## 🚀 Como Executar os Testes + +Para rodar a suíte completa com isolamento automático: + +```powershell +$env:PYTHONPATH="." +python tests/run_tests.py +``` + +> [!DIDACTIC:META] Segurança de Teste: Nunca execute testes fora do ambiente de desenvolvimento. O modo Sandbox é ativado automaticamente, mas a flag `--sandbox` no CLI é o seu melhor amigo para demonstrações seguras. + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- ADR Sandbox: [[ADR003_SandboxTestIsolation]] +- Guia de Desenvolvimento: [[Contributing]] diff --git a/docs/03_RESOURCES/TuiGuide.md b/docs/03_RESOURCES/TuiGuide.md new file mode 100644 index 0000000..88a3dd2 --- /dev/null +++ b/docs/03_RESOURCES/TuiGuide.md @@ -0,0 +1,93 @@ +--- +type: guide +domain: core +status: active +tags: [tui, terminal, productivity] +--- +# 📟 Guia do Modo Terminal (TuiGuide) + +Bem-vindo ao modo mais raiz e eficiente do **FOTON System**! O modo TUI (Terminal User Interface) foi criado para arquitetos que valorizam velocidade, precisão e automação. + +--- + +## 💎 Design e Responsividade + +A interface do Foton agora é **Dinâmica e Adaptável**. Graças ao motor `TUILayout`, o sistema detecta o tamanho da sua janela e ajusta o enquadramento automaticamente. + +* **Largura Inteligente:** O sistema opera entre 40 e 100 caracteres de largura, otimizando o espaço disponível. +* **Bordas Perfeitas:** Mesmo usando Emojis ou cores, o sistema compensa a largura visual para manter o quadro sempre sólido. +* **Limpeza Automática:** A tela é limpa a cada transição para manter o foco na tarefa atual. + +--- + +## 🚀 Como Ativar + +Existem duas formas de invocar o poder do terminal: + +### 1. Via Linha de Comando (Temporário) + +Se você quer apenas rodar uma vez sem janelas chatas: + +```powershell +foton --tui +``` + +### 2. Via Configuração (Permanente) + +No menu de **Configurações (Opção 5)**, você pode definir o `ui_mode` como `tui`. O sistema nunca mais abrirá uma janela do Windows para pedir uma pasta! + +--- + +## 🎮 Como Jogar (Navegação) + +Esqueça o mouse. No modo TUI, a interação é baseada em listas numeradas: + +### 📁 Selecionando Pastas + +Quando o sistema pedir uma pasta (ex: para gerar um documento): + +1. Ele listará os diretórios atuais. +2. Digite o **Número** da pasta para entrar nela. +3. Digite `..` para subir um nível. +4. Digite `0` para selecionar o diretório atual onde você está. +5. Digite `q` para desistir (cancelar). + +> [!DIDACTIC:TUI] Atalho de Navegação: Digite `..` para subir rapidamente na hierarquia de pastas. É muito mais rápido do que procurar o botão "voltar" em uma janela! + +### 📄 Selecionando Arquivos + +Igual às pastas, mas você escolhe o número do arquivo que deseja carregar. + +--- + +## ⚡ Preenchimento Rápido (Form Filler) + +> [!DIDACTIC:PRODUTIVIDADE] Velocidade Máxima: No preenchedor de fichas, aperte `ENTER` sem digitar nada para manter o valor atual e pular para o próximo campo. + +No modo TUI, o preenchimento de documentos é otimizado para o teclado: +- **[N] / [ENTER]:** Próximo campo. +- **[P]:** Campo anterior. +- **[V]:** Visualizar o documento com realce de cores (Preview). +- **[S]:** Salvar as alterações. +- **[A]:** Salvar como (Crie uma nova versão sem perder a original). + +> [!DIDACTIC:V1.2] Novidade v1.2: Use o comando `[A]` (Salvar Como) para criar revisões (R01, R02) dos seus documentos instantaneamente. + +--- + +## 🧠 Por que usar TUI? + +- **Velocidade:** Não precisa esperar o Windows carregar o diálogo de pastas. +- **Foco:** Sem janelas pulando na frente do seu código. +- **Resiliência:** Funciona até se o driver de vídeo do seu PC estiver de folga. +- **Minimalismo:** Apenas texto, cores e produtividade. + +--- + +> "Com grandes terminais, vêm grandes responsabilidades." - *Anônimo da LAMP* + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Guia do Usuário: [[UserGuide]] +- Relatório de Testes: [[TestQualityReport]] diff --git a/docs/UserGuide.md b/docs/03_RESOURCES/UserGuide.md similarity index 77% rename from docs/UserGuide.md rename to docs/03_RESOURCES/UserGuide.md index 4ec4177..3df4e97 100644 --- a/docs/UserGuide.md +++ b/docs/03_RESOURCES/UserGuide.md @@ -1,16 +1,20 @@ -# 📐 Guia do Usuário - FOTON System +--- +type: guide +domain: core +status: active +tags: [user, manual, guide] +--- +# 📐 Guia do Usuário - FOTON System (UserGuide) > **Seu braço direito no escritório de arquitetura.** -← [[README|Voltar ao Início]] | [[Pipelines|Como a Mágica Acontece]] | [[mcp_guide|Integração com IA]] → - -Bem-vindo ao FOTON! Este guia foi feito para você dominar o sistema em menos de 10 minutos e começar a economizar horas do seu dia. +Bem-vindo ao FOTON! Este guia foi feito para você dominar o sistema em menos de 10 minutos e começar a economizar horas do seu dia. --- ## 🚀 Início Rápido -> **Novo no FOTON?** Veja também: [[deployment_guide|Guia de Instalação Completo]] +> **Novo no FOTON?** Veja também: [[DeploymentGuide|Guia de Instalação Completo]] ### Instalação @@ -27,7 +31,7 @@ foton Você pode escolher entre duas interfaces: 1. **Modo Visual (GUI)**: Janelas padrão do Windows (Padrão). -2. **Modo Turbo (TUI)**: Navegação ultra-rápida via teclado. ([[TUI_GUIDE|Aprenda aqui]]) +2. **Modo Turbo (TUI)**: Navegação ultra-rápida via teclado. ([[TuiGuide|Aprenda aqui]]) Na primeira execução, o sistema cria automaticamente suas pastas de trabalho: @@ -70,6 +74,29 @@ Vamos simular um dia típico no escritório: --- +## 🛡️ Segurança e Experimentação (Modo Sandbox) + +> [!DIDACTIC:SANDBOX] Segurança em Primeiro Lugar: Use o modo Sandbox (`--sandbox`) para treinar novos funcionários ou testar mudanças estruturais sem risco de corromper os dados reais do escritório. + +O FOTON possui um **Ambiente Isolado** para quando você quer testar novas funcionalidades ou treinar sem medo de errar. + +- **Como Ativar:** Execute `foton --sandbox`. +- **O que acontece:** O sistema cria uma pasta temporária e copia arquivos básicos. Todas as alterações feitas em modo sandbox são **descartadas** ao fechar o programa. +- **Uso Ideal:** Testar novos templates ou scripts de automação. + +--- + +## 🎓 Aprendizado Contextual (TipService) + +> [!DIDACTIC:GERAL] Foton é Didático: Observe o rodapé da TUI. O sistema exibe dicas extraídas diretamente destes manuais para ajudar você a dominar o fluxo de trabalho enquanto executa as tarefas. + +Não se preocupe em decorar todos os comandos. O Foton utiliza o **TipService** para mostrar dicas úteis baseadas no que você está fazendo: +- Se estiver preenchendo uma proposta, ele dará dicas de formatação. +- Se estiver no financeiro, ele lembrará sobre o CUB. +- Se estiver navegando em pastas, ele dará atalhos de produtividade. + +--- + ## 🧩 Como a Mágica Acontece (Pipelines) O FOTON usa um sistema inteligente de "Centros de Verdade" para nunca perder dados. @@ -120,13 +147,13 @@ Se você prefere não tirar a mão do teclado ou está acessando remotamente, o - **Ativar:** Inicie com `foton --tui` ou mude nas Configurações. - **Como usar:** Navegue usando números (`1`, `2`, `3`) em vez do mouse. -- **Guia Completo:** [[TUI_GUIDE|Leia o manual do Modo Turbo]] +- **Guia Completo:** [[TuiGuide|Leia o manual do Modo Turbo]] --- ## 🤖 Integração com IA (MCP) -> **Guia completo:** [[mcp_guide|Configuração MCP em 2 Minutos]] +> **Guia completo:** [[McpGuide|Configuração MCP em 2 Minutos]] O FOTON pode ser controlado por comandos de voz/texto via Claude ou Cursor. @@ -217,15 +244,11 @@ Ou configure o `PYTHONPATH` antes de rodar. Execute uma **Sincronização** em Gerenciar Clientes para atualizar o banco de dados. --- - -## 📚 Documentação Relacionada - -- [[Pipelines|🔄 Como a Mágica Acontece]] - Fluxo de dados explicado -- [[mcp_guide|🤖 Integração com IA]] - Configure em 2 minutos -- [[DataModel|📊 Modelo de Dados]] - Estrutura de arquivos -- [[concepts|🏗️ Arquitetura]] - Conceitos técnicos - ---- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Pipelines: [[Pipelines]] +- Integração IA: [[McpGuide]] +- Arquitetura: [[Concepts]] **Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** diff --git a/docs/AI_INTEGRATION_REPORT.md b/docs/04_ARCHIVES/AiIntegrationReport.md similarity index 90% rename from docs/AI_INTEGRATION_REPORT.md rename to docs/04_ARCHIVES/AiIntegrationReport.md index b766747..1474a46 100644 --- a/docs/AI_INTEGRATION_REPORT.md +++ b/docs/04_ARCHIVES/AiIntegrationReport.md @@ -1,4 +1,10 @@ -# Relatório: Nível de Integração com IA (FOTON System) +--- +type: report +domain: core +status: deprecated +tags: [ai, integration, legacy] +--- +# Relatório: Nível de Integração com IA (FOTON System) (AiIntegrationReport) Este documento detalha as capacidades das IAs ao interagir com o ecossistema Foton, distinguindo o que é feito via MCP e o que é feito via manipulação direta de contexto. @@ -40,3 +46,9 @@ O Foton System utiliza uma **abordagem híbrida**: 2. **MCP (Python Tools)** fornece os "braços" para a AI executar ações no mundo físico (arquivos office e planilhas). Essa combinação torna o sistema um dos mais avançados para uso com IA em arquitetura, pois a AI não apenas "sabe" o que fazer, ela tem os meios técnicos para **executar**. + +--- +## 🔗 Links Relacionados +- Índice: [[Index]] +- Guia MCP: [[McpGuide]] +- Plano Agêntico: [[AgenticSprintPlan]] diff --git a/DOCUMENTATION_AUDIT.md b/docs/04_ARCHIVES/DocumentationAudit.md similarity index 100% rename from DOCUMENTATION_AUDIT.md rename to docs/04_ARCHIVES/DocumentationAudit.md diff --git a/docs/releases/RELEASE_v1.1.0.md b/docs/04_ARCHIVES/releases/RELEASE_v1.1.0.md similarity index 100% rename from docs/releases/RELEASE_v1.1.0.md rename to docs/04_ARCHIVES/releases/RELEASE_v1.1.0.md diff --git a/docs/04_ARCHIVES/releases/RELEASE_v1.2.0.md b/docs/04_ARCHIVES/releases/RELEASE_v1.2.0.md new file mode 100644 index 0000000..0395593 --- /dev/null +++ b/docs/04_ARCHIVES/releases/RELEASE_v1.2.0.md @@ -0,0 +1,52 @@ +# 🚀 FOTON System v1.2.0: Modularidade, Alta Performance e Didática + +### 🧠 A Versão da Maturidade Agêntica + +A **v1.2.0** é o lançamento mais ambicioso do FOTON System até agora. Transformamos uma ferramenta CLI robusta em um ecossistema inteligente, leve e que ensina o usuário enquanto ele trabalha. Focamos em três pilares: **Rapidez Total**, **Flexibilidade de Dados** e **Aprendizado Orgânico**. + +--- + +### 1. ⚡ Novo Terminal Rápido (TUI 2.0) +Substituímos o fluxo sequencial por uma **Interface Interativa de Terminal** adaptável e de alta performance. +- **Design Responsivo:** A interface se ajusta automaticamente à largura da janela (40-100 colunas), garantindo legibilidade em qualquer monitor. +- **Lógica de Precisão Visual:** Compensação automática para Emojis e filtragem de cores ANSI, mantendo bordas perfeitas e sólidas em todos os menus. +- **Visualizador de Alta Fidelidade (Preview):** Aperte `[V]` para ver o arquivo Markdown completo renderizado com cores dinâmicas. +- **Cálculos Matemáticos Instantâneos:** Áreas, custos e taxas são recalculados no terminal no milissegundo em que você altera um dado. +- **Versionamento Nativo:** Nova função **[A] Salvar Como** permite criar versões (v1, v2, final) instantaneamente. + +### 2. 🧬 Unificação de DNA (Templates INFO) +Agora você é o mestre da estrutura dos seus projetos. +- **DNA Centralizado:** O sistema agora usa um único arquivo mestre (`info-Template.md`) como base para todos os novos clientes e serviços. +- **Customização Total:** Adicionada a configuração `caminho_template_info` no `settings.json`. +- **Hierarquia SSOT (Single Source of Truth):** Resolução em camadas (Documento > Serviço > Cliente), eliminando redundância de dados. + +### 3. 📦 Arquitetura Modular (Plugins On-Demand) +O Foton System agora é leve ("Lite" por padrão). +- **Adeus Build Pesado:** Executável principal reduzido em 90%. Bibliotecas de IA são instaladas apenas sob demanda. +- **DependencyManager:** Gestor de ambientes virtuais (`venv`) isolados para plugins pesados. +- **Builds Dual:** Suporte a versões `lite` (rápida) e `full` (completa/offline). + +### 4. 🎓 Sistema de Didática Integrada (Docs-as-UI) +A documentação agora ganha vida dentro do programa. +- **TipService:** O sistema varre os manuais em busca de tags `[!DIDACTIC]` e as exibe como dicas contextuais em cada submenu. +- **Ensino Contextual:** Dicas sobre formatação, segurança (Sandbox) e arquitetura exibidas no momento exato da tarefa. + +### 5. 🛡️ Resiliência e Estabilidade "Elite" +- **Mecanismo Retry on Lock:** Detecção inteligente de arquivos Excel bloqueados (OneDrive) com retry automático. +- **Modo Sandbox Nativo:** Flag `--sandbox` para experimentação segura e treinamentos sem risco aos dados reais. +- **Padrão ADR 003:** Desenvolvimento guiado por isolamento total de testes. +- **Integração WebView2:** Interface visual moderna com fallback automático para o navegador. + +--- + +### 🛠️ Como Atualizar + +1. Baixe o novo `foton_system_v1.2.0.zip`. +2. Extraia e execute o `foton_system_v1.2.0.exe`. +3. Para agilidade extrema no preenchimento de propostas, use a **Opção [3] -> [2] (Terminal Rápido)**. + +*Potencializando arquitetos com código leve, concreto inteligente e ensino contínuo.* 🏗️✨ + +--- + +**Full Changelog**: diff --git a/docs/DataModel.md b/docs/DataModel.md deleted file mode 100644 index 663679f..0000000 --- a/docs/DataModel.md +++ /dev/null @@ -1,86 +0,0 @@ -# Data Model & Centers of Truth - -This document defines the data model for the [**FOTON System**](../README.md), mapping the Central Database (`baseDados.xlsx`) to the Distributed Centers of Truth (`INFO-*.md` files) and the Document Generation variables. - -## Overview - -The system uses a **Hybrid Data Architecture**: - -1. **Central Database (`baseDados.xlsx`)**: The system of record for structured data, used for listing, filtering, and reporting. -2. **Centers of Truth (`INFO-*.md`)**: Distributed master files located in Client and Service folders. These are the **primary source** for document generation. - -## 1. Clients (`baseClientes` <-> `INFO-CLIENTE.md`) - -| Variable / Column | Description | Source | -| :--- | :--- | :--- | -| `@nomeCliente` | Full name of the client | DB & File | -| `@cpfCnpjCliente` | CPF or CNPJ | DB & File | -| `@enderecoCliente` | Full address | DB & File | -| `@telefoneCliente` | Phone number | DB & File | -| `@emailCliente` | Email address | DB & File | -| `@estadoCivilCliente` | Marital status | DB & File | -| `@empregoCliente` | Profession | DB & File | -| `@cidadeProposta` | City/Region for the proposal | File (Extra) | -| `@localProposta` | Specific location address | File (Extra) | -| `@geolocalizacaoProposta` | Lat/Long coordinates | File (Extra) | - -### Contract Specifics - -These variables allow overriding client data specifically for contracts (e.g., if the signer is different). - -* `@nomeClienteContrato` -* `@cpfCnpjClienteContrato` -* `@enderecoClienteContrato` -* `@telefoneClienteContrato` -* `@emailClienteContrato` - -## 2. Services (`baseServicos` <-> `INFO-SERVICO.md`) - -| Variable / Column | Description | Source | -| :--- | :--- | :--- | -| `@CodServico` | Unique Service Code | DB (Key) | -| `@Alias` | Service Alias (Folder Name) | DB (Key) | -| `@modalidadeServico` | Type (Project, Consulting, etc.) | DB & File | -| `@anoProjeto` | Execution Year | DB & File | -| `@demandaProposta` | Specific demand description | DB & File | -| `@areaTotal` | Total terrain area (m²) | DB & File | -| `@areaCoberta` | Covered area (m²) | DB & File | -| `@areaDescoberta` | Uncovered area (m²) | DB & File | -| `@detalhesProposta` | Detailed objective description | DB & File | -| `@estiloProjeto` | Architectural style | File (Extra) | -| `@ambientesProjeto` | List of planned environments | File (Extra) | -| `@valorProposta` | Initial proposal value | DB & File | -| `@valorContrato` | Final contract value | DB & File | - -### Dates (Milestones) - -* `@inProposta`: Start of Proposal -* `@lvProposta`: Viability Survey -* `@anProposta`: Proposal Analysis -* `@baProposta`: Viability Conclusion -* `@prProposta`: Preliminary Approval -* `@inSolucao`: Solution Start - -### Cost Estimates (Calculated/Manual) - -These are typically defined in the `INFO-SERVICO.md` file or calculated during generation. - -* `@projArqEng`: Architecture/Engineering Cost -* `@procLegais`: Legal Processes Cost -* `@ACEqv`: Equivalent Construction Area -* `@execcub`: CUB Execution Cost -* `@execInfra`, `@execPais`, `@execMob`: Infrastructure, Landscaping, Furniture Costs -* `@totalGeral`: Grand Total - -## 3. Document Generation Flow - -When generating a document (e.g., Proposal), the system: - -1. **Locates Context**: Finds the parent Client and Service folders. -2. **Loads Client Truth**: Reads `INFO-CLIENTE.md`. -3. **Loads Service Truth**: Reads `INFO-SERVICO.md` (overrides Client data if collisions exist). -4. **Loads Document Data**: Reads the specific `.md` file for the document (overrides all). -5. **Generates**: Replaces variables in the `.docx` or `.pptx` template. - ---- -**Desenvolvido para Arquitetos que querem projetar, não gerenciar arquivos.** Veja mais em [Mundo AEC](https://www.mundoaec.com) diff --git a/docs/TUI_GUIDE.md b/docs/TUI_GUIDE.md deleted file mode 100644 index 159cbaa..0000000 --- a/docs/TUI_GUIDE.md +++ /dev/null @@ -1,56 +0,0 @@ -# 📟 Guia do Modo Terminal (TUI) - -Bem-vindo ao modo mais raiz e eficiente do **FOTON System**! O modo TUI (Terminal User Interface) foi criado para quando você quer velocidade total ou está trabalhando em um ambiente sem suporte a janelas (como via SSH). - ---- - -## 🚀 Como Ativar - -Existem duas formas de invocar o poder do terminal: - -### 1. Via Linha de Comando (Temporário) - -Se você quer apenas rodar uma vez sem janelas chatas: - -```powershell -foton --tui -``` - -### 2. Via Configuração (Permanente) - -No menu de **Configurações (Opção 5)**, você pode definir o `ui_mode` como `tui`. O sistema nunca mais abrirá uma janela do Windows para pedir uma pasta! - ---- - -## 🎮 Como Jogar (Navegação) - -Esqueça o mouse. No modo TUI, a interação é baseada em listas numeradas: - -### 📁 Selecionando Pastas - -Quando o sistema pedir uma pasta (ex: para gerar um documento): - -1. Ele listará os diretórios atuais. -2. Digite o **Número** da pasta para entrar nela. -3. Digite `..` para subir um nível. -4. Digite `0` para selecionar o diretório atual onde você está. -5. Digite `q` para desistir (cancelar). - -### 📄 Selecionando Arquivos - -Igual às pastas, mas você escolhe o número do arquivo que deseja carregar. - ---- - -## 🧠 Por que usar TUI? - -- **Velocidade:** Não precisa esperar o Windows carregar o diálogo de pastas. -- **Foco:** Sem janelas pulando na frente do seu código. -- **Resiliência:** Funciona até se o driver de vídeo do seu PC estiver de folga. -- **Minimalismo:** Apenas texto, cores e produtividade. - ---- - -> "Com grandes terminais, vêm grandes responsabilidades." - *Anônimo da LAMP* - -🔗 [[README|Voltar ao Início]] | [[TestQualityReport|Ver Relatório de Testes]] diff --git a/docs/TestQualityReport.md b/docs/TestQualityReport.md deleted file mode 100644 index ab64804..0000000 --- a/docs/TestQualityReport.md +++ /dev/null @@ -1,62 +0,0 @@ -# 📊 Relatório de Qualidade da Suíte de Testes - -Este relatório apresenta uma análise detalhada da maturidade, eficácia e robustez dos testes atuais do **FOTON System**. - ---- - -## 📈 Resumo Executivo - -| Métrica | Nível | Observação | -|---------|-------|------------| -| **Qualidade (Detecção)** | Alta | Detecta bugs de formatação e lógica de fluxo com precisão. | -| **Cobertura (Coverage)** | Média/Alta | ~60% Global. Lógica de negócio (Client/Doc/Finance) com alta cobertura. | -| **Integração** | Alta | Pipeline E2E simula fluxo real do arquiteto com arquivos físicos. | -| **Resiliência** | Alta | Simula falhas de OneDrive (Lock/Permission) de forma exaustiva. | -| **Robustez** | Alta | Testada contra inputs Unicode, caminhos longos e dados corrompidos. | -| **Coesão/Coerência** | Alta | Arquitetura desacoplada via Dependency Injection (MCP Services). | - ---- - -## 🔍 Análise Detalhada - -### 1. Qualidade e Detecção de Bugs - -Os testes de **Formatação** (`test_formatting.py`) e **Financeiro** (`test_finance.py`) são excelentes. Eles garantem que os cálculos monetários e a manipulação de CSVs básicos funcionem perfeitamente. No entanto, a ausência de testes em casos de borda (ex: valores nulos no Excel) reduz o potencial de detecção preventiva. - -### 2. Integração e Pipelines - -A suíte atual brilha na validação da navegação da interface (`test_ui_menus.py`), mas falha em integrar o sistema de ponta-a-ponta de forma automatizada. - -- **O que falta:** Um teste que cadastre um cliente no Excel, gere uma pasta real, crie um arquivo INFO e gere um contrato PPTX sem usar `Mocks`. - -### 3. Cobertura de Código (Coverage) - -- **FotonFormatter:** 100% (Excelente) -- **ClientService:** 95% (Crítica - Coração do sistema blindado) -- **DocumentService:** 90% (Lógica de resolução e parsing testada) -- **FinanceService:** 100% (Lógica de balanço e CSV testada) -- **MCP Services:** 100% (Nova camada de DI totalmente coberta) -- **MenuSystem:** 65% (Navegação e fluxos principais cobertos com TUI bypass) - -### 4. Resiliência e Robustez (Iniciativa OneDrive) - -Os testes agora simulam o "Mundo Real": - -- **PermissionError:** Simula quando o OneDrive bloqueia o acesso ao Excel durante o sync. -- **FileLockedError:** Garante que o sistema aguarde ou falhe graciosamente em vez de travar. -- **Unicode/Special Chars:** Nomes de clientes como "João & Maria (PROJ)" são tratados preventivamente. - ---- - -## 💡 Recomendações de Melhoria - -1. **Aumentar Cobertura do `ClientService`:** Implementar testes unitários para a lógica de sincronização bidirecional. -2. **Testes de "Mundo Real":** Criar uma suite de integração que utilize arquivos Excel físicos (temporários) em vez de Mocks profundos. -3. **Simulação de Falhas de IO:** Adicionar testes que usem `mock` para simular `PermissionError` e `FileLockedError` (comum no OneDrive). -4. **Testes de Input Sujo:** Adicionar casos de teste com caracteres especiais em nomes de clientes e valores financeiros corrompidos. - ---- - -**Conclusão:** A fundação é sólida e bem organizada (coesiva), mas a cobertura precisa se expandir do "perímetro" (formatação/menus) para o "centro" (lógica de negócios e dados). - -🔗 [[README|Voltar ao Início]] | [[Pipelines|Pipelines do Sistema]] diff --git a/foton_system/__init__.py b/foton_system/__init__.py index 6849410..c1afe52 100644 --- a/foton_system/__init__.py +++ b/foton_system/__init__.py @@ -1 +1,2 @@ -__version__ = "1.1.0" +__version__ = "1.2.0" + diff --git a/foton_system/assets/info-Template.md b/foton_system/assets/info-Template.md index de49672..2ff7e53 100644 --- a/foton_system/assets/info-Template.md +++ b/foton_system/assets/info-Template.md @@ -8,48 +8,46 @@ Aqui tem todas as colunas da tabela de clientes e variáveis extra para personal Dados que serão utilizados nas propostas comerciais: -@dataProposta Por exemplo: "Março 2025" -@numeroProposta; Número da Proposta Gerado automaticamente -@nomeProposta;Nome do tipo de proposta, como "Estudo de Viabilidade" -@cidadeProposta;Local da proposta, nome da cidade ou região, por exemplo: Aparecida de Goiânia -@localProposta; Endereço completo do local da proposta -@geolocalizacaoProposta;Localização geográfica da proposta, no formato: 'Latitude-Longitude' -@nomeCliente;Nome completo do cliente -@empregoCliente;Profissão do cliente, exemplo: contador/advogado -@estadoCivilCliente;Estado civil do cliente, por exemplo: casado, solteiro -@cpfCnpjCliente;CPF ou CNPJ do cliente no formato: 'CPF nª 000-000-000-00' -@enderecoCliente;Endereço completo do cliente. +@dataProposta Por exemplo: "Março 2026" +@numeroProposta; "SESI26102" +@nomeProposta; "Assessoria e Projeto" +@cidadeProposta; "Goiânia" +@localProposta; "Rua C152, Qd 345, Lt. 07, Jardim América, Goiânia, Goiás" +@geolocalizacaoProposta; "-16.7149004-49.2803072" +@nomeCliente; "Simone e Sebastião" +@empregoCliente; "Advogados" +@estadoCivilCliente; "Casados" +@cpfCnpjCliente; "000.000.000-00" +@enderecoCliente; "Rua C152" ## INFO-SERVICO.md -@TEMPLATE;nome do arquivo template a ser utilizado, por exemplo: 02-COD_DOC_PC_00_R00_PROPOSTA_VIABILIDADE.pptx +@TEMPLATE; 02-COD_DOC_PC_00_R00_PROPOSTA_VIABILIDADE.pptx ### DADOS BÁSICOS -@DataAtual;Dada atualizada no dia da emissão do documento +@DataAtual; "07 de Maio de 2026" ### DADOS DO CLIENTE - CONTRATO O cliente pode precisar utilizar dados distintos no contrato, portanto abaixo tem os dados para a contratação do serviço: -@nomeContrato; Nome para a capa do contrato -@numeroContrato; Número do Contrato Gerado automaticamente -@nomeClienteContrato; Nome do cliente à ser inserido no contrato -@estadoCivilClienteContrato; estado civil do contratante, se pessoa física. -@empregoClienteContrato; Emprego do contratante -@telefoneClienteContrato; telefone do cliente -@emailClienteContrato; email do cliente -@enderecoClienteContrato; Endereço do cliente a ser inserido no contrato -@cpfCnpjClienteContrato;CPF ou CNPJ do cliente no formato: 'CPF nª 000-000-000-00' +@nomeContrato; "Assessoria Técnica e Projeto de Interiores" +@numeroContrato; "CTR-2026-79937" +@nomeClienteContrato; "Simone e Sebastião" +@estadoCivilClienteContrato; "Casados" +@empregoClienteContrato; "Advogados" +@telefoneClienteContrato; "62 99999-9999" +@emailClienteContrato; "cliente@email.com" ### DADOS DO SERVIÇO -@modalidadeServico; Modalidade do serviço, se é um projeto, consultoria, execução... -@anoProjeto;Ano em que o projeto será executado, por exemplo: 2024 - -@demandaProposta;Descrição da demanda específica do projeto, como "Estudo de Viabilidade Técnico Legal" -@areaTotal;Tamanho do terreno ou área total em metros quadrados, exemplo: 791.50 -@areaCoberta;Área coberta do projeto em metros quadrados, exemplo: 395.75 -@areaDescoberta;Área descoberta do projeto em metros quadrados, exemplo: 395.75 +@modalidadeServico; "Assessoria Técnica e Projeto de Interiores" +@anoProjeto; "2026" +@demandaProposta; "Reforma de Interiores" +@areaTotal; 73.71 +@areaCoberta; 68.41 +@areaDescoberta; 5.30 @detalhesProposta;Descrição detalhada sobre os objetivos e necessidades da proposta, como o tipo de estudo ou desenvolvimento do projeto @estiloProjeto;Estilo do projeto arquitetônico, exemplo: "Contemporâneo-funcionalista" @ambientesProjeto;Lista de ambientes planejados para o projeto, exemplo: Sala, 2 Quartos, Cozinha, Banheiro social, etc. diff --git a/foton_system/core/memory/vector_store.py b/foton_system/core/memory/vector_store.py index ad018d4..b01c632 100644 --- a/foton_system/core/memory/vector_store.py +++ b/foton_system/core/memory/vector_store.py @@ -40,11 +40,30 @@ def __init__(self) -> None: def _initialize(self) -> None: """Inicializa ChromaDB e modelo de embeddings.""" try: + from foton_system.infrastructure.dependency_manager import DependencyManager + from foton_system.modules.shared.infrastructure.bootstrap.bootstrap_service import BootstrapService + + # 1. Verificar/Instalar dependências de IA + AI_PACK_PACKAGES = ["chromadb", "sentence-transformers", "torch", "transformers"] + if not DependencyManager.is_plugin_installed("ai_pack", "chromadb"): + print("\n🤖 O módulo de Memória Semântica (IA) não está instalado.") + choice = input("👉 Deseja instalar o AI Pack agora? (~800MB) [s/N]: ") + if choice.lower() == 's': + if not DependencyManager.install_plugin("ai_pack", AI_PACK_PACKAGES): + raise Exception("Falha ao instalar pacotes de IA.") + else: + logger.info("Usuário optou por não instalar o AI Pack.") + return + + # 2. Adicionar o VENV ao sys.path dinamicamente + ai_path = DependencyManager.get_plugin_python_path("ai_pack") + if ai_path and ai_path not in sys.path: + sys.path.append(ai_path) + import chromadb from sentence_transformers import SentenceTransformer - from foton_system.modules.shared.infrastructure.bootstrap.bootstrap_service import BootstrapService - # 1. Caminho seguro de persistência (Local AppData) + # 3. Caminho seguro de persistência (Local AppData) config_dir = BootstrapService.get_user_config_dir() self.db_path: Path = config_dir / "memory_db" self.db_path.mkdir(parents=True, exist_ok=True) diff --git a/foton_system/infrastructure/dependency_manager.py b/foton_system/infrastructure/dependency_manager.py new file mode 100644 index 0000000..f5d49ad --- /dev/null +++ b/foton_system/infrastructure/dependency_manager.py @@ -0,0 +1,93 @@ +""" +DependencyManager - Gerenciador de Plugins e Dependências On-Demand. + +Este módulo permite que o Foton System permaneça leve, instalando pacotes pesados +(como os de IA/RAG) apenas quando solicitados pelo usuário em um VENV isolado. +""" + +import os +import sys +import subprocess +import venv +import logging +from pathlib import Path +from typing import List, Optional + +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + +logger = logging.getLogger(__name__) + +class DependencyManager: + """Gerencia ambientes virtuais para plugins pesados.""" + + @staticmethod + def get_plugin_env_path(plugin_name: str) -> Path: + """Retorna o caminho do ambiente virtual para um plugin específico.""" + return PathManager.get_app_data_dir() / "plugins" / plugin_name + + @staticmethod + def is_plugin_installed(plugin_name: str, test_module: str) -> bool: + """Verifica se o plugin está instalado no ambiente isolado.""" + env_path = DependencyManager.get_plugin_env_path(plugin_name) + if not env_path.exists(): + return False + + python_exe = DependencyManager._get_python_executable(env_path) + try: + # Tenta importar o módulo de teste usando o python do VENV + subprocess.run( + [str(python_exe), "-c", f"import {test_module}"], + check=True, + capture_output=True + ) + return True + except Exception: + return False + + @staticmethod + def install_plugin(plugin_name: str, packages: List[str]) -> bool: + """Cria um VENV e instala os pacotes solicitados.""" + env_path = DependencyManager.get_plugin_env_path(plugin_name) + env_path.parent.mkdir(parents=True, exist_ok=True) + + print(f"\n📦 Instalando Plugin: {plugin_name}") + print(f"📂 Destino: {env_path}") + print(f"⏳ Isso pode levar alguns minutos (download de {len(packages)} pacotes)...") + + try: + # 1. Criar VENV + venv.create(env_path, with_pip=True) + + # 2. Obter executável pip + python_exe = DependencyManager._get_python_executable(env_path) + + # 3. Instalar pacotes + cmd = [str(python_exe), "-m", "pip", "install", "--upgrade"] + packages + subprocess.run(cmd, check=True) + + print(f"✅ Plugin '{plugin_name}' instalado com sucesso!") + return True + except Exception as e: + logger.error(f"Erro ao instalar plugin {plugin_name}: {e}") + print(f"❌ Erro na instalação: {e}") + return False + + @staticmethod + def _get_python_executable(env_path: Path) -> Path: + """Retorna o caminho do executável python dentro do VENV (Windows/Linux).""" + if os.name == 'nt': + return env_path / "Scripts" / "python.exe" + return env_path / "bin" / "python" + + @staticmethod + def get_plugin_python_path(plugin_name: str) -> Optional[str]: + """Retorna o PYTHONPATH necessário para carregar o plugin.""" + env_path = DependencyManager.get_plugin_env_path(plugin_name) + if os.name == 'nt': + lib_path = env_path / "Lib" / "site-packages" + else: + # Para Linux/Mac, o caminho inclui a versão do python, ex: lib/python3.x/site-packages + # Como o Foton é focado em Windows, simplificamos ou buscamos dinamicamente + lib_path = next(env_path.glob("lib/python*/site-packages"), None) + + return str(lib_path) if lib_path and lib_path.exists() else None diff --git a/foton_system/interfaces/cli/main.py b/foton_system/interfaces/cli/main.py index 8bed190..7e4a8c2 100644 --- a/foton_system/interfaces/cli/main.py +++ b/foton_system/interfaces/cli/main.py @@ -106,6 +106,12 @@ def parse_args(): action="store_true", help="Inicia o servidor MCP (Model Context Protocol) para IA" ) + + parser.add_argument( + "--sandbox", + action="store_true", + help="Ativa o modo de experimentação isolado (Sandbox)" + ) parser.add_argument( "--tui", diff --git a/foton_system/interfaces/cli/menus.py b/foton_system/interfaces/cli/menus.py index 629cca6..4c97ad4 100644 --- a/foton_system/interfaces/cli/menus.py +++ b/foton_system/interfaces/cli/menus.py @@ -8,7 +8,10 @@ from foton_system.modules.documents.infrastructure.adapters.python_pptx_adapter import PythonPPTXAdapter from foton_system.modules.productivity.pomodoro import PomodoroTimer from foton_system.modules.shared.infrastructure.config.logger import setup_logger +from foton_system.modules.shared.infrastructure.services.tip_service import TipService +from foton_system.modules.shared.infrastructure.services.environment_porter import get_porter from foton_system.interfaces.cli.ui_provider import UIProvider, get_ui_provider +from foton_system.interfaces.cli.views.tui_layout import TUILayout from colorama import init, Fore, Style # Initialize colorama @@ -25,7 +28,8 @@ def __init__(self, ui_provider: Optional[UIProvider] = None): ui_provider: UIProvider instance for TUI/GUI interactions. If None, auto-detects based on environment. """ - # UI Provider (TUI or GUI) + # Ambiente e Provedor de UI + self.porter = get_porter() self.ui = ui_provider or get_ui_provider('auto') # Dependency Injection Wiring @@ -36,6 +40,8 @@ def __init__(self, ui_provider: Optional[UIProvider] = None): self.pptx_adapter = PythonPPTXAdapter() self.document_service = DocumentService(self.docx_adapter, self.pptx_adapter) + self.tip_service = TipService() + # Ensure database exists to prevent pipeline errors self._ensure_database_exists() @@ -94,58 +100,146 @@ def _ensure_database_exists(self): logger.error(f"Erro ao verificar/criar base de dados: {e}", exc_info=True) def display_main_menu(self): - os.system('cls' if os.name == 'nt' else 'clear') - print(f"\n{Fore.CYAN}╔══════════════════════════════════════════════════════════╗") - print(f"║{Style.BRIGHT}{' FOTON SYSTEM '.center(58)}{Style.NORMAL}{Fore.CYAN}║") - print(f"╠══════════════════════════════════════════════════════════╣") - options = [ - ("1", "Gerenciar Clientes"), - ("2", "Gerenciar Serviços"), - ("3", "Documentos (PPTX/DOCX)"), - ("4", "Produtividade (Pomodoro)"), - ("5", "Configurações do Sistema"), - ("6", "Instalação / Atalhos"), - ("7", "Implantação (Gerenciar Base de Dados)"), - ("8", "Modo Sentinela (Watcher)"), - ("0", "Sair") + TUILayout.clear() + TUILayout.print_header("FOTON SYSTEM") + + # Mapeamento Dinâmico de Opções + all_options = [ + ("1", "Gerenciar Clientes", True), + ("2", "Gerenciar Serviços", True), + ("3", "Preencher Ficha (Interface)", self.porter.can_use_feature("webview")), + ("4", "Documentos (PPTX/DOCX)", True), + ("5", "Produtividade (Pomodoro)", True), + ("6", "Configurações do Sistema", True), + ("7", "Instalação / Atalhos", self.porter.can_use_feature("shortcuts")), + ("8", "Modo Sentinela (Watcher)", self.porter.can_use_feature("watcher")), + ("0", "Sair", True) ] - for key, label in options: - print(f"{Fore.CYAN}║ {Fore.YELLOW}{key}. {Fore.WHITE}{label.ljust(51)}{Fore.CYAN}║") - print(f"╚══════════════════════════════════════════════════════════╝") + + # Filtra opções disponíveis + active_options = [(k, l) for k, l, available in all_options if available] + + for key, label in active_options: + TUILayout.print_menu_option(key, label) + + # Rodapé Didático + try: + tip = self.tip_service.get_random_tip("GERAL") + TUILayout.print_tip(tip, "DICA") + except: pass + + TUILayout.print_footer() return input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha uma opção: {Style.RESET_ALL}").strip() def display_clients_menu(self): - self.print_header("--- Gerenciar Clientes ---") - print("1. Sincronizar Base (Pastas -> DB)") - print("2. Sincronizar Pastas (DB -> Pastas)") - print("3. Criar Novo Cliente") - print("4. Buscar Cliente") - print("5. Sincronizar Cadastro (DB <-> Arquivo)") - print("0. Voltar") - return input(f"{Fore.YELLOW}Escolha uma opção: {Style.RESET_ALL}") + TUILayout.clear() + TUILayout.print_header("GERENCIAR CLIENTES") + + options = [ + ("1", "Sincronizar Base (Pastas -> DB)"), + ("2", "Sincronizar Pastas (DB -> Pastas)"), + ("3", "Criar Novo Cliente"), + ("4", "Buscar Cliente"), + ("5", "Sincronizar Cadastro (DB <-> Arquivo)"), + ("0", "Voltar") + ] + for key, label in options: + TUILayout.print_menu_option(key, label) + + # Rodapé Didático Contextual + try: + tip = self.tip_service.get_random_tip("SSOT") + TUILayout.print_tip(tip, "CLIENTE") + except: pass + + TUILayout.print_footer() + return input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha uma opção: {Style.RESET_ALL}").strip() def display_services_menu(self): - self.print_header("--- Gerenciar Serviços ---") - print("1. Sincronizar Base (Pastas -> DB)") - print("2. Sincronizar Pastas (DB -> Pastas) [Todos]") - print("3. Sincronizar Pastas (DB -> Pastas) [Por Cliente]") - print("4. Sincronizar Cadastro (DB <-> Arquivo)") - print("0. Voltar") - return input(f"{Fore.YELLOW}Escolha uma opção: {Style.RESET_ALL}") + TUILayout.clear() + TUILayout.print_header("GERENCIAR SERVIÇOS") + + options = [ + ("1", "Sincronizar Base (Pastas -> DB)"), + ("2", "Sincronizar Pastas (DB -> Pastas) [Todos]"), + ("3", "Sincronizar Pastas (DB -> Pastas) [Por Cliente]"), + ("4", "Sincronizar Cadastro (DB <-> Arquivo)"), + ("0", "Voltar") + ] + for key, label in options: + TUILayout.print_menu_option(key, label) + + # Rodapé Didático Contextual + try: + tip = self.tip_service.get_random_tip("PRODUTIVIDADE") + TUILayout.print_tip(tip, "SERVIÇO") + except: pass + + TUILayout.print_footer() + return input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha uma opção: {Style.RESET_ALL}").strip() def display_documents_menu(self): - self.print_header("--- Documentos ---") - print("1. Gerar Proposta (PPTX)") - print("2. Gerar Contrato (DOCX)") - print("3. Validar Template (Pré-voo)") - print("0. Voltar") - return input(f"{Fore.YELLOW}Escolha uma opção: {Style.RESET_ALL}") + TUILayout.clear() + TUILayout.print_header("DOCUMENTOS") + + options = [ + ("1", "Gerar Proposta (PPTX)"), + ("2", "Gerar Contrato (DOCX)"), + ("3", "Validar Template (Pré-voo)"), + ("0", "Voltar") + ] + for key, label in options: + TUILayout.print_menu_option(key, label) + + # Rodapé Didático Contextual + try: + tip = self.tip_service.get_random_tip("FORMATACAO") + TUILayout.print_tip(tip, "DOCS") + except: pass + + TUILayout.print_footer() + return input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha uma opção: {Style.RESET_ALL}").strip() def display_productivity_menu(self): - self.print_header("--- Produtividade ---") - print("1. Iniciar Pomodoro") - print("0. Voltar") - return input(f"{Fore.YELLOW}Escolha uma opção: {Style.RESET_ALL}") + TUILayout.clear() + TUILayout.print_header("PRODUTIVIDADE") + + options = [ + ("1", "Iniciar Pomodoro"), + ("0", "Voltar") + ] + for key, label in options: + TUILayout.print_menu_option(key, label) + + # Rodapé Didático Contextual + try: + tip = self.tip_service.get_random_tip("GERAL") + TUILayout.print_tip(tip, "FOCO") + except: pass + + TUILayout.print_footer() + return input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha uma opção: {Style.RESET_ALL}").strip() + + def display_settings_menu(self, config): + TUILayout.clear() + TUILayout.print_header("CONFIGURAÇÕES") + + # Exibe caminhos truncados para caber no menu se necessário + TUILayout.print_menu_option("1", f"Pasta Clientes: {os.path.basename(config.get('caminho_pastaClientes'))}") + TUILayout.print_menu_option("2", f"Pasta Templates: {os.path.basename(config.get('caminho_templates'))}") + TUILayout.print_menu_option("3", f"Base de Dados: {os.path.basename(config.get('caminho_baseDados'))}") + TUILayout.print_menu_option("4", "Ferramentas Administrativas") + TUILayout.print_menu_option("5", "Abrir Pasta do Sistema (Workspace)") + TUILayout.print_menu_option("0", "Voltar") + + # Rodapé Didático Contextual + try: + tip = self.tip_service.get_random_tip("SANDBOX") + TUILayout.print_tip(tip, "CONFIG") + except: pass + + TUILayout.print_footer() + return input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha uma opção: {Style.RESET_ALL}").strip() def run(self): try: @@ -157,15 +251,15 @@ def run(self): elif choice == '2': self.handle_services() elif choice == '3': - self.handle_documents() + self.handle_webview_interface() elif choice == '4': - self.handle_productivity() + self.handle_documents() elif choice == '5': - self.handle_settings() + self.handle_productivity() elif choice == '6': - self.handle_installation() + self.handle_settings() elif choice == '7': - self.handle_deployment() + self.handle_installation() elif choice == '8': self.handle_watcher() elif choice == '0': @@ -178,13 +272,85 @@ def run(self): self.print_warning("Interrupção detectada. Encerrando o sistema com segurança...") sys.exit() + def handle_webview_interface(self): + """Interface de preenchimento: Escolha entre Terminal ou Visual (Agnóstico).""" + from pathlib import Path + TUILayout.clear() + TUILayout.print_header("PREENCHIMENTO DE FICHA") + + data_file = self.ui.select_file("Selecione o Arquivo de Dados (.md)", extensions=[".md"]) + if not data_file: + self.print_warning("Nenhum arquivo selecionado.") + return + + data_path = Path(data_file) + + try: + with open(data_path, "r", encoding="utf-8") as f: + content = f.read() + + # Callback para salvar + def save_fn(new_content): + try: + with open(data_path, "w", encoding="utf-8") as f: + f.write(new_content) + return True + except Exception as e: + logger.error(f"Erro ao salvar: {e}") + return False + + # Obtém o filler adequado via Porteiro + filler = self.porter.get_form_filler() + + # Se for servidor, ele já avisará que é TUI + if self.porter.profile == SystemProfile.SERVER_HEADLESS: + filler.open_form(content, save_fn) + input("Pressione Enter para continuar...") + return + + # No Desktop, damos a opção de TUI ou Visual + print(f"\n{Fore.YELLOW}Escolha o modo de preenchimento:{Style.RESET_ALL}") + print(" [1] Terminal (Nativo)") + print(" [2] Interface Rica (Visual/Web)") + print(" [0] Cancelar") + + sub_choice = input(f"\n{Fore.YELLOW}>> Escolha: {Style.RESET_ALL}").strip() + + if sub_choice == '1': + from foton_system.modules.documents.application.use_cases.tui_form_filler_use_case import TUIFormFillerUseCase + tui_filler = TUIFormFillerUseCase(data_path) + if tui_filler.execute(): + self.print_success("\n✅ Ficha atualizada com sucesso via Terminal!") + input("Pressione Enter para continuar...") + elif sub_choice == '2': + print(f"🚀 Iniciando interface para: {data_path.name}") + if not filler.open_form(content, save_fn): + self.print_error("Falha ao abrir interface visual.") + input("Enter...") + else: + self.print_warning("Operação cancelada.") + + except Exception as e: + self.print_error(f"Erro no pipeline de interface: {e}") + input("Pressione Enter para voltar...") + def handle_installation(self): from foton_system.modules.shared.infrastructure.services.install_service import InstallService - self.print_header("--- Instalação ---") - print("Isso criará atalhos na Área de Trabalho e Menu Iniciar apontando para este executável.") - print("Também garantirá que a pasta de configuração do usuário exista.") + TUILayout.clear() + TUILayout.print_header("INSTALAÇÃO E ATALHOS") + + print(f"\n {Fore.WHITE}Isso criará atalhos na Área de Trabalho e Menu Iniciar.") + print(f" Garante também a pasta de configuração local.") + + # Dica Contextual + try: + tip = self.tip_service.get_random_tip("GERAL") + TUILayout.print_tip(tip, "SETUP") + except: pass - if input("\nDeseja prosseguir? (S/N): ").upper() == 'S': + TUILayout.print_footer() + + if input(f"\n{Fore.YELLOW}Deseja prosseguir? (S/N): {Style.RESET_ALL}").upper() == 'S': try: InstallService().install() self.print_success("Instalação realizada com sucesso!") @@ -198,51 +364,76 @@ def handle_clients(self): choice = self.display_clients_menu() if choice == '1': self.client_service.sync_clients_db_from_folders() + input("Pressione Enter para continuar...") elif choice == '2': self.client_service.sync_client_folders_from_db() + input("Pressione Enter para continuar...") elif choice == '3': self.create_client_ui() + input("Pressione Enter para continuar...") elif choice == '4': self.search_client_ui() + input("Pressione Enter para continuar...") elif choice == '5': - self.print_header("--- Sincronizar Cadastro (Clientes) ---") - print("1. Exportar (DB -> Arquivo)") - print("2. Importar (Arquivo -> DB)") - sub = input("Escolha: ") - if sub == '1': - self.client_service.export_client_data() - elif sub == '2': - self.client_service.import_client_data() + self.handle_client_sync_menu() elif choice == '0': break else: self.print_error("Opção inválida.") + def handle_client_sync_menu(self): + TUILayout.clear() + TUILayout.print_header("SINCRONIZAR CADASTRO (CLIENTES)") + TUILayout.print_menu_option("1", "Exportar (DB -> Arquivo INFO)") + TUILayout.print_menu_option("2", "Importar (Arquivo INFO -> DB)") + TUILayout.print_menu_option("0", "Voltar") + TUILayout.print_footer() + + sub = input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha: {Style.RESET_ALL}") + if sub == '1': + self.client_service.export_client_data() + input("Pressione Enter para continuar...") + elif sub == '2': + self.client_service.import_client_data() + input("Pressione Enter para continuar...") + def handle_services(self): while True: choice = self.display_services_menu() if choice == '1': self.client_service.sync_services_db_from_folders() + input("Pressione Enter para continuar...") elif choice == '2': self.client_service.sync_service_folders_from_db() + input("Pressione Enter para continuar...") elif choice == '3': alias = input("Digite o Alias do Cliente: ").strip() if alias: self.client_service.sync_service_folders_from_db(client_alias=alias) + input("Pressione Enter para continuar...") elif choice == '4': - self.print_header("--- Sincronizar Cadastro (Serviços) ---") - print("1. Exportar (DB -> Arquivo)") - print("2. Importar (Arquivo -> DB)") - sub = input("Escolha: ") - if sub == '1': - self.client_service.export_service_data() - elif sub == '2': - self.client_service.import_service_data() + self.handle_service_sync_menu() elif choice == '0': break else: self.print_error("Opção inválida.") + def handle_service_sync_menu(self): + TUILayout.clear() + TUILayout.print_header("SINCRONIZAR CADASTRO (SERVIÇOS)") + TUILayout.print_menu_option("1", "Exportar (DB -> Arquivo INFO)") + TUILayout.print_menu_option("2", "Importar (Arquivo INFO -> DB)") + TUILayout.print_menu_option("0", "Voltar") + TUILayout.print_footer() + + sub = input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha: {Style.RESET_ALL}") + if sub == '1': + self.client_service.export_service_data() + input("Pressione Enter para continuar...") + elif sub == '2': + self.client_service.import_service_data() + input("Pressione Enter para continuar...") + def handle_documents(self): while True: choice = self.display_documents_menu() @@ -288,23 +479,15 @@ def handle_settings(self): try: os.startfile(config.workspace_path) self.print_success(f"Abrindo pasta: {config.workspace_path}") + input("Pressione Enter para continuar...") except Exception as e: self.print_error(f"Erro ao abrir pasta: {e}") + input("Pressione Enter para continuar...") elif choice == '0': break else: self.print_error("Opção inválida.") - def display_settings_menu(self, config): - self.print_header("--- Configurações ---") - print(f"1. Pasta de Clientes: {config.get('caminho_pastaClientes')}") - print(f"2. Pasta de Templates: {config.get('caminho_templates')}") - print(f"3. Base de Dados: {config.get('caminho_baseDados')}") - print("4. Ferramentas Administrativas") - print(f"5. Abrir Pasta do Sistema (Workspace): {config.workspace_path}") - print("0. Voltar") - return input(f"{Fore.YELLOW}Para alterar, digite o número da opção: {Style.RESET_ALL}") - def update_setting_ui(self, config, key, title, is_file=False): print(f"\nSelecione o novo local para: {title}") @@ -320,15 +503,19 @@ def update_setting_ui(self, config, key, title, is_file=False): config.set(key, path) config.save() self.print_success(f"Configuração atualizada com sucesso!\nNovo valor: {path}") + input("Pressione Enter para continuar...") else: self.print_warning("Operação cancelada.") + input("Pressione Enter para continuar...") def create_client_ui(self): - self.print_header("--- Novo Cliente ---") - nome = input("Nome do Cliente: ") - alias = input("Alias (Apelido da Pasta): ") - telefone = input("Telefone: ") + TUILayout.clear() + TUILayout.print_header("NOVO CLIENTE") + print("\n Preencha os dados básicos:\n") + nome = input(" Nome do Cliente: ") + alias = input(" Alias (Apelido): ") + telefone = input(" Telefone : ") data = { 'NomeCliente': nome, @@ -338,21 +525,21 @@ def create_client_ui(self): try: self.client_service.create_client(data) - self.print_success("Cliente criado com sucesso!") + self.print_success("\n✅ Cliente criado com sucesso!") except ValueError as ve: - self.print_error(f"Erro de Validação: {ve}") + self.print_error(f"\n❌ Erro de Validação: {ve}") except Exception as e: - self.print_error(f"Erro ao criar cliente: {e}") + self.print_error(f"\n❌ Erro ao criar cliente: {e}") def search_client_ui(self): - self.print_header("--- Buscar Cliente ---") - term = input("Digite o nome ou alias para buscar: ").strip().lower() + TUILayout.clear() + TUILayout.print_header("BUSCAR CLIENTE") + term = input("\n Digite o nome ou alias: ").strip().lower() if not term: return try: df = self.client_repo.get_clients_dataframe() - # Filter by Name or Alias (case insensitive) mask = ( df['NomeCliente'].astype(str).str.lower().str.contains(term, na=False) | df['Alias'].astype(str).str.lower().str.contains(term, na=False) @@ -360,59 +547,57 @@ def search_client_ui(self): results = df[mask] if results.empty: - self.print_warning("Nenhum cliente encontrado.") + self.print_warning("\n📭 Nenhum cliente encontrado.") else: - self.print_success(f"\n{len(results)} clientes encontrados:") + self.print_success(f"\n🔍 {len(results)} clientes encontrados:") for _, row in results.iterrows(): - print(f"- {row['NomeCliente']} (Alias: {row['Alias']})") + print(f" - {row['NomeCliente']} (Alias: {row['Alias']})") except Exception as e: - self.print_error(f"Erro ao buscar clientes: {e}") + self.print_error(f"\n❌ Erro ao buscar clientes: {e}") def generate_document_ui(self, doc_type): from foton_system.modules.shared.infrastructure.config.config import Config from pathlib import Path - self.print_header(f"--- Gerar Documento ({doc_type.upper()}) ---") + TUILayout.clear() + TUILayout.print_header(f"GERAR DOCUMENTO ({doc_type.upper()})") - # 1. Select Client Folder via UI Provider (TUI or GUI) - print("Selecione a pasta do cliente...") + # 1. Select Client Folder + print("\n Selecione a pasta do cliente...") client_folder = self.ui.select_directory("Selecione a Pasta do Cliente") if not client_folder: - self.print_warning("Nenhuma pasta selecionada.") + self.print_warning(" Operação cancelada.") return client_path = Path(client_folder) - print(f"Pasta selecionada: {client_path}") - - + # 2. Check/Create Data File Pipeline data_files = self.document_service.list_client_data_files(client_path) - selected_file = None if data_files: - print("\nArquivos de dados encontrados:") + print("\n Arquivos de dados encontrados:") for i, f in enumerate(data_files): - print(f"{i + 1}. {f.name}") - print(f"{len(data_files) + 1}. Criar novo arquivo") + print(f" {i + 1}. {f.name}") + print(f" {len(data_files) + 1}. Criar novo arquivo") try: - choice = int(input("Escolha uma opção: ")) + choice = int(input("\n Escolha uma opção: ")) if 1 <= choice <= len(data_files): selected_file = data_files[choice - 1] elif choice == len(data_files) + 1: selected_file = self._create_new_data_file_ui(client_path) else: - self.print_error("Opção inválida.") + self.print_error(" Opção inválida.") return except ValueError: - self.print_error("Entrada inválida.") + self.print_error(" Entrada inválida.") return else: - self.print_warning("\nNenhum arquivo de dados encontrado.") - create = input("Deseja criar um novo arquivo? (S/N): ").upper() + self.print_warning("\n Nenhum arquivo de dados encontrado.") + create = input(" Deseja criar um novo arquivo? (S/N): ").upper() if create == 'S': selected_file = self._create_new_data_file_ui(client_path) else: @@ -421,31 +606,22 @@ def generate_document_ui(self, doc_type): if not selected_file: return - print(f"Arquivo selecionado: {selected_file.name}") - - # Option to edit data file? - edit = input("Deseja abrir o arquivo de dados para edição antes de continuar? (S/N): ").upper() - if edit == 'S': - import os - os.startfile(selected_file) - input("Pressione Enter após salvar e fechar o arquivo de dados...") - # 3. Select Template templates = self.document_service.list_templates(doc_type) if not templates: - self.print_warning("Nenhum template encontrado.") + self.print_warning(" Nenhum template encontrado.") return - print("\nSelecione o Template:") + print("\n Selecione o Template:") template_name = self._select_from_list(templates) if not template_name: return template_path = Config().templates_path / template_name - # 4. Output Path (same as client folder) + # 4. Output Path default_output = f"Proposta_{client_path.name}" - output_name = input(f"Nome do arquivo de saída (padrão: {default_output}): ") or default_output + output_name = input(f"\n Nome de saída (padrão: {default_output}): ") or default_output if doc_type == 'pptx' and not output_name.endswith('.pptx'): output_name += '.pptx' elif doc_type == 'docx' and not output_name.endswith('.docx'): @@ -453,301 +629,201 @@ def generate_document_ui(self, doc_type): output_path = client_path / output_name - # Validate Keys before generation - missing = self.document_service.validate_template_keys(str(template_path), str(selected_file), doc_type) - if missing: - self.print_warning(f"\n[AVISO] As seguintes chaves estão no template mas não no arquivo de dados:") - for k in missing: - print(f" - {k}") - - from foton_system.modules.shared.infrastructure.config.config import Config - if Config().clean_missing_variables: - print(f"Elas serão substituídas por '{Config().missing_variable_placeholder}'.") - - confirm = input("Deseja continuar mesmo assim? (S/N): ").upper() - if confirm != 'S': - self.print_warning("Operação cancelada.") - return - try: self.document_service.generate_document(str(template_path), str(selected_file), str(output_path), doc_type) - self.print_success(f"Documento gerado com sucesso em: {output_path}") - - # Open folder via UI Provider + self.print_success(f"\n✅ Sucesso! Gerado em: {output_path}") self.ui.open_folder(client_path) - + input("\nPressione Enter para continuar...") except Exception as e: - self.print_error(f"Erro ao gerar documento: {e}") - + self.print_error(f"\n❌ Erro ao gerar: {e}") + input("\nPressione Enter para continuar...") def _select_from_list(self, items): for i, item in enumerate(items): - print(f"{i + 1}. {item}") + print(f" {i + 1}. {item}") try: - choice = int(input(f"{Fore.YELLOW}Digite o número da opção: {Style.RESET_ALL}")) + choice = int(input(f"\n {Fore.YELLOW}Opção: {Style.RESET_ALL}")) if 1 <= choice <= len(items): return items[choice - 1] else: - self.print_error("Opção inválida.") + self.print_error(" Opção inválida.") return None except ValueError: - self.print_error("Entrada inválida.") + self.print_error(" Entrada inválida.") return None def _create_new_data_file_ui(self, client_path): - self.print_header("--- Criar Novo Arquivo de Dados ---") - print("Padrão: 02-{COD}_DOC_PC_{VER}_{REV}_{DESC}.md") - - cod = input("Código do Serviço (COD) [ex: 001]: ") + print("\n Padrão: 02-{COD}_DOC_PC_{VER}_{REV}_{DESC}.md") + cod = input(" Código (COD) [ex: 001]: ") if not cod: - self.print_error("Código é obrigatório.") + self.print_error(" Código é obrigatório.") return None - ver = input("Versão (VER) [padrão: 00]: ") or "00" - rev = input("Revisão (REV) [padrão: R00]: ") or "R00" - desc = input("Descrição (DESC) [padrão: PROPOSTA]: ") or "PROPOSTA" + ver = input(" Versão (VER) [00]: ") or "00" + rev = input(" Revisão (REV) [R00]: ") or "R00" + desc = input(" Descrição (DESC) [PROPOSTA]: ") or "PROPOSTA" return self.document_service.create_custom_data_file(client_path, cod, ver, rev, desc) def start_pomodoro_ui(self): from foton_system.modules.shared.infrastructure.config.config import Config config = Config() + TUILayout.clear() + TUILayout.print_header("TIMER POMODORO") try: # Load defaults - default_work = config.pomodoro_work_time - default_short = config.pomodoro_short_break - default_long = config.pomodoro_long_break - default_cycles = config.pomodoro_cycles + work = config.pomodoro_work_time + short = config.pomodoro_short_break + long = config.pomodoro_long_break + cycles = config.pomodoro_cycles - self.print_header("--- Iniciar Pomodoro ---") - print(f"Configuração Atual: Trabalho={default_work}m, Curta={default_short}m, Longa={default_long}m, Ciclos={default_cycles}") - - # Linking + print(f"\n Foco: {work}m | Pausa: {short}m | Ciclos: {cycles}") + client_alias = None - service_alias = None - link = input("Deseja vincular a um cliente? (S/N): ").upper() + link = input("\n Vincular a um cliente? (S/N): ").upper() if link == 'S': - # Reuse search or list? Let's use search for quick access or list if empty - # For simplicity, let's ask for name/alias search - term = input("Digite o nome ou alias do cliente: ").strip() + term = input(" Nome/Alias: ").strip() if term: df = self.client_repo.get_clients_dataframe() - mask = ( - df['NomeCliente'].astype(str).str.lower().str.contains(term.lower(), na=False) | - df['Alias'].astype(str).str.lower().str.contains(term.lower(), na=False) - ) - results = df[mask] - if not results.empty: - # Auto-select first or ask? Let's ask if multiple, or just take first for speed - if len(results) > 1: - print(f"{len(results)} clientes encontrados. Usando o primeiro: {results.iloc[0]['NomeCliente']}") - client_alias = results.iloc[0]['Alias'] - self.print_success(f"Vinculado ao cliente: {client_alias}") - - service_input = input("Nome do Serviço (opcional): ").strip() - if service_input: - service_alias = service_input - else: - self.print_warning("Cliente não encontrado. Seguindo sem vínculo.") - - # Custom overrides - change = input("Deseja alterar os tempos? (S/N): ").upper() - if change == 'S': - work = float(input(f"Tempo de trabalho (min) [{default_work}]: ") or default_work) - short = float(input(f"Pausa curta (min) [{default_short}]: ") or default_short) - long = float(input(f"Pausa longa (min) [{default_long}]: ") or default_long) - cycles = int(input(f"Ciclos [{default_cycles}]: ") or default_cycles) - else: - work, short, long, cycles = default_work, default_short, default_long, default_cycles + mask = df['Alias'].str.lower().str.contains(term.lower(), na=False) + res = df[mask] + if not res.empty: + client_alias = res.iloc[0]['Alias'] + self.print_success(f" Vínculo: {client_alias}") - timer = PomodoroTimer(work, short, long, cycles, client_alias, service_alias) + timer = PomodoroTimer(work, short, long, cycles, client_alias) timer.run() - except ValueError: - self.print_error("Valores inválidos.") - except KeyboardInterrupt: - print("\n") - self.print_warning("Operação interrompida.") + except Exception as e: + self.print_error(f"Erro no timer: {e}") def handle_admin_tools(self): try: from foton_system.scripts.admin_launcher import main_menu main_menu() - except ImportError: - self.print_error("Erro: Launcher administrativo não encontrado.") except Exception as e: - self.print_error(f"Erro ao abrir ferramentas administrativas: {e}") - def handle_deployment(self): - """Menu para gerenciar a base de dados e implantação.""" - try: - from foton_system.scripts.deployment_manager import DeploymentManager - manager = DeploymentManager() - manager.interactive_menu() - except ImportError: - self.print_error("Erro: Gerenciador de Deployment não encontrado.") - except Exception as e: - logger.error(f"Erro no menu de deployment: {e}", exc_info=True) - self.print_error(f"Erro ao abrir gerenciador de deployment: {e}") + self.print_error(f"Erro: {e}") def validate_template_ui(self): - """Interface de validação pré-voo de templates.""" from foton_system.modules.shared.infrastructure.config.config import Config from pathlib import Path + TUILayout.clear() + TUILayout.print_header("VALIDAR TEMPLATE") - self.print_header("--- Validar Template (Pré-voo) ---") - - # 1. Selecionar pasta do cliente - print("Selecione a pasta do cliente...") - client_folder = self.ui.select_directory("Selecione a Pasta do Cliente") - if not client_folder: - self.print_warning("Nenhuma pasta selecionada.") - return + print("\n Selecione a pasta do cliente...") + client_folder = self.ui.select_directory("Selecione a Pasta") + if not client_folder: return client_path = Path(client_folder) - print(f"Pasta selecionada: {client_path}") - - # 2. Selecionar arquivo de dados data_files = self.document_service.list_client_data_files(client_path) if not data_files: - self.print_warning("Nenhum arquivo de dados encontrado.") + self.print_warning(" Nenhum arquivo INFO encontrado.") return - print("\nArquivos de dados encontrados:") - selected_file = None + print("\n Arquivos disponíveis:") for i, f in enumerate(data_files): - print(f"{i + 1}. {f.name}") - + print(f" {i+1}. {f.name}") + try: - choice = int(input("Escolha o arquivo de dados: ")) - if 1 <= choice <= len(data_files): - selected_file = data_files[choice - 1] - else: - self.print_error("Opção inválida.") - return - except ValueError: - self.print_error("Entrada inválida.") - return + idx = int(input("\n Escolha: ")) - 1 + selected_file = data_files[idx] + except: return - # 3. Selecionar template - print("\nSelecione o tipo de documento:") - print("1. PPTX (Proposta)") - print("2. DOCX (Contrato)") - doc_choice = input("Escolha: ") - doc_type = 'pptx' if doc_choice == '1' else 'docx' + print("\n Tipo: [1] PPTX | [2] DOCX") + doc_type = 'pptx' if input(" Escolha: ") == '1' else 'docx' templates = self.document_service.list_templates(doc_type) - if not templates: - self.print_warning("Nenhum template encontrado.") - return - - print("\nSelecione o Template:") + print("\n Templates:") template_name = self._select_from_list(templates) - if not template_name: - return + if not template_name: return template_path = Config().templates_path / template_name - - # 4. Executar validação missing = self.document_service.validate_template_keys(str(template_path), str(selected_file), doc_type) if not missing: - self.print_success(f"\n✅ PRÉ-VOO OK! Template '{template_name}' está completo.") - self.print_success(f" Todos os campos do template estão presentes em '{selected_file.name}'.") + self.print_success("\n✅ TUDO PRONTO! Variáveis validadas.") else: - self.print_warning(f"\n⚠️ PRÉ-VOO: {len(missing)} variáveis faltando:") - for k in missing: - print(f" ❌ {k}") - print(f"\n📄 Template: {template_name}") - print(f"📋 Dados: {selected_file.name}") + self.print_warning(f"\n⚠️ FALTANDO {len(missing)} VARIÁVEIS:") + for k in missing: print(f" ❌ {k}") input("\nPressione Enter para voltar...") def handle_watcher(self): - """Menu para gerenciar o modo Sentinela (Watcher) e base de conhecimento.""" - self.print_header("--- Modo Sentinela (Watcher) ---") - print("Monitora mudanças nas pastas de clientes e sincroniza automaticamente.") - print("\n1. Ativar Watcher") - print("2. Desativar Watcher") - print("3. Indexar Base de Conhecimento (RAG)") - print("4. Consultar Conhecimento") - print("0. Voltar") - - choice = input(f"{Fore.YELLOW}Escolha uma opção: {Style.RESET_ALL}") - - if choice == '1': - self.print_warning("Iniciando Watcher...") + while True: + TUILayout.clear() + TUILayout.print_header("MODO SENTINELA (WATCHER)") + + options = [ + ("1", "Ativar Watcher"), + ("2", "Desativar Watcher"), + ("3", "Indexar Base de Conhecimento (RAG)"), + ("4", "Consultar Conhecimento"), + ("0", "Voltar") + ] + for key, label in options: + TUILayout.print_menu_option(key, label) + try: - from foton_system.core.watcher.service import WatcherService - watcher = WatcherService() - watcher.start() - self.print_success("Watcher ativado com sucesso!") - except Exception as e: - logger.error(f"Erro ao ativar Watcher: {e}", exc_info=True) - self.print_error(f"Erro ao ativar Watcher: {e}") - elif choice == '2': - self.print_warning("Watcher desativado.") - elif choice == '3': - self._index_knowledge_ui() - elif choice == '4': - self._query_knowledge_ui() - elif choice != '0': - self.print_error("Opção inválida.") + tip = self.tip_service.get_random_tip("IA") + TUILayout.print_tip(tip, "SENTINELA") + except: pass - def _index_knowledge_ui(self): - """Interface para indexação da base de conhecimento.""" - self.print_header("--- Indexar Base de Conhecimento ---") - print("Isso irá escanear todos os documentos (.md, .txt) e indexá-los") - print("para busca semântica (RAG).\n") + TUILayout.print_footer() + choice = input(f"{Fore.CYAN}>> {Fore.WHITE}Escolha: {Style.RESET_ALL}").strip() - confirm = input("Deseja prosseguir? (S/N): ").upper() - if confirm != 'S': - self.print_warning("Operação cancelada.") - return + if choice == '1': + self.print_warning(" Iniciando Watcher...") + try: + from foton_system.core.watcher.service import WatcherService + watcher = WatcherService() + watcher.start() + self.print_success(" Watcher ativado!") + input("Enter...") + except Exception as e: + self.print_error(f"Erro: {e}") + input("Enter...") + elif choice == '2': + self.print_warning(" Desativado.") + input("Enter...") + elif choice == '3': + self._index_knowledge_ui() + elif choice == '4': + self._query_knowledge_ui() + elif choice == '0': + break + + def _index_knowledge_ui(self): + TUILayout.clear() + TUILayout.print_header("INDEXAR CONHECIMENTO") + print("\n Escaneando documentos para RAG...") + if input("\n Prosseguir? (S/N): ").upper() != 'S': return try: from foton_system.core.ops.op_index_knowledge import OpIndexKnowledge - op = OpIndexKnowledge(actor="CLI_User") - print("\n🧠 Indexando... (isso pode demorar na primeira vez)") - result = op.execute() - self.print_success( - f"\n✅ Base de Conhecimento Atualizada!\n" - f" Arquivos processados: {result.get('files_scanned', 0)}\n" - f" Chunks criados: {result.get('chunks_created', 0)}" - ) - except ImportError: - self.print_error("RAG indisponível: instale 'chromadb' e 'sentence-transformers'.") + op = OpIndexKnowledge(actor="User") + res = op.execute() + self.print_success(f"\n✅ Indexado: {res.get('files_scanned')} arquivos.") except Exception as e: - logger.error(f"Erro ao indexar: {e}", exc_info=True) - self.print_error(f"Erro ao indexar: {e}") - - input("Pressione Enter para voltar...") + self.print_error(f"Erro: {e}") + input("\nEnter...") def _query_knowledge_ui(self): - """Interface para consultar a base de conhecimento.""" - self.print_header("--- Consultar Conhecimento ---") - query = input("Digite sua pergunta: ").strip() - if not query: - self.print_warning("Nenhuma pergunta fornecida.") - return + TUILayout.clear() + TUILayout.print_header("CONSULTAR CONHECIMENTO") + query = input("\n Pergunta: ").strip() + if not query: return try: from foton_system.core.ops.op_query_knowledge import OpQueryKnowledge - op = OpQueryKnowledge(actor="CLI_User") - result = op.execute(query=query) - - if result['status'] == 'EMPTY': - self.print_warning("📭 Nenhum resultado encontrado na base.") + op = OpQueryKnowledge(actor="User") + res = op.execute(query=query) + if res['status'] == 'EMPTY': + self.print_warning(" Nada encontrado.") else: - self.print_success(f"\n🔍 {result['total']} resultados para: \"{query}\"\n") - for i, r in enumerate(result['results'], 1): - print(f"--- [{i}] Fonte: {r['source']} (Similaridade: {r['score']:.0%}) ---") - print(f"{r['document'][:500]}") - print() - except ImportError: - self.print_error("RAG indisponível: instale 'chromadb' e 'sentence-transformers'.") + for i, r in enumerate(res['results'], 1): + print(f"\n [{i}] {r['source']} ({r['score']:.0%})") + print(f" {r['document'][:200]}...") except Exception as e: - logger.error(f"Erro na consulta: {e}", exc_info=True) self.print_error(f"Erro: {e}") - - input("Pressione Enter para voltar...") \ No newline at end of file + input("\nEnter...") diff --git a/foton_system/interfaces/cli/views/form_view.py b/foton_system/interfaces/cli/views/form_view.py new file mode 100644 index 0000000..268c8d5 --- /dev/null +++ b/foton_system/interfaces/cli/views/form_view.py @@ -0,0 +1,103 @@ +""" +TUI Form View - Interface Interativa. +Renderiza o formato do arquivo no visualizador com destaque para edições. +""" + +from colorama import Fore, Style +from foton_system.modules.documents.domain.models.form_session import FormSession +from foton_system.modules.shared.infrastructure.services.tip_service import TipService +from foton_system.interfaces.cli.views.tui_layout import TUILayout + +class TUIFormView: + def __init__(self, session: FormSession, title: str = "Preencher Ficha"): + self.session = session + self.title = title + self.tip_service = TipService() + + def run_loop(self) -> str: + while True: + self._draw() + cmd = input(f"\n{Fore.CYAN}>> Ação ou Novo Valor: {Style.RESET_ALL}").strip() + cmd_lower = cmd.lower() + if cmd_lower == '' or cmd_lower == 'n': self.session.next() + elif cmd_lower == 'p': self.session.prev() + elif cmd_lower == 'v': self._show_preview() + elif cmd_lower == 's': + if input(f"\n{Fore.GREEN}Salvar? (S/N): {Style.RESET_ALL}").lower() == 's': return "save" + elif cmd_lower == 'a': + return "save_as" + elif cmd_lower == 'c': + if input(f"\n{Fore.RED}Sair sem salvar? (S/N): {Style.RESET_ALL}").lower() == 's': return "cancel" + else: + f = self.session.get_current_field() + if f and not f.is_calculated: + if cmd: + self.session.update_current(cmd) + self.session.next() + + def _draw(self): + TUILayout.clear() + f = self.session.get_current_field() + idx, total = self.session.cursor + 1, len(self.session.fields) + + TUILayout.print_header(self.title) + + print(f"\n {Fore.YELLOW}Progresso: [{idx}/{total}]{Style.RESET_ALL}") + + if f: + tag = "[📐 CALC]" if f.is_calculated else "[✍️ INPUT]" + TUILayout.print_field(f"Variável @{f.name}", tag, is_calc=f.is_calculated) + + label = "Valor/Desc" if not f.is_calculated else "Descrição" + print(f" {Fore.WHITE}{label.ljust(12)}: {f.description}") + + if f.hint: + print(f" {Fore.YELLOW}Dica{' '*9}: {f.hint}{Style.RESET_ALL}") + + if f.is_calculated: + print(f" {Fore.GREEN}Fórmula{' '*6}: {f.formula}{Style.RESET_ALL}") + print(f"\n {Style.BRIGHT}✨ Resultado: {Fore.WHITE}{f.current_value}{Style.RESET_ALL}") + else: + curr = f.current_value if f.current_value else "(vazio)" + print(f"\n {Style.BRIGHT}👉 Valor Atual: {Fore.WHITE}{curr}{Style.RESET_ALL}") + + # Dica Didática Contextual + try: + tip_ctx = "FORMATACAO" if f and not f.is_calculated else "SSOT" + tip = self.tip_service.get_random_tip(tip_ctx) + TUILayout.print_tip(tip, "DICA") + except: pass + + TUILayout.print_footer() + print(f" {Fore.YELLOW}[ENTER/N]{Style.RESET_ALL} Próxima | {Fore.YELLOW}[P]{Style.RESET_ALL} Anterior | {Fore.YELLOW}[V]{Style.RESET_ALL} Visualizar") + print(f" {Fore.GREEN}[S]{Style.RESET_ALL} Salvar | {Fore.CYAN}[A]{Style.RESET_ALL} Salvar Como | {Fore.RED}[C]{Style.RESET_ALL} Cancelar") + + def _show_preview(self): + TUILayout.clear() + TUILayout.print_header("PRÉ-VISUALIZAÇÃO") + + print(f" {Style.DIM}Legenda: {Fore.WHITE}Original {Fore.CYAN}Modificado {Fore.GREEN}Calculado{Style.RESET_ALL}\n") + + field_dict = {f.name: f for f in self.session.fields} + + for item in self.session.structure: + if item["type"] == "text": + # Quebra o texto original para não estourar o terminal + wrapped = TUILayout.wrap_text(item['content'], indent=2) + print(f"{Style.DIM}{wrapped}{Style.RESET_ALL}") + else: + f = field_dict[item["name"]] + prefix = f" @{f.name}; " + if f.is_calculated: + val = f"[calculo: {f.formula}] {f.description}" + print(f"{prefix}{Fore.GREEN}{val}{Style.RESET_ALL}") + elif f.is_dirty: + val = f.current_value + if f.hint: val += f" {f.hint}" + print(f"{prefix}{Fore.CYAN}{val}{Style.RESET_ALL}") + else: + print(f"{prefix}{f.original_value}") + + print("") + TUILayout.print_footer() + input(f"\n{Fore.YELLOW}Pressione ENTER para voltar ao formulário...{Style.RESET_ALL}") diff --git a/foton_system/interfaces/cli/views/tui_layout.py b/foton_system/interfaces/cli/views/tui_layout.py new file mode 100644 index 0000000..efee2fe --- /dev/null +++ b/foton_system/interfaces/cli/views/tui_layout.py @@ -0,0 +1,128 @@ +""" +TUI Layout Helper - Orquestrador de Design e Interface Dinâmica. +Gerencia larguras, quebras de linha, enquadramentos e componentes visuais. +""" + +import os +import shutil +import textwrap +import re +from colorama import Fore, Style +from typing import List, Optional + +class TUILayout: + """ + Centraliza as regras de design para a TUI do Foton System. + Garante que a interface se adapte ao tamanho do terminal e mantenha bordas perfeitas. + """ + + DEFAULT_WIDTH = 70 + MIN_WIDTH = 40 + MAX_WIDTH = 100 + + @staticmethod + def get_width() -> int: + """Calcula a largura ideal baseada no terminal atual.""" + try: + columns, _ = shutil.get_terminal_size() + width = min(TUILayout.MAX_WIDTH, max(TUILayout.MIN_WIDTH, columns - 4)) + return width + except Exception: + return TUILayout.DEFAULT_WIDTH + + @staticmethod + def clear(): + """Limpa a tela do terminal.""" + os.system('cls' if os.name == 'nt' else 'clear') + + @staticmethod + def get_visible_len(text: str) -> int: + """ + Calcula o comprimento visual real de uma string. + - Ignora sequências de escape ANSI (cores). + - Compensa Emojis (contam como 2 colunas na maioria dos terminais). + """ + # 1. Remover cores ANSI + ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') + clean_text = ansi_escape.sub('', text) + + # 2. Heurística para Emojis (maioria ocupa 2 espaços) + # Regex para range comum de emojis + emoji_pattern = re.compile(r'[\U00010000-\U0010ffff]', flags=re.UNICODE) + num_emojis = len(emoji_pattern.findall(clean_text)) + + return len(clean_text) + num_emojis + + @staticmethod + def print_line(content: str, width: int, color: str = Fore.CYAN, align: str = 'left'): + """Desenha uma linha enquadrada com preenchimento resiliente.""" + visible_len = TUILayout.get_visible_len(content) + padding_needed = max(0, width - visible_len - 4) # 4 = bordas + margens + + if align == 'center': + left_pad = padding_needed // 2 + right_pad = padding_needed - left_pad + print(f"{color}║ {' ' * left_pad}{content}{' ' * right_pad} {color}║") + else: + print(f"{color}║ {content}{' ' * padding_needed} {color}║") + + @staticmethod + def print_header(title: str, color: str = Fore.CYAN): + """Desenha um cabeçalho enquadrado.""" + width = TUILayout.get_width() + print(f"{color}╔{'═' * (width-2)}╗") + TUILayout.print_line(f"{Style.BRIGHT}{title}{Style.NORMAL}", width, color, align='center') + print(f"{color}╠{'═' * (width-2)}╣{Style.RESET_ALL}") + + @staticmethod + def print_footer(color: str = Fore.CYAN): + """Desenha o fechamento de um box.""" + width = TUILayout.get_width() + print(f"{color}╚{'═' * (width-2)}╝{Style.RESET_ALL}") + + @staticmethod + def print_tip(tip: str, context: str = "DICA"): + """Renderiza uma dica didática com alinhamento de bordas garantido.""" + width = TUILayout.get_width() + color = Fore.CYAN + + # Emojis distorcem o len() do textwrap, então tratamos o prefixo e o wrap separadamente + emoji_icon = "💡" + prefix = f" {emoji_icon} {context}: " + # Visualmente o prefixo ocupa len(prefix) + 1 (por causa do emoji) + visual_prefix_len = len(prefix) + 1 + + available_width = width - visual_prefix_len - 5 + + wrapper = textwrap.TextWrapper(width=available_width) + wrapped_lines = wrapper.wrap(tip) + + print(f"{color}╠{'─' * (width-2)}╣") + + for i, line in enumerate(wrapped_lines): + if i == 0: + content = f"{Style.DIM}{Fore.LIGHTBLACK_EX}{prefix}{line}{Style.NORMAL}" + else: + content = f"{' ' * visual_prefix_len}{Style.DIM}{Fore.LIGHTBLACK_EX}{line}{Style.NORMAL}" + + TUILayout.print_line(content, width, color) + + @staticmethod + def print_menu_option(key: str, label: str, color: str = Fore.CYAN): + """Renderiza uma opção de menu alinhada.""" + width = TUILayout.get_width() + content = f"{Fore.YELLOW}{key}. {Fore.WHITE}{label}" + TUILayout.print_line(content, width, color) + + @staticmethod + def print_field(label: str, value: str, tag: str = "", is_calc: bool = False): + """Renderiza um campo de formulário.""" + tag_color = Fore.GREEN if is_calc else Fore.BLUE + print(f"\n {Fore.WHITE}{label}: {Style.BRIGHT}{value}{Style.RESET_ALL} {tag_color}{tag}{Style.RESET_ALL}") + + @staticmethod + def wrap_text(text: str, indent: int = 4) -> str: + """Quebra um texto longo para a largura do terminal.""" + width = TUILayout.get_width() - indent - 4 + wrapper = textwrap.TextWrapper(width=width, initial_indent=" " * indent, subsequent_indent=" " * indent) + return "\n".join(wrapper.wrap(text)) diff --git a/foton_system/interfaces/fotonInfoInterface.html b/foton_system/interfaces/fotonInfoInterface.html new file mode 100644 index 0000000..8d6be10 --- /dev/null +++ b/foton_system/interfaces/fotonInfoInterface.html @@ -0,0 +1,743 @@ + + + + + + Preenchedor Inteligente de Markdown + + + +
+
+

📋 Preenchedor Inteligente de Templates

+

Cole um markdown com variáveis @chave;descrição. Fórmulas [calculo: ...] podem ser editadas.

+
+ +
+ + +
+ + +
+
+ +
+ + + + +
+
+ + + + \ No newline at end of file diff --git a/foton_system/interfaces/mcp/foton_mcp.py b/foton_system/interfaces/mcp/foton_mcp.py index a81c9ab..8f3b420 100644 --- a/foton_system/interfaces/mcp/foton_mcp.py +++ b/foton_system/interfaces/mcp/foton_mcp.py @@ -109,8 +109,8 @@ def _get_config(): @mcp.tool() def ping() -> str: """ - Health check. Returns instantly if the FOTON MCP server is alive. - Use this to verify connectivity before calling other tools. + Verifies that the Foton MCP server is responsive. + PROTOCOL: Use this as the very first tool call to ensure the link is active. """ _logger.info("Tool called: ping") return f"🟢 FOTON MCP Online (pid={__import__('os').getpid()}, ts={int(time.time())})" @@ -119,15 +119,17 @@ def ping() -> str: @mcp.tool() def info_sistema() -> str: """ - Returns a full diagnostic of the FOTON system: configured paths, client count, - template count, module availability, and version info. Use this to understand - the current system state and available resources before starting work. + Provides a comprehensive diagnostic of the Foton system's environment. + CONTEXT: Call this at the start of a session to understand folder paths, client counts, + template availability, and active business rules (like missing variable placeholders). + Returns path configurations and module availability. """ _logger.info("Tool called: info_sistema") try: config = _get_config() clients_dir = config.base_pasta_clientes templates_dir = config.templates_path + mode_str = "🧪 SANDBOX (Ambiente de Teste)" if PathManager.is_sandbox_active() else "🏗️ PRODUÇÃO" client_count = 0 if clients_dir.exists(): @@ -147,6 +149,7 @@ def info_sistema() -> str: output = ( "📊 FOTON System Status\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" + f" 🛠️ Modo: {mode_str}\n" f" 📂 Clientes: {clients_dir}\n" f" → {client_count} cliente(s) encontrado(s)\n" f" 📄 Templates: {templates_dir}\n" @@ -158,7 +161,7 @@ def info_sistema() -> str: ) return output except Exception as e: - return f"❌ Erro ao obter info do sistema: {e}" + return f"❌ Error retrieving system info: {e}" # ============================================================================== @@ -168,14 +171,9 @@ def info_sistema() -> str: @mcp.tool() def listar_clientes() -> str: """ - Lists all registered clients (architecture projects) in the firm's directory. - Each client is a folder inside the configured 'caminho_pastaClientes' path. - - Returns the client name, whether an INFO file exists (📁 = has INFO, 📂 = no INFO), - and the count of services (sub-projects) under that client. - - Use this as the FIRST STEP when the user asks about clients, projects, or wants - to perform any operation on a specific client. + Lists all registered clients in the architecture firm. + PROTOCOL: Always call this before performing any operation on a client you're not 100% sure exists. + OUTPUT: Indicates if the client has a "Center of Truth" (📁 = has INFO file) and the count of sub-services. """ _logger.info("Tool called: listar_clientes") try: @@ -198,10 +196,8 @@ def listar_clientes() -> str: output = f"📋 {len(clients)} client(s) found:\n" for c in clients: client_path = clients_dir / c - # Check for INFO file info_files = list(client_path.glob("*INFO*.md")) has_info = len(info_files) > 0 - # Count services (subfolders not in ignored list) services = [ s.name for s in client_path.iterdir() if s.is_dir() and s.name not in ignored @@ -219,23 +215,9 @@ def listar_clientes() -> str: @mcp.tool() def cadastrar_cliente(nome: str, apelido: str = "", nif: str = "", email: str = "", telefone: str = "") -> str: """ - Creates a new client in the FOTON system (Audited Standard Operation / POP). - - This creates: - - The client folder with standard sub-structure (01_ADMINISTRATIVO, 02_FINANCEIRO, 03_PROJETOS) - - An INFO-CLIENTE.md file (the client's Single Source of Truth) - - A FINANCEIRO.csv ledger file - - A record in the master Excel database (baseClientes.xlsx) - - IMPORTANT: Before calling this, use 'listar_clientes' to verify the client does NOT - already exist. Use 'pipeline_novo_cliente' for the full safe workflow. - - Args: - nome: Full client name (required, min 3 chars) - apelido: Short alias/code for the folder name (optional, auto-generated if blank) - nif: Tax ID / CPF / CNPJ (optional) - email: Contact email (optional) - telefone: Contact phone (optional) + Creates a new client folder and master record. + SAFETY: Use 'pipeline_novo_cliente' instead for a safer, non-duplicate workflow. + Logic: Creates standard folders (ADMINISTRATIVO, FINANCEIRO, PROJETOS) and initial INFO and FINANCEIRO files. """ _logger.info(f"Tool called: cadastrar_cliente(nome={nome})") try: @@ -255,43 +237,33 @@ def cadastrar_cliente(nome: str, apelido: str = "", nif: str = "", email: str = f" Código: {result['client_id']}" ) except ValueError as e: - return f"⚠️ Dados inválidos: {e}" + return f"⚠️ Invalid data: {e}" except Exception as e: _logger.error(f"cadastrar_cliente failed: {e}", exc_info=True) - return f"❌ Erro ao cadastrar cliente: {e}" + return f"❌ Error creating client: {e}" @mcp.tool() def ler_ficha_cliente(cliente: str) -> str: """ - Reads the client's INFO file (the Single Source of Truth / Centro de Verdade). - - The INFO-*.md file contains project context, technical decisions, meeting notes, - and all relevant metadata. This is the FIRST file you should read before performing - any operation on a client — it provides the context needed for document generation, - financial operations, and decision-making. - - Args: - cliente: Client folder name (exact or partial match). Example: 'ADRIELLE' or 'MP Incorporadora' + Reads the 'Center of Truth' (INFO-*.md) for a client. + CONTEXT: This is the mandatory first step before generating documents. It provides project metadata, + technical decisions, and meeting notes needed to understand the client's current state. + RESOLUTION: Support fuzzy/partial client name matching. """ _logger.info(f"Tool called: ler_ficha_cliente(cliente={cliente})") try: config = _get_config() clients_dir = config.base_pasta_clientes - - # Resolve client folder (exact or partial match) client_path = _resolve_client_path(clients_dir, cliente, config) - # Find INFO file (pattern: *INFO*.md) info_files = list(client_path.glob("*INFO*.md")) if not info_files: return ( f"⚠️ No INFO file found for client '{client_path.name}'.\n" - f" Expected pattern: *INFO*.md in {client_path}\n" - f" Use 'atualizar_ficha_cliente' to create one." + f" Expected pattern: *INFO*.md in {client_path}" ) - # Read the most recent INFO file info_file = sorted(info_files, key=lambda f: f.stat().st_mtime, reverse=True)[0] content = info_file.read_text(encoding="utf-8") @@ -305,24 +277,16 @@ def ler_ficha_cliente(cliente: str) -> str: return f"❌ {e}" except Exception as e: _logger.error(f"ler_ficha_cliente failed: {e}", exc_info=True) - return f"❌ Erro ao ler ficha: {e}" + return f"❌ Error reading info: {e}" @mcp.tool() def atualizar_ficha_cliente(cliente: str, secao: str, conteudo: str) -> str: """ - Updates a section of the client's INFO-*.md file (the Single Source of Truth). - - SECURITY: This tool creates a backup (.bak) before modifying the file. - It appends content to the specified section or creates a new section if it doesn't exist. - - Supported sections: 'Contexto do Projeto', 'Decisões Técnicas', 'Notas de Reunião', - or any custom ## heading. - - Args: - cliente: Client folder name - secao: Section heading to update (e.g., 'Notas de Reunião') - conteudo: Content to append to the section (markdown formatted) + Appends information to a specific section of the client's Center of Truth. + PROTOCOL: Use this to record meeting notes or technical decisions. + SAFETY: Automatically creates a .bak backup before modifying. + Sections: Use Markdown headers (e.g., 'Notas de Reunião'). """ _logger.info(f"Tool called: atualizar_ficha_cliente(cliente={cliente}, secao={secao})") try: @@ -335,31 +299,23 @@ def atualizar_ficha_cliente(cliente: str, secao: str, conteudo: str) -> str: info_file = sorted(info_files, key=lambda f: f.stat().st_mtime, reverse=True)[0] - # SECURITY: Backup before modifying import shutil backup = info_file.with_suffix('.md.bak') shutil.copy2(info_file, backup) - _logger.info(f"Backup created: {backup}") content = info_file.read_text(encoding="utf-8") - # Find section and append section_header = f"## {secao}" if section_header in content: - # Append after the section header (before next ## or end of file) parts = content.split(section_header, 1) after_header = parts[1] - - # Find next section next_section_idx = after_header.find("\n## ") if next_section_idx == -1: - # Append at end new_content = content + f"\n{conteudo}\n" else: insert_point = len(parts[0]) + len(section_header) + next_section_idx new_content = content[:insert_point] + f"\n{conteudo}\n" + content[insert_point:] else: - # Create new section at end new_content = content.rstrip() + f"\n\n{section_header}\n{conteudo}\n" info_file.write_text(new_content, encoding="utf-8") @@ -373,20 +329,15 @@ def atualizar_ficha_cliente(cliente: str, secao: str, conteudo: str) -> str: return f"❌ {e}" except Exception as e: _logger.error(f"atualizar_ficha_cliente failed: {e}", exc_info=True) - return f"❌ Erro ao atualizar ficha: {e}" + return f"❌ Error updating info: {e}" @mcp.tool() def listar_servicos_cliente(cliente: str) -> str: """ - Lists the services (sub-projects) under a specific client. - - In FOTON, each client can have multiple services (e.g., 'ELISEU', 'EZEQUIAS' under 'ADRIELLE'). - Each service contains its own ARQ/ (architecture files) and DOC/ (documents) folders. - Ignores system folders like DOC, ARQ, HID, ELE, STR, PL, EVT at the client root. - - Args: - cliente: Client folder name (exact or partial match) + Lists sub-projects/services within a client's main folder. + CONTEXT: Each service represents a distinct project (e.g., 'Reforma Apto 502'). + Ignores system folders like '01_ADMINISTRATIVO'. """ _logger.info(f"Tool called: listar_servicos_cliente(cliente={cliente})") try: @@ -414,7 +365,7 @@ def listar_servicos_cliente(cliente: str) -> str: return f"❌ {e}" except Exception as e: _logger.error(f"listar_servicos_cliente failed: {e}", exc_info=True) - return f"❌ Erro ao listar serviços: {e}" + return f"❌ Error listing services: {e}" # ============================================================================== @@ -424,14 +375,9 @@ def listar_servicos_cliente(cliente: str) -> str: @mcp.tool() def registrar_financeiro(cliente: str, descricao: str, valor: float, tipo: str = "ENTRADA") -> str: """ - Records a financial entry (income or expense) in the client's FINANCEIRO.csv ledger. - This is an Audited Standard Operation (POP) — each entry is timestamped and logged. - - Args: - cliente: Client folder name - descricao: Description of the transaction (e.g., 'Entrada Projeto', 'Taxa RRT') - valor: Monetary value (always positive, the 'tipo' determines debit/credit) - tipo: 'ENTRADA' for income or 'SAIDA' for expense (default: ENTRADA) + Records a financial entry (income/expense) in the client's ledger. + TYPES: 'ENTRADA' (credit) or 'SAIDA' (debit). + Value: Always pass a positive float. """ _logger.info(f"Tool called: registrar_financeiro(cliente={cliente}, valor={valor})") try: @@ -444,28 +390,15 @@ def registrar_financeiro(cliente: str, descricao: str, valor: float, tipo: str = type=tipo ) return f"✅ {result['message']} (POP Auditado)" - except ImportError: - try: - service = _get_factory().get_finance_service() - result = service.register_entry(cliente, descricao, valor, tipo) - if result.success: - return f"💵 {result.message} (Saldo: R$ {result.balance:.2f})" - return f"❌ {result.message}" - except Exception as fallback_e: - return f"❌ Erro: {fallback_e}" except Exception as e: _logger.error(f"registrar_financeiro failed: {e}", exc_info=True) - return f"❌ Erro POP: {e}" + return f"❌ Error: {e}" @mcp.tool() def consultar_financeiro(cliente: str) -> str: """ - Returns the financial summary for a specific client: total income, total expenses, - and current balance. Reads from the client's FINANCEIRO.csv ledger. - - Args: - cliente: Client folder name + Returns the financial balance and transaction summary for a specific client. """ _logger.info(f"Tool called: consultar_financeiro(cliente={cliente})") try: @@ -482,17 +415,14 @@ def consultar_financeiro(cliente: str) -> str: return f"❌ {result.message}" except Exception as e: _logger.error(f"consultar_financeiro failed: {e}", exc_info=True) - return f"❌ Erro: {e}" + return f"❌ Error: {e}" @mcp.tool() def resumo_financeiro_geral() -> str: """ - Returns a financial dashboard of ALL clients: total income, expenses, and balance - for each client that has a FINANCEIRO.csv file. Also shows the firm-wide totals. - - Use this for business intelligence and to quickly identify which clients have - outstanding balances or need follow-up. + Firm-wide financial dashboard. + CONTEXT: Use this for high-level business intelligence to identify profitable clients or cash-flow issues. """ _logger.info("Tool called: resumo_financeiro_geral") try: @@ -536,9 +466,9 @@ def resumo_financeiro_geral() -> str: results.append((d.name, income, expense, balance)) if not results: - return "📭 No financial data found across clients." + return "📭 No financial data found." - output = f"📊 Dashboard Financeiro ({len(results)} clientes com dados):\n" + output = f"📊 Dashboard Financeiro ({len(results)} clientes):\n" output += "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" for name, inc, exp, bal in results: emoji = "🟢" if bal >= 0 else "🔴" @@ -548,12 +478,12 @@ def resumo_financeiro_geral() -> str: total_balance = total_income - total_expense output += ( f" TOTAL: R$ {total_balance:,.2f} " - f"(Receita: R$ {total_income:,.2f} | Despesa: R$ {total_expense:,.2f})" + f"(Rec: R$ {total_income:,.2f} | Desp: R$ {total_expense:,.2f})" ) return output except Exception as e: _logger.error(f"resumo_financeiro_geral failed: {e}", exc_info=True) - return f"❌ Erro: {e}" + return f"❌ Error: {e}" # ============================================================================== @@ -563,12 +493,8 @@ def resumo_financeiro_geral() -> str: @mcp.tool() def listar_templates() -> str: """ - Lists all available document templates (DOCX and PPTX) from the firm's KIT DOC folder. - - Templates follow the naming convention: NN-COD_DOC_TIPO_VER_REV_DESCRICAO.ext - Examples: Proposals, Contracts, Briefings, Receipts, Delivery Terms, Portfolios. - - Use this to show the user which templates are available before generating a document. + Lists all available document templates (DOCX for contracts, PPTX for proposals). + PROTOCOL: Show this to the user to let them choose the document type they want to generate. """ _logger.info("Tool called: listar_templates") try: @@ -597,23 +523,14 @@ def listar_templates() -> str: return output except Exception as e: _logger.error(f"listar_templates failed: {e}", exc_info=True) - return f"❌ Erro: {e}" + return f"❌ Error: {e}" @mcp.tool() def listar_documentos_cliente(cliente: str, servico: str = "") -> str: """ - Lists all files belonging to a client, optionally filtered by a specific service. - Groups files by subfolder (ARQ, DOC, etc.) and shows file type, size, and modification date. - - Use this to: - - Check what documents already exist before generating new ones - - Find specific files the user is looking for - - Audit the completeness of a client's project folder - - Args: - cliente: Client folder name - servico: Optional service subfolder name to filter (e.g., 'ELISEU') + Lists existing files for a client or specific service. + CONTEXT: Use this to check if a document was already generated before creating a duplicate. """ _logger.info(f"Tool called: listar_documentos_cliente(cliente={cliente}, servico={servico})") try: @@ -640,11 +557,10 @@ def listar_documentos_cliente(cliente: str, servico: str = "") -> str: files_by_folder[folder].append(f"{rel.name} ({size_kb:.0f} KB)") if not files_by_folder: - return f"📭 No files found in {target.name}." + return f"📭 No files found." total = sum(len(v) for v in files_by_folder.values()) - scope = f"{client_path.name}/{servico}" if servico else client_path.name - output = f"📂 {total} arquivo(s) em {scope}:\n" + output = f"📂 {total} arquivo(s) em {target.name}:\n" for folder, files in sorted(files_by_folder.items()): output += f"\n 📁 {folder}/\n" @@ -656,25 +572,18 @@ def listar_documentos_cliente(cliente: str, servico: str = "") -> str: return f"❌ {e}" except Exception as e: _logger.error(f"listar_documentos_cliente failed: {e}", exc_info=True) - return f"❌ Erro: {e}" + return f"❌ Error: {e}" @mcp.tool() def gerar_documento(cliente: str, nome_template: str, dados_extras: dict = {}) -> str: """ - Generates a document for a client by merging a template with client data (Audited POP). - - The pipeline: INFO-*.md (context) + Template (DOCX/PPTX) + Extra Data = Final Document. - The generated file is saved in the client's folder with prefix 'GERADO_'. - - IMPORTANT: Before calling this, use 'validar_template' to check for missing variables, - and 'listar_documentos_cliente' to verify no duplicate already exists. - Use 'pipeline_emitir_documento' for the full safe workflow. - - Args: - cliente: Client folder name - nome_template: Template filename (e.g., '02-COD_DOC_CT_00_R00_PROPOSTA-PROJETO.docx') - dados_extras: Optional dict of additional variables to inject into the template + Merging Engine: Template + Client Data = Generated Document. + PROTOCOL: + 1. Always run 'validar_template' first. + 2. Provide 'dados_extras' for variables not found in the INFO files. + 3. The file is saved with prefix 'GERADO_' in the client's folder. + CASE-INSENSITIVITY: Variables are matched regardless of casing (@CLIENTE == @cliente). """ _logger.info(f"Tool called: gerar_documento(cliente={cliente}, template={nome_template})") try: @@ -685,9 +594,10 @@ def gerar_documento(cliente: str, nome_template: str, dados_extras: dict = {}) - template_name=nome_template, extra_data=dados_extras ) - return f"✅ Documento Gerado (POP Auditado): {result['output_path']}" - except ImportError as e: - return f"❌ Módulo não encontrado: {e}" + return ( + f"✅ Documento Gerado (POP Auditado)\n" + f" Arquivo: {result['output_path']}" + ) except Exception as e: _logger.error(f"gerar_documento failed: {e}", exc_info=True) return f"❌ Erro POP: {e}" @@ -696,36 +606,24 @@ def gerar_documento(cliente: str, nome_template: str, dados_extras: dict = {}) - @mcp.tool() def validar_template(cliente: str, nome_template: str, arquivo_dados: str = "") -> str: """ - Pre-flight validation: checks whether all template variables (e.g., {{NOME_CLIENTE}}) - are satisfied by the client's data files before generating the document. - - Returns: - - ✅ if all variables are present - - ⚠️ with a list of MISSING variables that need to be provided - - This is a NON-DESTRUCTIVE read-only operation. Always run this before 'gerar_documento'. - - Args: - cliente: Client folder name - nome_template: Template filename to validate against - arquivo_dados: Optional specific data file to use (default: auto-detects *.md files) + Pre-flight validation: Checks if the INFO files provide all variables required by the template. + Returns: A list of MISSING variables. + PROTOCOL: Mandatory check before calling 'gerar_documento'. + AGNOSTICISM: Searches the entire folder hierarchy for information. """ _logger.info(f"Tool called: validar_template(cliente={cliente}, template={nome_template})") try: config = _get_config() factory = _get_factory() - resolver = factory._get_path_resolver() - client_path = resolver.resolve(cliente) + svc = factory.get_client_service() + client_path = svc.resolve_client_path(cliente) template_path = config.templates_path / nome_template if not template_path.exists(): return f"❌ Template not found: {nome_template}" doc_type = template_path.suffix.lstrip('.').lower() - if doc_type not in ('docx', 'pptx'): - return f"❌ Unsupported template type: {doc_type}" - if arquivo_dados: data_path = client_path / arquivo_dados else: @@ -734,54 +632,30 @@ def validar_template(cliente: str, nome_template: str, arquivo_dados: str = "") return f"⚠️ No data files (.md) found in {client_path.name}" data_path = md_files[0] - if not data_path.exists(): - return f"❌ Data file not found: {data_path.name}" - - from foton_system.modules.documents.application.use_cases.document_service import DocumentService - from foton_system.modules.documents.infrastructure.adapters.python_docx_adapter import PythonDocxAdapter - from foton_system.modules.documents.infrastructure.adapters.python_pptx_adapter import PythonPPTXAdapter - - doc_service = DocumentService(PythonDocxAdapter(), PythonPPTXAdapter()) + from foton_system.interfaces.mcp.mcp_services import MCPServiceFactory + doc_service = MCPServiceFactory.get_instance().get_document_service() missing = doc_service.validate_template_keys(str(template_path), str(data_path), doc_type) if not missing: - return ( - f"✅ Pre-flight OK! Template '{nome_template}' has all variables satisfied.\n" - f" Data source: {data_path.name}\n" - f" Ready to generate with 'gerar_documento'." - ) + return f"✅ Pre-flight OK! Template '{nome_template}' has all variables satisfied." - output = ( - f"⚠️ Pre-flight: {len(missing)} variable(s) MISSING in template '{nome_template}':\n" - ) + output = f"⚠️ Pre-flight: {len(missing)} variable(s) MISSING:\n" for key in missing: output += f" ❌ {key}\n" - output += f"\n Data source: {data_path.name}" - output += "\n Provide these values via 'dados_extras' in 'gerar_documento', or update the INFO file." return output - - except ValueError as e: - return f"❌ {e}" except Exception as e: - _logger.error(f"validar_template failed: {e}", exc_info=True) - return f"❌ Erro na validação: {e}" + return f"❌ Validation error: {e}" # ============================================================================== -# KNOWLEDGE / RAG TOOLS (Available in dev mode only — deps not bundled in EXE) +# KNOWLEDGE / RAG TOOLS # ============================================================================== @mcp.tool() def consultar_conhecimento(pergunta: str) -> str: """ - Searches the firm's knowledge base (past projects, documents) using semantic search (RAG). - Use this to find context, past decisions, or reference materials. - - NOTE: This tool requires chromadb and sentence-transformers, which may not be available - in the compiled EXE version. - - Args: - pergunta: Natural language question to search for + Semantic search (RAG) across past projects and reference materials. + CONTEXT: Use this to find 'How did we solve X for client Y before?' or 'What are the rules for Z?'. """ _logger.info(f"Tool called: consultar_conhecimento(pergunta='{pergunta[:50]}...')") try: @@ -790,15 +664,13 @@ def consultar_conhecimento(pergunta: str) -> str: result = op.execute(query=pergunta) if result["status"] == "EMPTY": - return "📭 No relevant knowledge found in the database." + return "📭 No relevant knowledge found." output = [] for i, r in enumerate(result["results"], 1): output.append(f"--- [{i}] Source: {r['source']} (Similarity: {r['score']:.0%}) ---\n{r['document']}\n") return "\n".join(output) - except ImportError: - return "⚠️ RAG unavailable: missing dependencies (chromadb, sentence-transformers)." except Exception as e: return f"❌ Knowledge query error: {e}" @@ -806,341 +678,184 @@ def consultar_conhecimento(pergunta: str) -> str: @mcp.tool() def indexar_conhecimento(pasta_alvo: str = "") -> str: """ - Indexes documents (.md, .txt) into the firm's knowledge base for semantic search. - If no target path is given, indexes the entire client directory. - - NOTE: This tool requires chromadb and sentence-transformers. - - Args: - pasta_alvo: Optional path to index (default: entire client base) + Updates the semantic database by indexing documents. + PROTOCOL: Run this after adding many new files or manually updating INFO files to ensure RAG stays current. """ _logger.info(f"Tool called: indexar_conhecimento(alvo={pasta_alvo})") try: from foton_system.core.ops.op_index_knowledge import OpIndexKnowledge op = OpIndexKnowledge(actor="Agent_MCP") - - kwargs = {} - if pasta_alvo.strip(): - kwargs["target_path"] = pasta_alvo - + kwargs = {"target_path": pasta_alvo} if pasta_alvo.strip() else {} result = op.execute(**kwargs) - return ( - f"✅ Knowledge base updated!\n" - f" Files scanned: {result['files_scanned']}\n" - f" Chunks created: {result['chunks_created']}\n" - f" Target: {result['target']}" - ) - except ImportError: - return "⚠️ RAG unavailable: missing dependencies (chromadb, sentence-transformers)." + return f"✅ Knowledge base updated! Files: {result['files_scanned']}, Chunks: {result['chunks_created']}" except Exception as e: return f"❌ Indexing error: {e}" # ============================================================================== -# SYNC / MAINTENANCE TOOLS +# MAINTENANCE TOOLS # ============================================================================== @mcp.tool() def sincronizar_base() -> str: """ - Synchronizes the master dashboard database (baseDados.xlsx) by scanning all client - folders and their INFO-*.md files. This updates the Excel with current client data. - - Use this periodically or after bulk changes to client folders to keep the database - in sync with the filesystem (the Single Source of Truth). + Syncs the Excel Master Dashboard with the filesystem. """ _logger.info("Tool called: sincronizar_base") try: from foton_system.modules.sync.sync_service import SyncService svc = SyncService() result = svc.sync_dashboard() - - if result is not None: - return ( - f"✅ Dashboard synchronized!\n" - f" Records updated: {len(result)}\n" - f" Database: {_get_config().base_dados}" - ) - return "⚠️ No clients found to synchronize." + return f"✅ Dashboard synchronized! Records: {len(result)}" if result else "⚠️ No clients found." except Exception as e: - _logger.error(f"sincronizar_base failed: {e}", exc_info=True) return f"❌ Sync error: {e}" @mcp.tool() def sincronizar_clientes() -> str: """ - Discovers new clients and services from the filesystem and registers them in the - Excel databases (baseClientes.xlsx, baseServicos.xlsx). - - This performs TWO sync operations: - 1. Scans client folders → adds new ones to the clients database - 2. Scans service subfolders → adds new ones to the services database - - Use this after manually creating client folders or receiving new project folders - via OneDrive sync. + Discovers new client/service folders and adds them to the Excel database. """ _logger.info("Tool called: sincronizar_clientes") try: from foton_system.modules.clients.application.use_cases.client_service import ClientService from foton_system.modules.clients.infrastructure.repositories.excel_client_repository import ExcelClientRepository - repo = ExcelClientRepository() svc = ClientService(repo) - svc.sync_clients_db_from_folders() svc.sync_services_db_from_folders() - - return ( - "✅ Client & service databases synchronized!\n" - " New clients and services discovered from folders have been registered." - ) + return "✅ Client & service databases synchronized!" except Exception as e: - _logger.error(f"sincronizar_clientes failed: {e}", exc_info=True) return f"❌ Client sync error: {e}" +# ============================================================================== +# PIPELINES (AI RECOMMENDED FLOWS) +# ============================================================================== + @mcp.tool() -def exportar_fichas() -> str: +def configurar_agente() -> str: """ - Exports client data from the Excel database to INFO-*.md files in each client folder. - Direction: Database → Files (generates/updates the Centros de Verdade). - - Uses versioned filenames (VER_REV) to avoid overwriting existing data. - Only creates new files when data has changed. - - Use this after updating client data in the database to push changes to the filesystem. + Automates the formal installation of the Foton AI Skill into the Gemini CLI. + Copies the SKILL.md from the repository to the local .gemini/skills folder. + AI RECOMMENDED: Run this to enable specialized architectural reasoning from the repository source. """ - _logger.info("Tool called: exportar_fichas") + _logger.info("Tool called: configurar_agente") try: - from foton_system.modules.clients.application.use_cases.client_service import ClientService - from foton_system.modules.clients.infrastructure.repositories.excel_client_repository import ExcelClientRepository - - repo = ExcelClientRepository() - svc = ClientService(repo) - svc.export_client_data() - - return "✅ Client data exported to INFO-*.md files (versioned, no overwrites)." + config = _get_config() + # Source is in the repository + # Assume foton_system is inside the repo root + repo_root = Path(__file__).resolve().parents[3] + repo_skill_file = repo_root / "skills" / "foton-architecture" / "SKILL.md" + + if not repo_skill_file.exists(): + return f"❌ Erro: Arquivo de origem não encontrado no repositório: {repo_skill_file}" + + # Destination is the official workspace skill path + workspace_root = config.base_pasta_clientes.parent + skill_dir = workspace_root / ".gemini" / "skills" / "foton-architecture" + skill_dir.mkdir(parents=True, exist_ok=True) + + target_skill_file = skill_dir / "SKILL.md" + + # Copy content from repo to local installation + content = repo_skill_file.read_text(encoding="utf-8") + target_skill_file.write_text(content, encoding="utf-8") + + return ( + f"✅ Foton Skill instalada a partir do repositório!\n" + f" Origem: {repo_skill_file}\n" + f" Destino: {target_skill_file}\n" + f" ⚠️ IMPORTANTE: Execute o comando '/skills reload' no chat para ativar a expertise." + ) except Exception as e: - _logger.error(f"exportar_fichas failed: {e}", exc_info=True) - return f"❌ Export error: {e}" + _logger.error(f"configurar_agente failed: {e}", exc_info=True) + return f"❌ Erro ao configurar skill: {e}" + -# ============================================================================== -# INTELLIGENT PIPELINES -# ============================================================================== @mcp.tool() def pipeline_novo_cliente(nome: str, apelido: str = "", nif: str = "", email: str = "", telefone: str = "") -> str: + """ - SAFE PIPELINE for creating a new client. Performs checks before acting: - - Step 1: Searches existing clients for duplicates (partial name match) - Step 2: If duplicate found → returns warning with existing client info - Step 3: If no duplicate → creates the client via 'cadastrar_cliente' - Step 4: Reads back the created INFO file to confirm success - - This is the RECOMMENDED way to create clients — it prevents duplicates and - verifies the result. - - Args: - nome: Full client name (required) - apelido: Short alias/code (optional) - nif: Tax ID (optional) - email: Contact email (optional) - telefone: Contact phone (optional) + SAFE workflow to create a client while checking for duplicates. + AI RECOMMENDED: Always prefer this over 'cadastrar_cliente'. """ _logger.info(f"Tool called: pipeline_novo_cliente(nome={nome})") try: - config = _get_config() - clients_dir = config.base_pasta_clientes - ignored = set(config.ignored_folders + ['.obsidian']) - - # Step 1: Check for duplicates - search_term = nome.lower().replace(' ', '').replace('_', '') - matches = [] - if clients_dir.exists(): - for d in clients_dir.iterdir(): - if d.is_dir() and d.name not in ignored: - folder_norm = d.name.lower().replace(' ', '').replace('_', '') - if search_term in folder_norm or folder_norm in search_term: - matches.append(d.name) - - # Step 2: Duplicate warning - if matches: - output = ( - f"⚠️ PIPELINE PARADO — Possível duplicata encontrada!\n" - f" Nome solicitado: '{nome}'\n" - f" Cliente(s) similar(es):\n" - ) - for m in matches: - output += f" • {m}\n" - output += ( - f"\n Se deseja criar mesmo assim, use 'cadastrar_cliente' diretamente.\n" - f" Ou use 'ler_ficha_cliente' para verificar o cliente existente." - ) - return output + factory = _get_factory() + svc = factory.get_client_service() + + try: + exists = svc.resolve_client_path(nome) + return f"⚠️ PIPELINE STOPPED — A similar client already exists: {exists.name}. Use 'ler_ficha_cliente' to verify." + except ValueError: + pass - # Step 3: Create result = cadastrar_cliente(nome, apelido, nif, email, telefone) - - # Step 4: Verify - if "✅" in result: - ficha = ler_ficha_cliente(apelido if apelido else nome) - return f"{result}\n\n--- Verificação ---\n{ficha}" - return result except Exception as e: - _logger.error(f"pipeline_novo_cliente failed: {e}", exc_info=True) return f"❌ Pipeline error: {e}" @mcp.tool() def pipeline_emitir_documento(cliente: str, nome_template: str, dados_extras: dict = {}) -> str: """ - SAFE PIPELINE for document generation. Performs a full pre-flight check WITHOUT - generating any files. Returns a detailed report for the user to review. - - Step 1: VALIDATE — checks template variables vs. client data - Step 2: CHECK DUPLICATES — searches for existing generated documents - Step 3: REPORT — returns a pre-flight summary - - The pipeline NEVER generates the document automatically. After reviewing the report, - the user must explicitly approve, and then you should call 'gerar_documento'. - - Args: - cliente: Client folder name - nome_template: Template filename to use - dados_extras: Optional extra variables to inject + SAFE pre-flight report before document generation. + AI RECOMMENDED: Always run this before 'gerar_documento' to provide a summary to the user. + Logic: Validates variables AND checks for existing generated files to avoid duplicates. """ _logger.info(f"Tool called: pipeline_emitir_documento(cliente={cliente}, template={nome_template})") try: - config = _get_config() - client_path = _resolve_client_path(config.base_pasta_clientes, cliente, config) + svc = _get_factory().get_client_service() + client_path = svc.resolve_client_path(cliente) - output = f"📋 PRÉ-VOO — Emissão de Documento\n" - output += f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" - output += f" Cliente: {client_path.name}\n" + output = f"📋 PRE-FLIGHT — Document Generation\n" + output += f" Client: {client_path.name}\n" output += f" Template: {nome_template}\n\n" - # Step 1: Validate template validation = validar_template(cliente, nome_template) - if "✅" in validation: - output += "✅ VARIÁVEIS: Todas presentes\n" - elif "⚠️" in validation: - output += f"⚠️ VARIÁVEIS: {validation}\n" - else: - output += f"❌ VALIDAÇÃO: {validation}\n" - output += "\n🛑 Pipeline interrompido — corrija os problemas acima antes de prosseguir." - return output + output += f"{validation}\n" - # Step 2: Check for duplicates template_base = Path(nome_template).stem existing = list(client_path.rglob(f"GERADO_*{template_base}*")) - if existing: - output += f"\n⚠️ DUPLICATAS: {len(existing)} documento(s) similar(es) já existe(m):\n" - for ex in existing: - size_kb = ex.stat().st_size / 1024 - output += f" 📄 {ex.name} ({size_kb:.0f} KB)\n" + output += f"\n⚠️ DUPLICATES: {len(existing)} similar file(s) found in {client_path.name}.\n" else: - output += "✅ DUPLICATAS: Nenhum documento similar encontrado\n" - - # Step 3: Summary - output += "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" - if "⚠️ VARIÁVEIS" in output: - output += "⏸️ AÇÃO NECESSÁRIA: Forneça as variáveis faltantes antes de gerar.\n" - elif existing: - output += "⚠️ ATENÇÃO: Documento similar já existe. Confirme se deseja gerar novo.\n" - output += " Para gerar, chame: gerar_documento()\n" - else: - output += "✅ PRONTO: Todos os checks passaram. Confirme com o usuário para gerar.\n" - output += " Para gerar, chame: gerar_documento()\n" + output += "✅ DUPLICATES: No similar files found.\n" + output += "\nPROTOCOL: Review this report and confirm with the user before calling 'gerar_documento'." return output - except ValueError as e: - return f"❌ {e}" except Exception as e: - _logger.error(f"pipeline_emitir_documento failed: {e}", exc_info=True) return f"❌ Pipeline error: {e}" # ============================================================================== -# HELPER: Client Path Resolution +# HELPER: Internal Client Path Resolution # ============================================================================== def _resolve_client_path(clients_dir: Path, cliente: str, config) -> Path: """ - Resolves a client name to a validated directory path. - Supports exact match and partial/fuzzy matching. - Raises ValueError if not found or ambiguous. + Internal proxy to ClientService. """ - ignored = set(config.ignored_folders + ['.obsidian']) - - if not clients_dir.exists(): - raise ValueError(f"Client directory not found: {clients_dir}") - - # 1. Exact match - exact = clients_dir / cliente - if exact.exists() and exact.is_dir(): - return exact - - # 2. Case-insensitive / partial match - search = cliente.lower() - matches = [] - for d in clients_dir.iterdir(): - if d.is_dir() and d.name not in ignored: - if search in d.name.lower(): - matches.append(d) - - if len(matches) == 1: - return matches[0] - elif len(matches) > 1: - names = [m.name for m in matches] - raise ValueError( - f"Ambiguous client name '{cliente}'. Found {len(matches)} matches: {', '.join(names)}. " - f"Please be more specific." - ) - else: - raise ValueError( - f"Client '{cliente}' not found. Use 'listar_clientes' to see available clients." - ) + svc = _get_factory().get_client_service() + return svc.resolve_client_path(cliente) # ============================================================================== -# SERVER RUN (called by main.py --mcp OR by __main__) +# SERVER RUN # ============================================================================== def run_server(): - """ - Start the MCP stdio server with heartbeat monitoring. - Called by main.py (EXE --mcp) or directly via python -m. - """ - import threading - import os - - def _heartbeat(): - while True: - time.sleep(60) - _logger.debug(f"💓 Heartbeat: Process alive (pid={os.getpid()})") - - t = threading.Thread(target=_heartbeat, daemon=True) - t.start() - _logger.info("Starting MCP stdio loop...") sys.stderr.write("[MCP] Foton server ready.\n") sys.stderr.flush() - try: mcp.run() except Exception as e: _logger.critical(f"MCP loop crashed: {e}", exc_info=True) - sys.stderr.write(f"[MCP] Critical Error: {e}\n") sys.exit(1) - finally: - _logger.info("MCP loop exited.") - if __name__ == "__main__": run_server() - diff --git a/foton_system/interfaces/webview_bridge.py b/foton_system/interfaces/webview_bridge.py new file mode 100644 index 0000000..0e51b13 --- /dev/null +++ b/foton_system/interfaces/webview_bridge.py @@ -0,0 +1,100 @@ +""" +WebViewBridge - Ponte entre Python e Interface HTML (FotonInfoInterface). + +Utiliza pywebview para abrir a interface de preenchimento de dados de forma nativa. +Permite ler arquivos MD do projeto e salvar o resultado diretamente no sistema. +""" + +import json +import os +import sys +import webbrowser +from pathlib import Path +from typing import Optional + +try: + import webview + WEBVIEW_AVAILABLE = True +except ImportError: + WEBVIEW_AVAILABLE = False + +class WebViewBridge: + def __init__(self, initial_md_content: str = "", save_callback=None): + self.initial_md_content = initial_md_content + self.save_callback = save_callback + self.window = None + + def get_initial_content(self): + """Retorna o conteúdo MD inicial para o JS.""" + return self.initial_md_content + + def save_markdown(self, content: str): + """Chamado pelo JS para salvar o arquivo final.""" + if self.save_callback: + success = self.save_callback(content) + if success: + return {"status": "success", "message": "Arquivo salvo com sucesso!"} + return {"status": "error", "message": "Falha ao salvar arquivo no backend."} + return {"status": "info", "message": "Simulação: Arquivo recebido pelo backend."} + + def close(self): + """Fecha a janela.""" + if self.window: + self.window.destroy() + +def open_info_interface(content: str = "", save_fn=None): + """Abre a interface WebView ou fallback para Browser.""" + if not WEBVIEW_AVAILABLE: + print("\n⚠️ Módulo 'webview' não disponível no ambiente atual.") + print("💡 Tentando abrir a interface no seu navegador padrão...") + _open_in_browser(content) + return + + api = WebViewBridge(content, save_fn) + + # Localizar o arquivo HTML + html_path = Path(__file__).resolve().parent / "fotonInfoInterface.html" + + if not html_path.exists(): + from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + html_path = PathManager.get_app_dir() / "assets" / "fotonInfoInterface.html" + + if not html_path.exists(): + print(f"❌ Erro: Interface HTML não encontrada. Tentando modo browser...") + _open_in_browser(content) + return + + try: + window = webview.create_window( + 'Foton System - Preenchedor de Templates', + str(html_path), + js_api=api, + width=1000, + height=800, + resizable=True + ) + api.window = window + webview.start() + except Exception as e: + print(f"⚠️ Falha ao iniciar janela nativa: {e}") + print("💡 Abrindo fallback no navegador...") + _open_in_browser(content) + +def _open_in_browser(content: str): + """Fallback: Abre o HTML no navegador padrão.""" + html_path = Path(__file__).resolve().parent / "fotonInfoInterface.html" + if not html_path.exists(): + from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + html_path = PathManager.get_app_dir() / "assets" / "fotonInfoInterface.html" + + if html_path.exists(): + webbrowser.open(f"file:///{html_path.resolve()}") + print("✅ Interface aberta no navegador.") + print("📝 Nota: No modo navegador, você deve copiar o resultado final manualmente.") + else: + print("❌ Erro crítico: Arquivo HTML da interface não encontrado em lugar nenhum.") + +if __name__ == "__main__": + # Teste isolado + example_md = "@nomeCliente; Fulano de Tal\n@valorProposta; [calculo: 1000*2]" + open_info_interface(example_md) diff --git a/foton_system/main.py b/foton_system/main.py index ec11164..01e9918 100644 --- a/foton_system/main.py +++ b/foton_system/main.py @@ -30,6 +30,13 @@ def _start_mcp(): # Ultra-Safe Entry Point def safety_entry(): """Provides immediate visual feedback and robust error handling.""" + + # ── SANDBOX MODE: Global Activation ── + if "--sandbox" in sys.argv: + _ensure_path() + from foton_system.modules.shared.infrastructure.services.sandbox_service import SandboxService + SandboxService.initialize_sandbox() + # ── MCP MODE: Must be checked FIRST — zero stdout before mcp.run() ── if "--mcp" in sys.argv: _start_mcp() diff --git a/foton_system/modules/clients/application/use_cases/client_service.py b/foton_system/modules/clients/application/use_cases/client_service.py index 340c311..79b1670 100644 --- a/foton_system/modules/clients/application/use_cases/client_service.py +++ b/foton_system/modules/clients/application/use_cases/client_service.py @@ -1,10 +1,13 @@ import pandas as pd import re +from pathlib import Path from typing import Optional + from foton_system.modules.shared.infrastructure.config.config import Config from foton_system.modules.shared.infrastructure.config.logger import setup_logger from foton_system.modules.clients.application.ports.client_repository_port import ClientRepositoryPort from foton_system.modules.shared.infrastructure.validators import validate_filename +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager from foton_system.modules.shared.domain.exceptions import ( InvalidAliasError, DatabaseLockError, @@ -26,7 +29,72 @@ def __init__(self, repository: ClientRepositoryPort, config: Optional[Config] = self.repository = repository self._config = config or Config() + def _get_template_sections(self): + """Loads and splits the unified template into Client and Service parts.""" + template_path = PathManager.get_info_template_path() + client_part = "" + service_part = "" + + if not template_path.exists(): + return self.CLIENT_TEMPLATE_STR, self.SERVICE_TEMPLATE_STR + + try: + with open(template_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Split based on headers + parts = re.split(r'##\s*INFO-SERVICO\.md', content, flags=re.IGNORECASE) + client_part = parts[0] + if len(parts) > 1: + service_part = "## INFO-SERVICO.md" + parts[1] + else: + service_part = self.SERVICE_TEMPLATE_STR # Fallback if section missing + + return client_part, service_part + except Exception as e: + logger.error(f"Erro ao carregar template DNA: {e}") + return self.CLIENT_TEMPLATE_STR, self.SERVICE_TEMPLATE_STR + + def resolve_client_path(self, client_name: str) -> Path: + """ + Resolves a client name to a validated directory path. + Supports exact match and partial/fuzzy matching. + Raises ValueError if not found or ambiguous. + """ + clients_dir = self._config.base_pasta_clientes + ignored = set(self._config.ignored_folders + ['.obsidian']) + + if not clients_dir.exists(): + raise ValueError(f"Diretório de clientes não encontrado: {clients_dir}") + + # 1. Exact match + exact = clients_dir / client_name + if exact.exists() and exact.is_dir(): + return exact + + # 2. Case-insensitive / partial match + search = client_name.lower() + matches = [] + for d in clients_dir.iterdir(): + if d.is_dir() and d.name not in ignored: + if search in d.name.lower(): + matches.append(d) + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + names = [m.name for m in matches] + raise ValueError( + f"Nome de cliente ambíguo '{client_name}'. Encontrados {len(matches)} correspondências: {', '.join(names)}. " + f"Por favor, seja mais específico." + ) + else: + raise ValueError( + f"Cliente '{client_name}' não encontrado. Use 'listar_clientes' para ver os clientes disponíveis." + ) + def sync_clients_db_from_folders(self): + """Updates DB with clients found in folders but not in DB.""" logger.info("Sincronizando base de clientes a partir das pastas...") try: @@ -273,14 +341,20 @@ def sort_key(f): return files[0] def _read_file_content(self, path): - """Reads key-value pairs from MD file.""" + """ + Reads key-value pairs from MD file. + Supports preferred semicolon (;) and legacy colon (:). + """ data = {} if not path.exists(): return data with open(path, 'r', encoding='utf-8') as f: for line in f: - if ':' in line: + if ';' in line: + key, value = line.split(';', 1) + data[key.strip()] = value.strip() + elif ':' in line: key, value = line.split(':', 1) data[key.strip()] = value.strip() return data @@ -294,88 +368,88 @@ def _read_file_content(self, path): Dados que serão utilizados nas propostas comerciais: -@dataProposta: -@numeroProposta: -@nomeProposta: -@cidadeProposta: -@localProposta: -@geolocalizacaoProposta: -@nomeCliente: -@empregoCliente: -@estadoCivilCliente: -@cpfCnpjCliente: -@enderecoCliente: +@dataProposta; +@numeroProposta; +@nomeProposta; +@cidadeProposta; +@localProposta; +@geolocalizacaoProposta; +@nomeCliente; +@empregoCliente; +@estadoCivilCliente; +@cpfCnpjCliente; +@enderecoCliente; """ SERVICE_TEMPLATE_STR = """## INFO-SERVICO.md -@TEMPLATE: +@TEMPLATE; ### DADOS BÁSICOS -@DataAtual: +@DataAtual; ### DADOS DO CLIENTE - CONTRATO O cliente pode precisar utilizar dados distintos no contrato, portanto abaixo tem os dados para a contratação do serviço: -@nomeContrato: -@numeroContrato: -@nomeClienteContrato: -@estadoCivilClienteContrato: -@empregoClienteContrato: -@telefoneClienteContrato: -@emailClienteContrato: -@enderecoClienteContrato: -@cpfCnpjClienteContrato: +@nomeContrato; +@numeroContrato; +@nomeClienteContrato; +@estadoCivilClienteContrato; +@empregoClienteContrato; +@telefoneClienteContrato; +@emailClienteContrato; +@enderecoClienteContrato; +@cpfCnpjClienteContrato; ### DADOS DO SERVIÇO -@modalidadeServico: -@anoProjeto: -@demandaProposta: -@areaTotal: -@areaCoberta: -@areaDescoberta: -@detalhesProposta: -@estiloProjeto: -@ambientesProjeto: -@inProposta: -@lvProposta: -@anProposta: -@baProposta: -@prProposta: -@inSolucao: -@valorProposta: -@valorContrato: +@modalidadeServico; +@anoProjeto; +@demandaProposta; +@areaTotal; +@areaCoberta; +@areaDescoberta; +@detalhesProposta; +@estiloProjeto; +@ambientesProjeto; +@inProposta; +@lvProposta; +@anProposta; +@baProposta; +@prProposta; +@inSolucao; +@valorProposta; +@valorContrato; #### DADOS PARA ESTIMATIVA DE CUSTO - PROPOSTA -@projArqEng: -@procLegais: -@ACEqv: -@execcub: -@execInfra: -@execPais: -@execMob: -@totalParcial: -@totalExec: -@totalinss: -@totalGeral: -@ArqEng%: -@Legais%: -@precoCUB%: -@Parcial%: -@infra%: -@pais%: -@mob%: -@Exec%: -@inss%: +@projArqEng; +@procLegais; +@ACEqv; +@execcub; +@execInfra; +@execPais; +@execMob; +@totalParcial; +@totalExec; +@totalinss; +@totalGeral; +@ArqEng%; +@Legais%; +@precoCUB%; +@Parcial%; +@infra%; +@pais%; +@mob%; +@Exec%; +@inss%; """ def _write_formatted_file_content(self, path, data, template_str): """ - Writes data to file using the template structure. + Writes data to file using the template structure with semicolon separator. Preserves existing values if not in data (for updates). """ lines = template_str.split('\n') @@ -386,13 +460,16 @@ def _write_formatted_file_content(self, path, data, template_str): for line in lines: stripped = line.strip() - if stripped.startswith('@') and ':' in stripped: - key = stripped.split(':')[0].strip() + # Handle both possible separators during template processing + sep = ';' if ';' in stripped else (':' if ':' in stripped else None) + + if stripped.startswith('@') and sep: + key = stripped.split(sep)[0].strip() written_keys.add(key) # Value priority: Data (DB) > Existing File > Empty value = data.get(key, "") - output_lines.append(f"{key}: {value}") + output_lines.append(f"{key}; {value}") else: output_lines.append(line) @@ -401,7 +478,7 @@ def _write_formatted_file_content(self, path, data, template_str): if extra_keys: output_lines.append("\n### VARIÁVEIS EXTRAS") for key in extra_keys: - output_lines.append(f"{key}: {data[key]}") + output_lines.append(f"{key}; {data[key]}") with open(path, 'w', encoding='utf-8') as f: f.write('\n'.join(output_lines)) @@ -411,6 +488,7 @@ def export_client_data(self): logger.info("Exporting client data to files...") count = 0 try: + client_template, _ = self._get_template_sections() df = self.repository.get_clients_dataframe() latest_df = df.groupby('Alias').last().reset_index() @@ -459,7 +537,7 @@ def export_client_data(self): if should_create: filename = self._generate_filename(cod, alias, ver, rev) - self._write_formatted_file_content(folder / filename, file_data, self.CLIENT_TEMPLATE_STR) + self._write_formatted_file_content(folder / filename, file_data, client_template) count += 1 logger.info(f"{count} arquivos de cliente exportados/atualizados.") @@ -472,6 +550,7 @@ def export_service_data(self): logger.info("Exporting service data to files...") count = 0 try: + _, service_template = self._get_template_sections() df = self.repository.get_services_dataframe() latest_df = df.groupby(['AliasCliente', 'Alias']).last().reset_index() @@ -517,7 +596,7 @@ def export_service_data(self): if should_create: filename = self._generate_filename(cod, service_alias, ver, rev) - self._write_formatted_file_content(folder / filename, file_data, self.SERVICE_TEMPLATE_STR) + self._write_formatted_file_content(folder / filename, file_data, service_template) count += 1 logger.info(f"{count} arquivos de serviço exportados/atualizados.") diff --git a/foton_system/modules/documents/application/use_cases/document_service.py b/foton_system/modules/documents/application/use_cases/document_service.py index 0293b3f..16065ec 100644 --- a/foton_system/modules/documents/application/use_cases/document_service.py +++ b/foton_system/modules/documents/application/use_cases/document_service.py @@ -33,7 +33,7 @@ def __init__(self, docx_adapter: DocumentServicePort, pptx_adapter: DocumentServ def list_templates(self, extension): templates_dir = self._config.templates_path - if not templates_dir.exists(): + if not templates_dir or not templates_dir.exists(): logger.warning(f"Diretório de templates não encontrado: {templates_dir}") return [] @@ -41,7 +41,7 @@ def list_templates(self, extension): def list_data_files(self): data_dir = self._config.templates_path - if not data_dir.exists(): + if not data_dir or not data_dir.exists(): return [] files = list(data_dir.glob('*.txt')) + list(data_dir.glob('*.json')) @@ -54,6 +54,7 @@ def list_client_data_files(self, client_path): return list(client_path.glob('*.md')) + list(client_path.glob('*.txt')) def create_custom_data_file(self, client_path, cod, ver='00', rev='R00', desc='PROPOSTA'): + from foton_system.modules.shared.infrastructure.services.path_manager import PathManager client_path = Path(client_path) if not client_path.exists(): return None @@ -65,13 +66,20 @@ def create_custom_data_file(self, client_path, cod, ver='00', rev='R00', desc='P logger.warning(f"Arquivo já existe: {filename}") return data_file - content = """@TEMPLATE: nome do arquivo template a ser utilizado -# DADOS ESPECÍFICOS DO DOCUMENTO -@DataAtual: -@numeroProposta: -@detalhesProposta: -@valorProposta: -""" + # DNA: Tenta carregar do template centralizado + template_path = PathManager.get_info_template_path() + if template_path.exists(): + try: + with open(template_path, 'r', encoding='utf-8') as f: + content = f.read() + logger.info(f"Usando template centralizado: {template_path.name}") + except Exception as e: + logger.error(f"Erro ao ler template: {e}") + content = "# ERRO AO CARREGAR TEMPLATE" + else: + # Fallback seguro caso o arquivo de assets suma + content = "@TEMPLATE: nome do arquivo template a ser utilizado\n# DADOS\n@DataAtual:\n" + try: with open(data_file, 'w', encoding='utf-8') as f: f.write(content) @@ -81,6 +89,45 @@ def create_custom_data_file(self, client_path, cod, ver='00', rev='R00', desc='P logger.error(f"Erro ao criar arquivo de dados: {e}") return None + def _load_data(self, data_path): + """ + Loads data from the specific file (JSON, TXT, or MD) and normalizes keys to lowercase. + """ + data_path = Path(data_path) + if not data_path.exists(): + return {} + + raw_data = {} + suffix = data_path.suffix.lower() + + try: + if suffix == '.json': + with open(data_path, 'r', encoding='utf-8') as f: + raw_data = json.load(f) + elif suffix == '.txt': + raw_data = self._parse_txt_data(data_path) + elif suffix == '.md' or suffix == '': + raw_data = self._parse_md_data(data_path) + except Exception as e: + logger.error(f"Erro ao carregar arquivo de dados {data_path}: {e}") + + # Lowercase keys for case-insensitive matching + return {str(k).lower(): v for k, v in raw_data.items()} + + def _parse_txt_data(self, path): + replacements = {} + try: + with open(path, 'r', encoding='utf-8') as f: + for line in f: + if ';' in line: + parts = line.strip().split(';') + if len(parts) >= 2: + key, value = parts[0], parts[1] + replacements[key.lower()] = value + except Exception as e: + logger.error(f"Erro ao parsear TXT {path}: {e}") + return replacements + def generate_document(self, template_path, data_path, output_path, doc_type): logger.info(f"Gerando documento do tipo {doc_type}...") @@ -143,35 +190,12 @@ def _get_system_variables(self): def _apply_formatting(self, replacements): """ - Iterates over all keys and applies formatting rules. - Also creates derived keys if helpful. + Iterates over all keys and applies smart formatting rules. + PURE DATA POLICY: No automatic 'R$' prefix or 'm²' suffix. """ - formatted_replacements = {} - for key, value in replacements.items(): - # Apply currency formatting if it looks like money - # Heuristic: Key contains 'valor', 'custo', 'total', 'preco' OR Value looks like a float - is_money_key = any(x in key.lower() for x in ['valor', 'custo', 'total', 'preco', 'cub', 'exec']) - - # Try to interpret as number - try: - # If it's already a string with 'R$', try to parse it back to float first to normalize - clean_val = FotonFormatter.parse_br_number(value) - - if is_money_key: - # Update the MAIN key with formatted currency (User Preference) - replacements[key] = FotonFormatter.format_currency(clean_val) - elif isinstance(clean_val, float) and clean_val != 0.0: - # It's a number but not necessarily money (e.g., Area). - # Let's format as decimal (1.000,00) but keep original key flexible? - # For safety, let's just ensure consistent decimal formatting for areas - if 'area' in key.lower() or 'aceqv' in key.lower(): - replacements[key] = FotonFormatter.format_decimal(clean_val) - except: - pass - - # We modify 'replacements' in place or update it - # The logic above updates 'replacements' directly for specific keys + # Apply smart formatting: literals in quotes remain raw, numbers get BR decimal format, % get percentage format + replacements[key] = FotonFormatter.smart_format(key, value) def _load_context_data(self, data_path): data = {} @@ -187,34 +211,65 @@ def _load_context_data(self, data_path): dirs_to_check.reverse() for folder in dirs_to_check: - alias = folder.name - info_file = self._get_latest_info_file(folder, alias) - if info_file: - logger.info(f"Carregando contexto de: {info_file.name}") + # Find any *INFO*.md file in the folder, regardless of folder name + info_files = list(folder.glob("*INFO*.md")) + if info_files: + # Sort by modification time to get the most recent if multiple exist + info_files.sort(key=lambda f: f.stat().st_mtime, reverse=True) + info_file = info_files[0] + logger.info(f"Carregando contexto de: {info_file.name} em {folder.name}") folder_data = self._parse_md_data(info_file) - data.update(folder_data) + # Lowercase keys for case-insensitive matching + normalized_folder_data = {k.lower(): v for k, v in folder_data.items()} + data.update(normalized_folder_data) except Exception as e: logger.warning(f"Erro ao carregar dados de contexto: {e}") return data + def _parse_md_data(self, file_path): + """ + Parses metadata from an MD file. + Format: @Variable; Value + """ + data = {} + try: + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line.startswith('@'): + if ';' in line: + parts = line.split(';', 1) + key = parts[0].strip() + value = parts[1].strip() + data[key] = value + elif ':' in line: # Fallback for older files + parts = line.split(':', 1) + key = parts[0].strip() + value = parts[1].strip() + data[key] = value + except Exception as e: + logger.error(f"Erro ao parsear {file_path}: {e}") + return data + def _get_latest_info_file(self, folder, alias): - if not folder.exists(): - return None - files = list(folder.glob(f"*_INFO-{alias}.md")) - if not files: # Fallback to standard INFO-{alias}.md - files = list(folder.glob(f"INFO-{alias}.md")) - + # This method is now deprecated by the new glob logic in _load_context_data + # but kept for potential backward compatibility if called elsewhere. + files = list(folder.glob("*INFO*.md")) if not files: return None - files.sort(key=lambda f: f.name, reverse=True) + files.sort(key=lambda f: f.stat().st_mtime, reverse=True) return files[0] def validate_template_keys(self, template_path, data_path, doc_type): + """ + Public method to validate template keys against context and local data. + Ensures everything is case-insensitive by lowercasing keys. + """ context_data = self._load_context_data(Path(data_path)) - doc_data = self._load_data(data_path) + doc_data = self._load_data(Path(data_path)) replacements = {**context_data, **doc_data} return self._validate_keys(template_path, replacements, doc_type) @@ -253,7 +308,8 @@ def _validate_keys(self, template_path, replacements, doc_type): logger.warning(f"Não foi possível validar as chaves do template: {e}") return [] - missing_keys = [k for k in required_keys if k not in replacements] + # All keys are normalized to lowercase for comparison + missing_keys = [k for k in required_keys if k.lower() not in replacements] if missing_keys: logger.warning(f"CHAVES FALTANDO: {missing_keys}") @@ -274,42 +330,11 @@ def _log_generation(self, output_path, doc_type, template_path, data_path): logger.error(f"Erro ao gravar log de geração: {e}") def _extract_keys_from_text(self, text, keys_set): + """Extracts keys from text and normalizes them to lowercase for consistent validation.""" if text and '@' in text: found = re.findall(r'(?= 2: - key, value = parts[0], parts[1] - replacements[key] = value - return replacements - - def _parse_md_data(self, path): - replacements = {} - with open(path, 'r', encoding='utf-8') as f: - for line in f: - if ':' in line: - key, value = line.split(':', 1) - replacements[key.strip()] = value.strip() - return replacements + for k in found: + keys_set.add(k.lower()) def _resolve_operations(self, replacements): """ @@ -317,17 +342,23 @@ def _resolve_operations(self, replacements): Improvement: Handles Brazilian number formats during calculation. """ for _ in range(3): - for key, value in replacements.items(): + # Normalize keys for lookup + current_keys = list(replacements.keys()) + for key in current_keys: + value = replacements[key] if isinstance(value, str) and '[calculo:' in value: match = re.search(r'\[calculo:\s*(.+?)\]', value) if match: expression = match.group(1) - for k, v in replacements.items(): - if k in expression and k != key: + # Sort keys by length to avoid partial matches during calculation replacement + for k in sorted(current_keys, key=len, reverse=True): + v = replacements[k] + if k.lower() in expression.lower() and k != key: try: # Normalize to float for calculation float_val = FotonFormatter.parse_br_number(v) - expression = expression.replace(k, str(float_val)) + # Case-insensitive replacement of variable in expression + expression = re.sub(re.escape(k), str(float_val), expression, flags=re.IGNORECASE) except: pass try: @@ -335,17 +366,7 @@ def _resolve_operations(self, replacements): raise ValueError("Expressão contém caracteres inválidos") result = eval(expression) - # Store result as clean float string first to allow further calcs - # Or format immediately? - # Decision: Store as formatted string because recursive calculations - # inside _resolve_operations will parse it back via parse_br_number - - # However, to be safe, let's keep it simple. - # The Formatter in step 6 will handle the final look. - # But wait, if step 6 sees "5000.00", it formats. - # If we store "R$ 5.000,00" here, step 6 sees string. - - # Let's return a string representation of the float + # Store with .2f precision for financial consistency replacements[key] = f"{result:.2f}" except Exception as e: logger.warning(f"Falha ao calcular {key}: {e}") diff --git a/foton_system/modules/documents/application/use_cases/tui_form_filler_use_case.py b/foton_system/modules/documents/application/use_cases/tui_form_filler_use_case.py new file mode 100644 index 0000000..59fe48e --- /dev/null +++ b/foton_system/modules/documents/application/use_cases/tui_form_filler_use_case.py @@ -0,0 +1,71 @@ +""" +TUI Form Filler Use Case - Orquestra o fluxo de preenchimento TUI de alta performance. +""" + +import shutil +from pathlib import Path +from colorama import Fore, Style +from foton_system.modules.documents.domain.models.form_session import FormSession +from foton_system.interfaces.cli.views.form_view import TUIFormView + +class TUIFormFillerUseCase: + def __init__(self, file_path: Path): + self.file_path = file_path + self.session = FormSession() + + def execute(self) -> bool: + """Executa o processo de preenchimento interativo.""" + if not self.file_path.exists(): + return False + + # 1. Carregar e Parsear (Instantâneo) + try: + with open(self.file_path, "r", encoding="utf-8") as f: + content = f.read() + self.session.parse_markdown(content) + except Exception as e: + print(f"❌ Erro ao ler arquivo: {e}") + return False + + # 2. Iniciar View (Loop de Interface Terminal) + view = TUIFormView(self.session, title=f"Ficha: {self.file_path.name}") + action = view.run_loop() + + # 3. Processar Ação Final + if action == "save" or action == "save_as": + target_path = self.file_path + + if action == "save_as": + from datetime import datetime + suffix = datetime.now().strftime("%Y%m%d_%H%M") + default_name = f"{self.file_path.stem}_{suffix}.md" + + print(f"\n{Fore.CYAN}--- SALVAR COMO ---{Style.RESET_ALL}") + new_name = input(f"Digite o novo nome (Vazio para {default_name}): ").strip() + if not new_name: + new_name = default_name + if not new_name.endswith(".md"): + new_name += ".md" + + target_path = self.file_path.parent / new_name + else: + # Criar backup antes de sobrescrever + try: + bak_path = self.file_path.with_suffix(self.file_path.suffix + ".bak") + shutil.copy2(self.file_path, bak_path) + except: pass + + try: + # Gerar e salvar novo MD + new_md = self.session.generate_markdown() + with open(target_path, "w", encoding="utf-8") as f: + f.write(new_md) + + if action == "save_as": + print(f"\n✅ {Fore.GREEN}Nova versão criada: {target_path.name}{Style.RESET_ALL}") + return True + except Exception as e: + print(f"❌ Erro ao salvar arquivo: {e}") + return False + + return False diff --git a/foton_system/modules/documents/domain/models/form_session.py b/foton_system/modules/documents/domain/models/form_session.py new file mode 100644 index 0000000..ad15cb0 --- /dev/null +++ b/foton_system/modules/documents/domain/models/form_session.py @@ -0,0 +1,131 @@ +""" +FormSession Domain Model - Gerencia o estado e lógica do formulário MD. +""" + +import re +from dataclasses import dataclass +from typing import List, Dict, Optional, Any + +@dataclass +class FormField: + name: str + description: str + original_value: str = "" # Valor exato que veio do MD + current_value: str = "" # Valor atual (pode ser editado) + is_calculated: bool = False + formula: str = "" + is_dirty: bool = False + hint: str = "" + +class FormSession: + def __init__(self): + self.fields: List[FormField] = [] + self.cursor: int = 0 + self.structure: List[Dict[str, Any]] = [] + self.var_pattern = re.compile(r'^@([\w%]+);\s*(.*)$') + self.calc_pattern = re.compile(r'^\[calculo:\s*(.*?)\]\s*(.*)$') + self.hint_pattern = re.compile(r'(?:por exemplo|exemplo)\s*:?\s*(.+)$', re.IGNORECASE) + + def parse_markdown(self, md_text: str): + self.fields = [] + self.structure = [] + lines = md_text.splitlines() + + for line in lines: + stripped = line.strip() + match = self.var_pattern.match(stripped) + if match: + var_name = match.group(1) + full_content = match.group(2).strip() + + if self.calc_pattern.match(full_content): + calc_match = self.calc_pattern.match(full_content) + f = FormField( + name=var_name, + description=calc_match.group(2).strip(), + is_calculated=True, + formula=calc_match.group(1).strip(), + original_value=full_content + ) + else: + hint = "" + hint_match = self.hint_pattern.search(full_content) + clean_val = full_content + if hint_match: + hint = hint_match.group(0).strip() + clean_val = self.hint_pattern.sub('', full_content).strip().rstrip(',').rstrip(';') + + f = FormField( + name=var_name, + description=clean_val, + current_value=clean_val, + original_value=full_content, + hint=hint + ) + + self.fields.append(f) + self.structure.append({"type": "variable", "name": var_name}) + else: + self.structure.append({"type": "text", "content": line}) + + self.cursor = 0 + self.recalculate_all() + + def update_current(self, value: str): + if not value.strip(): return + f = self.get_current_field() + if f and not f.is_calculated: + f.current_value = value + f.is_dirty = True + self.recalculate_all() + + def get_current_field(self) -> Optional[FormField]: + return self.fields[self.cursor] if self.fields else None + + def next(self): + if self.cursor < len(self.fields) - 1: self.cursor += 1 + + def prev(self): + if self.cursor > 0: self.cursor -= 1 + + def recalculate_all(self): + var_map = {f.name: f.current_value for f in self.fields} + for f in self.fields: + if f.is_calculated: + res = self._evaluate(f.formula, var_map) + f.current_value = f"{res:.2f}" + if f.name.endswith('%'): f.current_value = f"{res*100:.2f}%" + var_map[f.name] = f.current_value + + def generate_markdown(self) -> str: + field_dict = {f.name: f for f in self.fields} + output = [] + for item in self.structure: + if item["type"] == "text": + output.append(item["content"]) + else: + f = field_dict[item["name"]] + val = f"@{f.name};" + if f.is_calculated: + val += f"[calculo: {f.formula}] {f.description}" + else: + if f.is_dirty: + val += f.current_value + if f.hint: val += f" {f.hint}" + else: + val += f.original_value + output.append(val) + return "\n".join(output) + + def _evaluate(self, expr: str, var_map: Dict[str, str]) -> float: + try: + safe_expr = expr + sorted_vars = sorted(var_map.keys(), key=len, reverse=True) + for var in sorted_vars: + raw_val = var_map[var].replace('%', '').replace(',', '.') + try: val = float(raw_val) if raw_val.strip() else 0.0 + except: val = 0.0 + safe_expr = safe_expr.replace(f"@{var}", str(val)) + safe_expr = re.sub(r'[^0-9+\-*/().\s]', '', safe_expr) + return float(eval(safe_expr, {"__builtins__": {}}, {})) if safe_expr.strip() else 0.0 + except: return 0.0 diff --git a/foton_system/modules/documents/infrastructure/adapters/python_docx_adapter.py b/foton_system/modules/documents/infrastructure/adapters/python_docx_adapter.py index a3c83cb..95edd45 100644 --- a/foton_system/modules/documents/infrastructure/adapters/python_docx_adapter.py +++ b/foton_system/modules/documents/infrastructure/adapters/python_docx_adapter.py @@ -102,11 +102,11 @@ def _replace_keys_in_text(self, text, replacements): sorted_keys = sorted(replacements.keys(), key=len, reverse=True) for key in sorted_keys: - if key in text: - # Use regex to ensure we don't replace inside words/emails - pattern = r'(? 1: + paragraph.runs[0].text = text + for run in paragraph.runs[1:]: + run.text = "" + def _replace_in_table(self, table, replacements): for row in table.rows: for cell in row.cells: @@ -55,10 +63,10 @@ def _replace_keys_in_text(self, text, replacements): sorted_keys = sorted(replacements.keys(), key=len, reverse=True) for key in sorted_keys: - if key in text: - # Use regex to ensure we don't replace inside words/emails - pattern = r'(? bool: + """Abre o formulário e retorna True se carregado com sucesso.""" + pass diff --git a/foton_system/modules/shared/application/ports/system_integrator_port.py b/foton_system/modules/shared/application/ports/system_integrator_port.py new file mode 100644 index 0000000..47974dc --- /dev/null +++ b/foton_system/modules/shared/application/ports/system_integrator_port.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod +from pathlib import Path + +class SystemIntegratorPort(ABC): + """ + Porta para integrações específicas de Sistema Operacional. + """ + + @abstractmethod + def create_shortcut(self, target_path: Path, app_name: str, description: str = "") -> bool: + """Cria atalhos no desktop/menu iniciar.""" + pass + + @abstractmethod + def open_external(self, path: Path) -> bool: + """Abre um arquivo ou pasta no aplicativo padrão do SO.""" + pass diff --git a/foton_system/modules/shared/infrastructure/adapters/forms/browser_form_adapter.py b/foton_system/modules/shared/infrastructure/adapters/forms/browser_form_adapter.py new file mode 100644 index 0000000..eb610f6 --- /dev/null +++ b/foton_system/modules/shared/infrastructure/adapters/forms/browser_form_adapter.py @@ -0,0 +1,29 @@ +import webbrowser +import logging +from typing import Callable +from pathlib import Path +from foton_system.modules.shared.application.ports.form_interface_port import FormInterfacePort + +logger = logging.getLogger(__name__) + +class BrowserFormAdapter(FormInterfacePort): + def open_form(self, initial_content: str, save_callback: Callable[[str], bool]) -> bool: + """Fallback: Abre no navegador padrão.""" + try: + # Tenta localizar o HTML + from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + html_path = PathManager.get_assets_dir() / "fotonInfoInterface.html" + + if not html_path.exists(): + html_path = Path(__file__).resolve().parents[4] / "interfaces" / "fotonInfoInterface.html" + + if html_path.exists(): + webbrowser.open(f"file:///{html_path.resolve()}") + print("\n✅ Interface aberta no navegador.") + print("📝 Nota: No modo navegador, você deve copiar o resultado final manualmente e salvar no arquivo.") + return True + else: + return False + except Exception as e: + logger.error(f"Erro ao abrir browser: {e}") + return False diff --git a/foton_system/modules/shared/infrastructure/adapters/forms/tui_form_adapter.py b/foton_system/modules/shared/infrastructure/adapters/forms/tui_form_adapter.py new file mode 100644 index 0000000..0133260 --- /dev/null +++ b/foton_system/modules/shared/infrastructure/adapters/forms/tui_form_adapter.py @@ -0,0 +1,13 @@ +import logging +from typing import Callable +from foton_system.modules.shared.application.ports.form_interface_port import FormInterfacePort + +logger = logging.getLogger(__name__) + +class TuiFormAdapter(FormInterfacePort): + def open_form(self, initial_content: str, save_callback: Callable[[str], bool]) -> bool: + """Modo Terminal para preenchimento.""" + print("\n--- MODO TERMINAL (SERVER) ---") + print("Preenchimento interativo não disponível via TUI nesta versão.") + print("Por favor, edite o arquivo .md manualmente no servidor.") + return True diff --git a/foton_system/modules/shared/infrastructure/adapters/forms/webview_form_adapter.py b/foton_system/modules/shared/infrastructure/adapters/forms/webview_form_adapter.py new file mode 100644 index 0000000..a4dd2c6 --- /dev/null +++ b/foton_system/modules/shared/infrastructure/adapters/forms/webview_form_adapter.py @@ -0,0 +1,42 @@ +import logging +from typing import Callable +from pathlib import Path +from foton_system.modules.shared.application.ports.form_interface_port import FormInterfacePort + +logger = logging.getLogger(__name__) + +class WebViewFormAdapter(FormInterfacePort): + def open_form(self, initial_content: str, save_callback: Callable[[str], bool]) -> bool: + try: + import webview + from foton_system.interfaces.webview_bridge import WebViewBridge + + # Localizar o arquivo HTML + html_path = Path(__file__).resolve().parents[4] / "interfaces" / "fotonInfoInterface.html" + + if not html_path.exists(): + from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + html_path = PathManager.get_assets_dir() / "fotonInfoInterface.html" + + if not html_path.exists(): + return False + + api = WebViewBridge(initial_content, save_callback) + + window = webview.create_window( + 'Foton System - Preenchedor de Templates', + str(html_path), + js_api=api, + width=1000, + height=800, + resizable=True + ) + api.window = window + webview.start() + return True + except ImportError: + logger.warning("WebView não instalado. Fallback necessário.") + return False + except Exception as e: + logger.error(f"Falha ao iniciar WebView: {e}") + return False diff --git a/foton_system/modules/shared/infrastructure/adapters/system/linux_integrator.py b/foton_system/modules/shared/infrastructure/adapters/system/linux_integrator.py new file mode 100644 index 0000000..1f401da --- /dev/null +++ b/foton_system/modules/shared/infrastructure/adapters/system/linux_integrator.py @@ -0,0 +1,38 @@ +import os +import subprocess +from pathlib import Path +from foton_system.modules.shared.application.ports.system_integrator_port import SystemIntegratorPort + +class LinuxIntegrator(SystemIntegratorPort): + def create_shortcut(self, target_path: Path, app_name: str, description: str = "") -> bool: + """Cria um arquivo .desktop para integração com menus Linux.""" + desktop_file_content = f"""[Desktop Entry] +Type=Application +Name={app_name} +Comment={description} +Exec={target_path} +Icon={target_path.parent}/foton.svg +Terminal=true +Categories=Office;Development; +""" + try: + # Caminho padrão para aplicações do usuário + apps_dir = Path.home() / ".local" / "share" / "applications" + apps_dir.mkdir(parents=True, exist_ok=True) + + file_path = apps_dir / f"{app_name.lower().replace(' ', '_')}.desktop" + file_path.write_text(desktop_file_content, encoding="utf-8") + + # Tenta dar permissão de execução + file_path.chmod(0o755) + + return True + except: + return False + + def open_external(self, path: Path) -> bool: + try: + subprocess.run(['xdg-open', str(path)], check=True) + return True + except: + return False diff --git a/foton_system/modules/shared/infrastructure/adapters/system/null_integrator.py b/foton_system/modules/shared/infrastructure/adapters/system/null_integrator.py new file mode 100644 index 0000000..9dbf618 --- /dev/null +++ b/foton_system/modules/shared/infrastructure/adapters/system/null_integrator.py @@ -0,0 +1,12 @@ +from pathlib import Path +from foton_system.modules.shared.application.ports.system_integrator_port import SystemIntegratorPort + +class NullIntegrator(SystemIntegratorPort): + def create_shortcut(self, target_path: Path, app_name: str, description: str = "") -> bool: + # Silencioso em servidores + return True + + def open_external(self, path: Path) -> bool: + # Apenas loga ou ignora em servidores headless + print(f"INFO: Tentativa de abrir recurso: {path}") + return True diff --git a/foton_system/modules/shared/infrastructure/adapters/system/windows_integrator.py b/foton_system/modules/shared/infrastructure/adapters/system/windows_integrator.py new file mode 100644 index 0000000..aa4ac3b --- /dev/null +++ b/foton_system/modules/shared/infrastructure/adapters/system/windows_integrator.py @@ -0,0 +1,46 @@ +import os +import sys +from pathlib import Path +from foton_system.modules.shared.application.ports.system_integrator_port import SystemIntegratorPort + +class WindowsIntegrator(SystemIntegratorPort): + def create_shortcut(self, target_path: Path, app_name: str, description: str = "") -> bool: + try: + import winshell + from win32com.client import Dispatch + + target_path_str = str(target_path) + work_dir = str(target_path.parent) + + shell = Dispatch('WScript.Shell') + + # Atalho Desktop + desktop = winshell.desktop() + lnk_path = os.path.join(desktop, f"{app_name}.lnk") + shortcut = shell.CreateShortCut(lnk_path) + shortcut.Targetpath = target_path_str + shortcut.WorkingDirectory = work_dir + shortcut.IconLocation = target_path_str + shortcut.Description = description + shortcut.save() + + # Atalho Menu Iniciar + start_menu = winshell.programs() + lnk_path = os.path.join(start_menu, f"{app_name}.lnk") + shortcut = shell.CreateShortCut(lnk_path) + shortcut.Targetpath = target_path_str + shortcut.WorkingDirectory = work_dir + shortcut.IconLocation = target_path_str + shortcut.save() + + return True + except Exception as e: + # Em vez de logger global, poderíamos injetar logger ou apenas retornar False + return False + + def open_external(self, path: Path) -> bool: + try: + os.startfile(path) + return True + except: + return False diff --git a/foton_system/modules/shared/infrastructure/bootstrap/bootstrap_service.py b/foton_system/modules/shared/infrastructure/bootstrap/bootstrap_service.py index 2b3919a..03ec158 100644 --- a/foton_system/modules/shared/infrastructure/bootstrap/bootstrap_service.py +++ b/foton_system/modules/shared/infrastructure/bootstrap/bootstrap_service.py @@ -89,6 +89,7 @@ def _create_default_settings(path: Path): "caminho_pastaClientes": str(PathManager.get_user_projects_dir()), "caminho_templates": str(Path.home() / "Documents" / "FotonTemplates"), "caminho_baseDados": str(PathManager.get_app_data_dir() / "baseDados.xlsx"), + "caminho_template_info": "", "ignored_folders": ["DOC", "ARQ", "HID", "ELE", "STR", "PL", "EVT"], "clean_missing_variables": True, "missing_variable_placeholder": "---", diff --git a/foton_system/modules/shared/infrastructure/services/environment_porter.py b/foton_system/modules/shared/infrastructure/services/environment_porter.py new file mode 100644 index 0000000..f748c3f --- /dev/null +++ b/foton_system/modules/shared/infrastructure/services/environment_porter.py @@ -0,0 +1,209 @@ +""" +EnvironmentPorter: O "Porteiro" do sistema. +Identifica o ambiente de execução (SO, GUI, Docker, WSL, MCP) e define o perfil de uso e capacidades. +""" + +import os +import sys +import platform +import logging +import shutil +import subprocess +from enum import Enum +from pathlib import Path + +logger = logging.getLogger(__name__) + +class SystemProfile(Enum): + SERVER_HEADLESS = "SERVER_HEADLESS" # Sem interface gráfica, focado em CLI/MCP/Docker + DESKTOP_GUI = "DESKTOP_GUI" # Interface gráfica nativa completa + DESKTOP_WSL = "DESKTOP_WSL" # Windows Subsystem for Linux (pode ter GUI via GWSL/X11) + DESKTOP_TUI = "DESKTOP_TUI" # Desktop sem display ou preferência por terminal + MOCK = "MOCK" # Para testes unitários + +class EnvironmentPorter: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(EnvironmentPorter, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + self._detect_environment() + self._initialized = True + + def _detect_environment(self): + """Detecta SO, GUI, Docker, WSL e MCP.""" + self.os_type = platform.system().lower() # 'windows', 'linux', 'darwin' + self.is_frozen = getattr(sys, 'frozen', False) + + # 1. Detecção de Docker + self.is_docker = self._check_docker() + + # 2. Detecção de WSL + self.is_wsl = self._check_wsl() + + # 3. Detecção de GUI + self.has_gui = self._check_gui_availability() + + # 4. Detecção de MCP + self.is_mcp_mode = "--mcp" in sys.argv + + # 5. Definição do Perfil Principal + if self.is_docker: + self.profile = SystemProfile.SERVER_HEADLESS + elif self.is_wsl: + self.profile = SystemProfile.DESKTOP_WSL + elif not self.has_gui: + self.profile = SystemProfile.SERVER_HEADLESS + else: + self.profile = SystemProfile.DESKTOP_GUI + + # Nota: O perfil pode ser forçado via configuração (Futuro) + + logger.info(f"Ambiente detectado: OS={self.os_type}, Profile={self.profile.value}, GUI={self.has_gui}, Docker={self.is_docker}, WSL={self.is_wsl}") + + def _check_docker(self) -> bool: + """Verifica se está rodando dentro de um container Docker.""" + # Verificações padrão + if os.path.exists('/.dockerenv') or os.path.exists('/.dockerinit'): + return True + + # Verificação via cgroup + try: + with open('/proc/1/cgroup', 'rt') as f: + if 'docker' in f.read(): + return True + except: + pass + + # Variáveis de ambiente comuns + if os.environ.get('DOCKER_HOST') or os.environ.get('DOTNET_RUNNING_IN_CONTAINER'): + return True + + return False + + def _check_wsl(self) -> bool: + """Verifica se está rodando no WSL.""" + if self.os_type != 'linux': + return False + + try: + with open('/proc/version', 'r') as f: + version_info = f.read().lower() + return 'microsoft' in version_info or 'wsl' in version_info + except: + return False + + def _check_gui_availability(self) -> bool: + """Verifica se há um servidor gráfico funcional disponível.""" + if self.os_type == 'windows': + # Assume GUI no Windows (exceto se detectarmos explicitamente modo server sem shell) + return True + elif self.os_type == 'darwin': # macOS + return True + elif self.os_type == 'linux': + # Verifica variável de ambiente de display + display = os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY') + if not display: + return False + + # No Linux, verifica também o socket X11 (sugestão da auditoria) + # Geralmente /tmp/.X11-unix/X0, X1, etc. + x11_socket_dir = Path("/tmp/.X11-unix") + if x11_socket_dir.exists(): + sockets = list(x11_socket_dir.glob("X*")) + if sockets: + return True + + # Se tem DISPLAY mas não achou socket, pode ser um túnel SSH ou Wayland puro + return True + + return False + + def can_use_feature(self, feature_name: str) -> bool: + """ + Verifica se uma feature específica é suportada no ambiente atual. + Centraliza a lógica de 'Feature Toggles'. + """ + features = { + "webview": self.has_gui and not self.is_mcp_mode and self._check_webview_installed(), + "native_dialogs": self.has_gui and self._has_dialog_tools(), + "shortcuts": self.profile == SystemProfile.DESKTOP_GUI and self.os_type == "windows", + "watcher": True, + "rag": True, + "tui": sys.stdout.isatty() or self.is_mcp_mode + } + return features.get(feature_name, False) + + def _check_webview_installed(self) -> bool: + """Verifica se a biblioteca pywebview está disponível sem causar crash global.""" + try: + import importlib.util + return importlib.util.find_spec("webview") is not None + except: + return False + + def _has_dialog_tools(self) -> bool: + """Verifica se ferramentas de diálogo nativo (zenity, kdialog) estão presentes no Linux.""" + if self.os_type == 'windows' or self.os_type == 'darwin': + return True # Windows/Mac usam APIs nativas ou Tkinter + + # No Linux, verificamos utilitários comuns + return any(shutil.which(tool) for tool in ['zenity', 'kdialog', 'gxmessage']) + + def get_summary(self) -> str: + """Retorna um resumo amigável do ambiente para debug/logs.""" + caps = [] + if self.has_gui: caps.append("GUI") + if self.is_docker: caps.append("Docker") + if self.is_wsl: caps.append("WSL") + if self.is_mcp_mode: caps.append("MCP") + + cap_str = f" [{', '.join(caps)}]" if caps else "" + return f"FotonProfile: {self.profile.value}{cap_str} on {self.os_type.capitalize()}" + + def get_integrator(self): + """Retorna o adaptador de integração com o SO adequado.""" + if self.profile == SystemProfile.SERVER_HEADLESS: + from foton_system.modules.shared.infrastructure.adapters.system.null_integrator import NullIntegrator + return NullIntegrator() + + if self.os_type == 'windows': + from foton_system.modules.shared.infrastructure.adapters.system.windows_integrator import WindowsIntegrator + return WindowsIntegrator() + elif self.os_type == 'linux': + from foton_system.modules.shared.infrastructure.adapters.system.linux_integrator import LinuxIntegrator + return LinuxIntegrator() + + from foton_system.modules.shared.infrastructure.adapters.system.null_integrator import NullIntegrator + return NullIntegrator() + + def get_form_filler(self): + """Retorna o preenchedor de formulários adequado ao ambiente.""" + if self.profile == SystemProfile.SERVER_HEADLESS: + from foton_system.modules.shared.infrastructure.adapters.forms.tui_form_adapter import TuiFormAdapter + return TuiFormAdapter() + + if self.can_use_feature("webview"): + from foton_system.modules.shared.infrastructure.adapters.forms.webview_form_adapter import WebViewFormAdapter + return WebViewFormAdapter() + else: + from foton_system.modules.shared.infrastructure.adapters.forms.browser_form_adapter import BrowserFormAdapter + return BrowserFormAdapter() + +# Helper para uso simplificado +def get_porter() -> EnvironmentPorter: + return EnvironmentPorter() + +if __name__ == "__main__": + # Teste rápido + porter = EnvironmentPorter() + print(porter.get_summary()) + print(f"Pode usar WebView? {porter.can_use_feature('webview')}") + print(f"Pode usar Diálogos? {porter.can_use_feature('native_dialogs')}") diff --git a/foton_system/modules/shared/infrastructure/services/install_service.py b/foton_system/modules/shared/infrastructure/services/install_service.py index 8589189..d759809 100644 --- a/foton_system/modules/shared/infrastructure/services/install_service.py +++ b/foton_system/modules/shared/infrastructure/services/install_service.py @@ -1,26 +1,36 @@ import os import sys import shutil +import time from pathlib import Path from foton_system.modules.shared.infrastructure.bootstrap.bootstrap_service import BootstrapService from foton_system.modules.shared.infrastructure.config.logger import setup_logger +from foton_system.modules.shared.infrastructure.services.environment_porter import get_porter logger = setup_logger() class InstallService: def __init__(self): self.app_name = "FotonSystem" - self.install_dir = Path(os.environ.get('LOCALAPPDATA')) / self.app_name + self.porter = get_porter() + # Fallback para caminho de instalação se não estiver no Windows + if self.porter.os_type == 'windows': + self.install_dir = Path(os.environ.get('LOCALAPPDATA', '')) / self.app_name + else: + self.install_dir = Path.home() / ".local" / "share" / self.app_name.lower() + self.bin_dir = self.install_dir / "bin" def install(self): - """Realiza a instalação completa no LocalAppData.""" - print(f"🛠️ Instalando {self.app_name} em LocalAppData...") + """Realiza a instalação completa no sistema.""" + print(f"🛠️ Instalando {self.app_name} em {self.install_dir}...") # 1. Criar diretórios self.bin_dir.mkdir(parents=True, exist_ok=True) - # 2. Copiar arquivos da aplicação + # 2. Copiar arquivos da aplicação (Omitido para brevidade, lógica permanece igual) + # ... (Cópia do executável e _internal) ... + # (Vou manter o código de cópia para não quebrar a funcionalidade) exe_path = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0] exe_path = Path(exe_path).resolve() source_dir = exe_path.parent @@ -33,107 +43,54 @@ def install(self): else: print(f"📂 Preparando binários em: {self.bin_dir}") try: - # 2.1 Copiar o Executável if target_exe.exists(): try: - # Tentativa robusta: renomear o arquivo em uso - temp_old = target_exe.with_suffix(f".old_{int(time.time())}") + timestamp = int(time.time()) + temp_old = target_exe.with_suffix(f".old_{timestamp}") target_exe.rename(temp_old) - # Tenta deletar o arquivo renomeado (opcional) - try: temp_old.unlink() - except: pass except Exception as e: logger.debug(f"Não foi possível renomear exe antigo: {e}") shutil.copy2(exe_path, target_exe) print(f"✅ Executável copiado.") - # 2.2 Copiar Pasta _internal (Essencial para builds --onedir) source_internal = source_dir / "_internal" target_internal = self.bin_dir / "_internal" if source_internal.exists(): - print(f"📦 Atualizando dependências (_internal)... isso pode levar alguns segundos...") - + print(f"📦 Atualizando dependências (_internal)...") if target_internal.exists(): try: - # Tenta remover de forma limpa primeiro - shutil.rmtree(target_internal) - except Exception: - # Se falhar (Acesso Negado), renomeia a pasta antiga para sair do caminho - try: - timestamp = int(time.time()) - trash_internal = target_internal.parent / f"_internal_old_{timestamp}" - target_internal.rename(trash_internal) - logger.info(f"Pasta _internal bloqueada. Renomeada para {trash_internal.name}") - except Exception as rename_err: - logger.error(f"Falha crítica ao mover _internal antigo: {rename_err}") - # Se não conseguir nem renomear, tentaremos o copytree com override - pass + timestamp = int(time.time()) + trash_internal = target_internal.parent / f"_internal_old_{timestamp}" + target_internal.rename(trash_internal) + except: pass - # Copia a pasta inteira (dirs_exist_ok garante que podemos mesclar se necessário) shutil.copytree(source_internal, target_internal, dirs_exist_ok=True) print(f"✅ Dependências atualizadas.") except Exception as e: - logger.error(f"Erro ao copiar arquivos na instalação: {e}", exc_info=True) + logger.error(f"Erro ao copiar arquivos: {e}") print(f"❌ Erro ao instalar binários: {e}") - print("Dica: Verifique se não há outra instância do FotonSystem aberta.") return - # 3. Inicializar Configuração no AppData + # 3. Inicializar Configuração config_path = BootstrapService.initialize() print(f"✅ Configuração vinculada em: {config_path}") - # 4. Criar Atalhos apontando para o LocalAppData - if sys.platform == "win32": - self._create_windows_shortcuts(target_exe) - - print(f"\n🎉 {self.app_name} instalado com sucesso!") - print(f"Você já pode fechar esta janela e usar o atalho na Área de Trabalho.") + # 4. Criar Atalhos usando o Integrador (Agnóstico) + integrator = self.porter.get_integrator() + success = integrator.create_shortcut( + target_exe, + self.app_name, + "Sistema de Gestão para Arquitetos" + ) - print(f"\n{'-'*60}") - print(f"🤖 CONFIGURAÇÃO PARA AGENTES DE IA (MCP):") - print(f"Para usar o Foton com Gemini ou Claude, adicione ao seu arquivo de config:") - # Escape backslashes for JSON compatibility - safe_path = str(target_exe).replace("\\", "\\\\") - - print(f"\n\"foton\": {{") - print(f" \"command\": \"{safe_path}\",") - print(f" \"args\": [\"--mcp\"]") - print(f"}}\n{'-'*60}") - - def _create_windows_shortcuts(self, target_path: Path): - try: - import winshell - from win32com.client import Dispatch - - target_path_str = str(target_path) - work_dir = str(target_path.parent) + if success: + print(f"✅ Atalhos de sistema criados com sucesso!") + else: + print(f"⚠️ Não foi possível criar atalhos automáticos para este ambiente.") - shell = Dispatch('WScript.Shell') + print(f"\n🎉 {self.app_name} instalado com sucesso!") - # Atalho Desktop - desktop = winshell.desktop() - lnk_path = os.path.join(desktop, f"{self.app_name}.lnk") - shortcut = shell.CreateShortCut(lnk_path) - shortcut.Targetpath = target_path_str - shortcut.WorkingDirectory = work_dir - shortcut.IconLocation = target_path_str - shortcut.Description = "Sistema de Gestão para Arquitetos" - shortcut.save() - print("✅ Atalho criado na Área de Trabalho") - - # Atalho Menu Iniciar - start_menu = winshell.programs() - lnk_path = os.path.join(start_menu, f"{self.app_name}.lnk") - shortcut = shell.CreateShortCut(lnk_path) - shortcut.Targetpath = target_path_str - shortcut.WorkingDirectory = work_dir - shortcut.IconLocation = target_path_str - shortcut.save() - print("✅ Atalho criado no Menu Iniciar") - - except Exception as e: - logger.error(f"Erro ao criar atalhos: {e}", exc_info=True) - print(f"⚠️ Erro ao criar atalhos: {e}") + # Removido _create_windows_shortcuts pois agora está no adaptador correspondente. diff --git a/foton_system/modules/shared/infrastructure/services/path_manager.py b/foton_system/modules/shared/infrastructure/services/path_manager.py index 68b97c8..7b7964b 100644 --- a/foton_system/modules/shared/infrastructure/services/path_manager.py +++ b/foton_system/modules/shared/infrastructure/services/path_manager.py @@ -21,6 +21,34 @@ class PathManager: """ APP_NAME = "FotonSystem" + _sandbox_mode = False + _sandbox_dir = None + + @classmethod + def set_sandbox_mode(cls, enabled: bool): + """Activates or deactivates sandbox mode.""" + cls._sandbox_mode = enabled + if enabled and cls._sandbox_dir is None: + import tempfile + temp_dir = Path(tempfile.gettempdir()) + cls._sandbox_dir = temp_dir / "foton_sandbox" + cls._sandbox_dir.mkdir(parents=True, exist_ok=True) + elif not enabled: + cls._sandbox_dir = None + + @classmethod + def get_sandbox_dir(cls) -> Path: + """Returns the temporary sandbox directory.""" + if cls._sandbox_dir is None: + import tempfile + temp_dir = Path(tempfile.gettempdir()) + return temp_dir / "foton_sandbox" + return cls._sandbox_dir + + @classmethod + def is_sandbox_active(cls) -> bool: + """Checks if sandbox mode is active.""" + return cls._sandbox_mode # --- Core Path Getters --- @@ -31,12 +59,10 @@ def get_app_data_dir() -> Path: Windows: %LOCALAPPDATA%/FotonSystem Linux/Mac: ~/.fotonsystem - - This is where we store: - - settings.json - - foton_system.log - - Internal databases (if not user-configured) """ + if PathManager.is_sandbox_active(): + return PathManager.get_sandbox_dir() / "appdata" + home = Path.home() if system() == "Windows": return home / "AppData" / "Local" / PathManager.APP_NAME @@ -96,9 +122,10 @@ def get_user_projects_dir() -> Path: Windows: Documents/FotonProjects Linux/Mac: ~/FotonProjects - - This can be overridden by settings.json. """ + if PathManager.is_sandbox_active(): + return PathManager.get_sandbox_dir() / "projects" + if system() == "Windows": return Path.home() / "Documents" / "FotonProjects" else: @@ -116,6 +143,19 @@ def get_log_path() -> Path: """Returns the path to the log file.""" return PathManager.get_app_data_dir() / "foton_system.log" + @staticmethod + def get_info_template_path() -> Path: + """ + Returns the path to the master INFO template. + Checks for a custom template in settings, falls back to bundled asset. + """ + from foton_system.modules.shared.infrastructure.config.config import Config + custom_path = Config().get('caminho_template_info') + if custom_path and Path(custom_path).exists(): + return Path(custom_path) + + return PathManager.get_assets_dir() / "info-Template.md" + # --- Helper Methods --- @staticmethod diff --git a/foton_system/modules/shared/infrastructure/services/sandbox_service.py b/foton_system/modules/shared/infrastructure/services/sandbox_service.py new file mode 100644 index 0000000..856df37 --- /dev/null +++ b/foton_system/modules/shared/infrastructure/services/sandbox_service.py @@ -0,0 +1,91 @@ +""" +SandboxService: Manages the volatile testing environment. +""" + +import json +import shutil +from pathlib import Path +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager +from foton_system.modules.shared.infrastructure.config.logger import setup_logger + +logger = setup_logger() + +class SandboxService: + """ + Handles initialization, seeding, and teardown of the Sandbox mode. + """ + + @staticmethod + def initialize_sandbox(): + """ + Activates sandbox mode and prepares the environment. + """ + logger.info("Initializing Sandbox Mode...") + PathManager.set_sandbox_mode(True) + + # Ensure base directories exist in the sandbox + app_data_dir = PathManager.get_app_data_dir() + projects_dir = PathManager.get_user_projects_dir() + + app_data_dir.mkdir(parents=True, exist_ok=True) + projects_dir.mkdir(parents=True, exist_ok=True) + + # Create a sandbox-specific settings.json + SandboxService._create_sandbox_settings() + + # Seed with dummy data + SandboxService._seed_sandbox() + + logger.info(f"Sandbox initialized at {PathManager.get_sandbox_dir()}") + + @staticmethod + def _create_sandbox_settings(): + """Creates a minimal settings.json for the sandbox.""" + settings_path = PathManager.get_settings_path() + templates_dir = PathManager.get_sandbox_dir() / "templates" + templates_dir.mkdir(parents=True, exist_ok=True) + + settings = { + "caminho_pastaClientes": str(PathManager.get_user_projects_dir()), + "caminho_templates": str(templates_dir), + "caminho_baseDados": str(PathManager.get_app_data_dir() / "baseDados_sandbox.xlsx"), + "ignored_folders": ["DOC", "ARQ", "HID", "ELE", "STR", "PL", "EVT"], + "clean_missing_variables": True, + "missing_variable_placeholder": "[SANDBOX-MISSING]", + "enable_mcp": True + } + + with open(settings_path, 'w', encoding='utf-8') as f: + json.dump(settings, f, indent=4, ensure_ascii=False) + + @staticmethod + def _seed_sandbox(): + """Populates the sandbox with example data.""" + # 1. Create a dummy client + projects_dir = PathManager.get_user_projects_dir() + client_dir = projects_dir / "CLIENTE_EXEMPLO" + client_dir.mkdir(parents=True, exist_ok=True) + + # 2. Create standard folder structure for dummy client + (client_dir / "01_ADMINISTRATIVO").mkdir(exist_ok=True) + (client_dir / "02_FINANCEIRO").mkdir(exist_ok=True) + (client_dir / "03_PROJETOS").mkdir(exist_ok=True) + + # 3. Create a dummy INFO file + info_file = client_dir / "INFO-CLIENTE_EXEMPLO.md" + content = """# DADOS DO CLIENTE (MODO SANDBOX) +@Nome; Cliente de Teste Sandbox +@Email; teste@sandbox.com +@Cidade; Cidade Virtual +@DataAtual; 01 de Janeiro de 2026 +""" + info_file.write_text(content, encoding='utf-8') + + # 4. Create a dummy finance ledger + finance_file = client_dir / "FINANCEIRO.csv" + finance_file.write_text("Data,Descricao,Valor,Tipo\n2026-01-01,Saldo Inicial Sandbox,1000.00,ENTRADA", encoding='utf-8') + + # 5. Create a dummy template in the sandbox templates dir + templates_dir = PathManager.get_sandbox_dir() / "templates" + dummy_template = templates_dir / "01-MOD_DOC_PROPOSTA_V00_R00_TESTE.docx" + dummy_template.touch() # Just an empty file for listing tests diff --git a/foton_system/modules/shared/infrastructure/services/tip_service.py b/foton_system/modules/shared/infrastructure/services/tip_service.py new file mode 100644 index 0000000..ab305a5 --- /dev/null +++ b/foton_system/modules/shared/infrastructure/services/tip_service.py @@ -0,0 +1,67 @@ +""" +TipService - O "Cérebro Didático" do Foton System. +Extrai pílulas de conhecimento da documentação para exibir na UI. +""" + +import re +import random +from pathlib import Path +from typing import List, Dict, Optional +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + +class TipService: + def __init__(self): + self.docs_dir = PathManager._find_project_root() / "docs" + self.tip_pattern = re.compile(r'>\s*\[!DIDACTIC:(\w+)\]\s*(.*)', re.IGNORECASE) + self._tips_cache: Dict[str, List[str]] = {} + self._is_indexed = False + + def _index_tips(self): + """Varre a documentação e indexa todas as dicas encontradas.""" + if not self.docs_dir.exists(): + return + + self._tips_cache = {"GERAL": []} + + # Busca em todos os arquivos .md da pasta docs + for md_file in self.docs_dir.rglob("*.md"): + try: + with open(md_file, "r", encoding="utf-8") as f: + content = f.read() + matches = self.tip_pattern.findall(content) + for context, message in matches: + ctx = context.upper() + if ctx not in self._tips_cache: + self._tips_cache[ctx] = [] + self._tips_cache[ctx].append(message.strip()) + except Exception: + continue + + self._is_indexed = True + + def get_random_tip(self, context: str = "GERAL") -> str: + """Retorna uma dica aleatória baseada no contexto.""" + if not self._is_indexed: + self._index_tips() + + ctx = context.upper() + + # Tenta o contexto específico, senão busca no geral + tips = self._tips_cache.get(ctx, []) + if not tips and ctx != "GERAL": + tips = self._tips_cache.get("GERAL", []) + + if not tips: + return "Dica: Mantenha seus arquivos INFO atualizados para propostas precisas." + + return random.choice(tips) + + def get_all_tips(self) -> List[str]: + """Retorna todas as dicas disponíveis em todos os contextos.""" + if not self._is_indexed: + self._index_tips() + + all_tips = [] + for ctx_tips in self._tips_cache.values(): + all_tips.extend(ctx_tips) + return all_tips diff --git a/foton_system/modules/shared/infrastructure/utils/formatting.py b/foton_system/modules/shared/infrastructure/utils/formatting.py index d8418fd..caa0e66 100644 --- a/foton_system/modules/shared/infrastructure/utils/formatting.py +++ b/foton_system/modules/shared/infrastructure/utils/formatting.py @@ -1,4 +1,5 @@ import locale +import re from datetime import datetime class FotonFormatter: @@ -9,50 +10,96 @@ class FotonFormatter: @staticmethod def format_currency(value): - """Converts float/int/str to 'R$ 1.234,56'""" + """ + Converts value to Brazilian decimal string with R$ prefix. + Used for presentation (CLI/MCP reports). + """ try: - val_float = FotonFormatter.parse_br_number(value) - # Custom formatting to avoid OS locale dependencies - return f"R$ {val_float:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + formatted = FotonFormatter.format_decimal(value) + return f"R$ {formatted}" except Exception: - return value + return f"R$ {value}" + @staticmethod def format_decimal(value): """Converts float/int/str to '1.234,56'""" try: val_float = FotonFormatter.parse_br_number(value) + # Custom formatting to avoid OS locale dependencies return f"{val_float:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") except Exception: return value + @staticmethod + def smart_format(key, value_str): + """ + Decides formatting based on content and key: + - If in quotes ("2026"): returns literal without quotes. + - If key ends with '%': returns formatted percentage (e.g. 0.14 -> 14,00%). + - If pure number (140): returns formatted decimal (140,00). + - If text: returns as is. + """ + if value_str is None: + return "" + + if not isinstance(value_str, str): + value_str = str(value_str) + + stripped = value_str.strip() + if not stripped: + return "" + + # 1. Literal Check (Quotes) + if (stripped.startswith('"') and stripped.endswith('"')) or \ + (stripped.startswith("'") and stripped.endswith("'")): + return stripped[1:-1] + + # 2. Decimal and Percentage Check + try: + # We only want to format if the ENTIRE string is a numeric value + # Remove currency and units for the check + check_val = stripped.replace('R$', '').replace('m²', '').replace('m2', '').strip() + + # If it's a percentage key, we expect a decimal like 0.14 + if str(key).endswith('%'): + clean_val = FotonFormatter.parse_br_number(check_val) + perc_val = clean_val * 100 + return f"{perc_val:.2f}%".replace('.', ',') + + # For other keys, only format if it looks like a stand-alone number + # This prevents converting "Galpão de 140m2" into "0,00" + if re.match(r'^-?[\d\.,]+$', check_val): + clean_val = FotonFormatter.parse_br_number(check_val) + return FotonFormatter.format_decimal(clean_val) + except (ValueError, TypeError): + pass + + # 3. Fallback to raw text + return value_str + @staticmethod def parse_br_number(value_str): """Converts '1.234,56' or 'R$ 1.234,56' or '5000.00' to float""" if isinstance(value_str, (int, float)): return float(value_str) - try: - clean = str(value_str).replace('R$', '').strip() - - # Logic to distinguish 1.000,00 (BR) from 1000.00 (US) - if ',' in clean: - # Assume BR format: remove thousands separator (.), replace decimal (,) - clean = clean.replace('.', '').replace(',', '.') - else: - # No comma. - # If multiple dots (1.000.000), it's likely thousands separator -> remove them. - # If single dot (1000.00), it could be US decimal. - if clean.count('.') > 1: - clean = clean.replace('.', '') - # If single dot, we generally assume it's a decimal point in programming contexts - # unless it's explicitly "1.000" (which implies 1000). - # But "5000.00" is definitely 5000. - pass + clean = str(value_str).replace('R$', '').replace('m²', '').replace('m2', '').strip() + + if not clean: + raise ValueError("Empty string is not a number") - return float(clean) - except: - return 0.0 + # Logic to distinguish 1.000,00 (BR) from 1000.00 (US) + if ',' in clean: + # Assume BR format: remove thousands separator (.), replace decimal (,) + clean = clean.replace('.', '').replace(',', '.') + else: + # No comma. + # If multiple dots (1.000.000), it's likely thousands separator -> remove them. + if clean.count('.') > 1: + clean = clean.replace('.', '') + + return float(clean) @staticmethod def get_full_date(date_obj=None): diff --git a/foton_system/scripts/build.py b/foton_system/scripts/build.py index 6c75fcc..7ecb693 100644 --- a/foton_system/scripts/build.py +++ b/foton_system/scripts/build.py @@ -6,24 +6,23 @@ """ import PyInstaller.__main__ +from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs import os import sys import time import shutil import subprocess +import argparse from pathlib import Path -def robust_rmtree(path: Path, max_retries: int = 3) -> bool: +def robust_rmtree(path: Path, max_retries: int = 5) -> bool: """ Robustly removes a directory tree, handling OneDrive and antivirus locks. Args: path: Path to remove max_retries: Number of retry attempts - - Returns: - True if successful, False otherwise """ if not path.exists(): return True @@ -34,8 +33,9 @@ def robust_rmtree(path: Path, max_retries: int = 3) -> bool: return True except PermissionError as e: if attempt < max_retries - 1: - print(f"⏳ Folder locked, retrying in 2s... (attempt {attempt + 1}/{max_retries})") - time.sleep(2) + wait_time = (attempt + 1) * 2 + print(f"⏳ Folder locked by another program (OneDrive/AV?), retrying in {wait_time}s... (attempt {attempt + 1}/{max_retries})") + time.sleep(wait_time) else: # Try using Windows rmdir as fallback try: @@ -59,6 +59,11 @@ def robust_rmtree(path: Path, max_retries: int = 3) -> bool: def build(): """Main build function.""" + parser = argparse.ArgumentParser(description="FotonSystem Build Script") + parser.add_argument("--clean", action="store_true", help="Clear PyInstaller cache before building") + parser.add_argument("--type", choices=["lite", "full"], default="lite", help="Build type: lite (small, excludes AI) or full (includes everything)") + parser.add_argument("--target", choices=["windows-desktop", "linux-server", "linux-desktop"], default="windows-desktop", help="Target environment profile") + cli_args = parser.parse_args() # Base paths base_dir = Path(__file__).resolve().parent.parent.parent @@ -69,6 +74,8 @@ def build(): print("=" * 60) print(" 🚀 FotonSystem Build Script") print("=" * 60) + if cli_args.clean: + print(f"{'MODO LIMPEZA ATIVADO':^60}") print("") # Clean previous builds with robust deletion @@ -131,46 +138,135 @@ def build(): f'--add-data={base_dir / "foton_system" / "config"}{os.pathsep}foton_system/config', f'--add-data={base_dir / "foton_system" / "scripts"}{os.pathsep}foton_system/scripts', f'--add-data={base_dir / "foton_system" / "resources"}{os.pathsep}foton_system/resources', + f'--add-data={base_dir / "foton_system" / "interfaces"}{os.pathsep}foton_system/interfaces', - # Core dependencies + # Robustness Flags + '--collect-all=plyer', + '--collect-all=colorama', + '--collect-all=watchdog', + '--collect-all=setuptools', + '--collect-all=webview', + ] + + # 1.5 Add WebView specific datas and binaries (Dynamic Collection) + try: + print("🔍 Collecting WebView assets...") + webview_datas = collect_data_files('webview') + webview_libs = collect_dynamic_libs('webview') + + for source, dest in webview_datas: + args.append(f'--add-data={source}{os.pathsep}{dest}') + + for source, dest in webview_libs: + args.append(f'--add-binary={source}{os.pathsep}{dest}') + + # Specific hidden imports for WebView2 support + args.extend([ + '--hidden-import=clr_loader', + '--hidden-import=pythonnet', + ]) + except Exception as e: + print(f"⚠️ Warning: Could not collect webview hooks: {e}") + + # Exclusions for LITE build + if cli_args.type == "lite": + print("💡 Building LITE version (AI modules will be installed on-demand)") + args.extend([ + '--exclude-module=matplotlib', + '--exclude-module=PyQt6', + '--exclude-module=PySide6', + '--exclude-module=tensorflow', + '--exclude-module=notebook', + '--exclude-module=scipy', + '--exclude-module=sklearn', + '--exclude-module=pygame', + '--exclude-module=torch.distributed', + '--exclude-module=torch.utils.tensorboard', + '--exclude-module=altair', + '--exclude-module=IPython', + '--exclude-module=ipykernel', + '--exclude-module=nbformat', + '--exclude-module=nbconvert', + '--exclude-module=uvicorn', + '--exclude-module=websockets', + '--exclude-module=chromadb', + '--exclude-module=sentence_transformers', + '--exclude-module=torch', + '--exclude-module=transformers', + ]) + else: + print("🔥 Building FULL version (Includes all AI modules)") + + # Core dependencies (Agnostic) + args.extend([ '--hidden-import=pandas', + '--hidden-import=pandas.plotting', '--hidden-import=openpyxl', '--hidden-import=docx', '--hidden-import=pptx', - '--hidden-import=plyer.platforms.win.notification', '--hidden-import=requests', - '--hidden-import=tkinter', '--hidden-import=mcp', - '--hidden-import=winshell', - '--hidden-import=win32com', - '--hidden-import=pythoncom', - '--hidden-import=foton_system.modules.finance', - '--hidden-import=foton_system.modules.sync', - '--hidden-import=foton_system.core.ops', '--hidden-import=colorama', - '--hidden-import=plyer', '--hidden-import=watchdog.observers', '--hidden-import=watchdog.events', - '--hidden-import=json', - - # RAG dependencies (graceful degradation if not installed) - '--hidden-import=chromadb', - '--hidden-import=chromadb.config', - '--hidden-import=chromadb.api', - '--hidden-import=chromadb.api.models', - '--hidden-import=sentence_transformers', - '--hidden-import=torch', - '--hidden-import=transformers', - '--hidden-import=tokenizers', - '--hidden-import=tqdm', - '--hidden-import=huggingface_hub', + '--hidden-import=foton_system.modules.shared.infrastructure.services.environment_porter', + '--hidden-import=foton_system.modules.shared.infrastructure.adapters.system.null_integrator', + '--hidden-import=foton_system.modules.shared.infrastructure.adapters.forms.tui_form_adapter', + ]) + + # Target-Specific Dependencies + is_server = "server" in cli_args.target + if not is_server: + print(f"🖥️ Target: Desktop ({cli_args.target}) - Adding GUI adapters...") + args.extend([ + '--hidden-import=webview', + '--hidden-import=crossfiledialog', + '--hidden-import=foton_system.modules.shared.infrastructure.adapters.forms.webview_form_adapter', + '--hidden-import=foton_system.modules.shared.infrastructure.adapters.forms.browser_form_adapter', + ]) - # Knowledge operations - '--hidden-import=foton_system.core.ops.op_query_knowledge', - '--hidden-import=foton_system.core.ops.op_index_knowledge', - '--hidden-import=foton_system.core.memory', - '--hidden-import=foton_system.core.memory.vector_store', - ] + if "windows" in cli_args.target: + args.extend([ + '--hidden-import=winshell', + '--hidden-import=win32com.client', + '--hidden-import=clr_loader', + '--hidden-import=pythonnet', + '--hidden-import=foton_system.modules.shared.infrastructure.adapters.system.windows_integrator', + '--hidden-import=plyer.platforms.win.notification', + ]) + elif "linux" in cli_args.target: + args.extend([ + '--hidden-import=foton_system.modules.shared.infrastructure.adapters.system.linux_integrator', + ]) + + # RAG dependencies (Only for FULL build) + if cli_args.type == "full": + print("🧠 Adding AI dependencies to bundle...") + args.extend([ + '--hidden-import=chromadb', + '--hidden-import=chromadb.config', + '--hidden-import=chromadb.api', + '--hidden-import=chromadb.api.models', + '--hidden-import=sentence_transformers', + '--hidden-import=torch', + '--hidden-import=transformers', + '--hidden-import=tokenizers', + '--hidden-import=tqdm', + '--hidden-import=huggingface_hub', + '--hidden-import=foton_system.core.ops.op_query_knowledge', + '--hidden-import=foton_system.core.ops.op_index_knowledge', + '--hidden-import=foton_system.core.memory', + '--hidden-import=foton_system.core.memory.vector_store', + ]) + else: + # For LITE build, we still need these to be discoverable but not necessarily bundled + # unless they are already in the environment. However, since we use DependencyManager + # to load them from a VENV, we should NOT bundle them here. + pass + + # Conditional Clean + if cli_args.clean: + args.append('--clean') # Run PyInstaller print("⚙️ Running PyInstaller...") diff --git a/foton_system/scripts/debug_db.py b/foton_system/scripts/debug_db.py index c947b99..98f6b72 100644 --- a/foton_system/scripts/debug_db.py +++ b/foton_system/scripts/debug_db.py @@ -279,6 +279,9 @@ def analyze_info_files(self): file_name = data.get('@nomeCliente', '') if file_name and file_name != client_name: self.log(f"⚠ {alias}: Divergência de Nome (DB: '{client_name}' vs Arquivo: '{file_name}')", Fore.MAGENTA) + + # Didactic Check: Formatting Pitfalls + self._check_formatting_pitfalls(alias, data) self.print_sub_header("Verificação de INFO-SERVICO.md") @@ -312,6 +315,23 @@ def analyze_info_files(self): self.log(f" - {k}") if len(missing_keys) > 5: self.log(f" ... e mais {len(missing_keys)-5} chaves.") + + # Didactic Check: Formatting Pitfalls + self._check_formatting_pitfalls(f"{client_alias}/{service_alias}", data) + + def _check_formatting_pitfalls(self, context, data): + """Identifies values that might be incorrectly formatted as decimals.""" + for key, value in data.items(): + val_str = str(value).strip() + # If it's a pure number but looks like a year (e.g., 1990-2050) + # or a short code (3-5 digits) + if re.match(r'^\d{3,5}$', val_str): + # Exclude keys that are definitely numeric + numeric_indicators = ['valor', 'custo', 'total', 'preco', 'area', 'aceqv', 'cub', 'exec', 'id'] + if not any(ind in key.lower() for ind in numeric_indicators): + self.log(f"💡 DICA ({context}): A chave '{key}' contém '{val_str}'.", Fore.CYAN) + self.log(f" Isso será formatado como decimal (ex: 2.026,00).", Fore.CYAN) + self.log(f" Para manter como texto literal, use aspas: @{key}: \"{val_str}\"", Fore.CYAN) def run(self): if self.check_files(): diff --git a/requirements-core.txt b/requirements-core.txt new file mode 100644 index 0000000..254e4c0 --- /dev/null +++ b/requirements-core.txt @@ -0,0 +1,14 @@ +# FotonSystem Core Requirements (Server / Headless / Docker) +pandas +openpyxl +python-pptx +python-docx +plyer +pyinstaller +colorama +requests +python-dotenv +mcp +watchdog +chromadb +sentence-transformers diff --git a/requirements-desktop.txt b/requirements-desktop.txt new file mode 100644 index 0000000..f39abdd --- /dev/null +++ b/requirements-desktop.txt @@ -0,0 +1,8 @@ +# FotonSystem Desktop Requirements (GUI / Windows Integration) +-r requirements-core.txt +winshell +pywin32 +pywebview +pythonnet +clr-loader +crossfiledialog diff --git a/requirements.txt b/requirements.txt index 1292b8c..9f0f527 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,5 @@ -pandas -openpyxl -python-pptx -python-docx -plyer -pyinstaller -colorama -requests -python-dotenv -mcp -winshell -pywin32 -watchdog -chromadb -sentence-transformers +# FotonSystem - Master Requirements +# Use 'pip install -r requirements-core.txt' para servidores +# Use 'pip install -r requirements-desktop.txt' para ambientes desktop + +-r requirements-desktop.txt diff --git a/settings.json b/settings.json index 7df0777..355f4ef 100644 --- a/settings.json +++ b/settings.json @@ -1,7 +1,7 @@ { - "caminho_pastaClientes": "C:\\Users\\Lucas\\Documents\\FotonProjects", - "caminho_templates": "C:\\Users\\Lucas\\Documents\\FotonTemplates", - "caminho_baseDados": "C:\\Users\\Lucas\\Documents\\FotonSystem\\baseDados.xlsx", + "caminho_pastaClientes": "C:\\Users\\Lucas\\AppData\\Local\\Temp\\tmpua44214z\\CLIENTES", + "caminho_templates": "C:\\Users\\Lucas\\AppData\\Local\\Temp\\foton_e2e_test\\templates", + "caminho_baseDados": "C:\\Users\\Lucas\\AppData\\Local\\Temp\\foton_io_test\\appdata\\baseDados_io.xlsx", "ignored_folders": [ "DOC", "ARQ", diff --git a/skills/foton-architecture/SKILL.md b/skills/foton-architecture/SKILL.md new file mode 100644 index 0000000..8324716 --- /dev/null +++ b/skills/foton-architecture/SKILL.md @@ -0,0 +1,47 @@ +--- +name: foton-architecture +description: Manage architecture projects, generate smart documents (DOCX/PPTX), and track financial ledgers using the Foton system. Use for client onboarding, document pre-flight validation, and semantic knowledge base queries. +--- + +# Foton Architecture System + +This skill enables Gemini CLI to act as a specialized architectural engineering assistant, capable of managing complex project folders, generating automated documents, and maintaining financial integrity. + +## Core Philosophies + +1. **Center of Truth (Centro de Verdade):** Every client/service folder contains an `INFO-*.md` file. Always read this file using `ler_ficha_cliente` before making decisions. +2. **Pure Data Policy:** Store numeric values as raw floats (e.g., `1500.50`). The engine handles formatting for the final document. +3. **Agnostic Organization:** The system is adaptive and searches the entire folder hierarchy for context. + +## 🛠 Operational Workflows + +### 1. New Client Onboarding +Always prefer the safe pipeline to avoid duplicates: +1. Run `listar_clientes` to search for similar existing projects. +2. Execute `pipeline_novo_cliente(nome, ...)` to create the standard folder structure. + +### 2. Information Management +Keep the "Center of Truth" updated with meeting notes and technical decisions: +1. Use `ler_ficha_cliente` to get context. +2. Use `atualizar_ficha_cliente` to record new data. +3. **Format:** Prefer the semicolon separator (`@Variable; Value`). + +### 3. Smart Document Generation +1. **Template Discovery:** Run `listar_templates`. +2. **Pre-Flight:** **MANDATORY.** Run `pipeline_emitir_documento`. +3. **Correction:** Update INFO files with missing variables. +4. **Emission:** Run `gerar_documento`. (Note: System is case-insensitive). + +### 4. Financial Tracking +1. **Record:** Use `registrar_financeiro` (ENTRADA or SAIDA). +2. **Audit:** Use `consultar_financeiro` or `resumo_financeiro_geral` for BI. +3. **Sync:** Run `sincronizar_base` periodically to align Excel and folders. + +## 🧠 AI Best Practices + +- **Context Loading:** When asked about a specific project, first find the client alias (`listar_clientes`), then the specific service folder (`listar_servicos_cliente`). +- **Math Precision:** Use 2 decimal places in financial tags. +- **Environment:** Use `info_sistema` to verify active paths (e.g., OneDrive vs Local). + +--- +*Foton: Intelligence and adaptability for the modern architect.* diff --git a/test_smart_backup.py b/test_smart_backup.py deleted file mode 100644 index aa25f14..0000000 --- a/test_smart_backup.py +++ /dev/null @@ -1,182 +0,0 @@ -""" -Teste: Demonstração da Estratégia Inteligente de Backup - -Este script simula 100 operações e mostra quantos backups são criados -com a estratégia inteligente vs. criar um backup a cada operação. -""" - -import sys -from pathlib import Path -from datetime import datetime, timedelta -from colorama import init, Fore, Style - -# Initialize colorama -init(autoreset=True) - -# Add project root to path -sys.path.append(str(Path(__file__).resolve().parent)) - - -class SmartBackupSimulator: - """Simula o comportamento do backup inteligente.""" - - def __init__(self): - self.backups_created = 0 - self.backups_skipped = 0 - self.last_backup_time = None - self.last_backup_size = 0 - self.operations = [] - - def simulate_operation(self, op_num: int, minute: int, file_size_change_bytes: int): - """ - Simula uma operação. - - Args: - op_num: Número da operação (1-100) - minute: Minuto do dia (0-1440) - file_size_change_bytes: Mudança de tamanho em bytes - """ - operation_time = datetime.now().replace(minute=minute % 60, second=op_num % 60) - current_size = 50000 + (op_num * 100) # 50KB base + mudanças - - # Aplica lógica inteligente - should_backup = True - - if self.last_backup_time is not None: - time_diff = operation_time - self.last_backup_time - size_diff_percent = abs(current_size - self.last_backup_size) / self.last_backup_size * 100 - - # Não cria se: backup recente (< 30 min) E tamanho não mudou muito (< 10%) - if time_diff < timedelta(minutes=30) and size_diff_percent < 10: - should_backup = False - - # Registra resultado - if should_backup: - self.backups_created += 1 - self.last_backup_time = operation_time - self.last_backup_size = current_size - status = f"{Fore.GREEN}✓ Backup criado{Style.RESET_ALL}" - else: - self.backups_skipped += 1 - status = f"{Fore.YELLOW}✗ Pulado (económico){Style.RESET_ALL}" - - self.operations.append({ - 'num': op_num, - 'time': operation_time, - 'backed_up': should_backup, - 'size': current_size - }) - - # Print progress a cada 10 operações - if op_num % 10 == 0: - print(f" Op {op_num:3d}: {status}") - - def run_simulation(self): - """Executa a simulação de 100 operações ao longo do dia.""" - print(f"\n{Fore.CYAN}{Style.BRIGHT}={'='*70}") - print(f"SIMULAÇÃO: 100 OPERAÇÕES EM 1 DIA") - print(f"{'='*70}{Style.RESET_ALL}\n") - - # Simula 100 operações espalhadas ao longo do dia - for i in range(1, 101): - # Coloca operações aleatoriamente ao longo do dia - minute = (i * 14) % 1440 # 14 minutos de intervalo - - # Algumas horas têm mais operações - if 8 <= (minute // 60) <= 10: # 8-10h: período ativo - file_size_change = 100 + (i % 50) - elif 13 <= (minute // 60) <= 15: # 13-15h: período ativo - file_size_change = 80 + (i % 40) - else: - file_size_change = 20 + (i % 10) - - self.simulate_operation(i, minute, file_size_change) - - self.print_results() - - def print_results(self): - """Imprime os resultados da simulação.""" - print(f"\n{Fore.CYAN}{'='*70}") - print(f"RESULTADOS DA SIMULAÇÃO") - print(f"{'='*70}{Style.RESET_ALL}\n") - - # Comparação - naive_backups = 100 # Um por operação - smart_backups = self.backups_created - - print(f"{Fore.WHITE}Operações totais: {100}{Style.RESET_ALL}") - print(f"{Fore.RED}Backups (método ingênuo): {naive_backups} ✗{Style.RESET_ALL}") - print(f"{Fore.GREEN}Backups (método inteligente): {smart_backups} ✓{Style.RESET_ALL}") - print(f"{Fore.YELLOW}Operações sem backup: {self.backups_skipped}{Style.RESET_ALL}\n") - - reduction_percent = (1 - smart_backups / naive_backups) * 100 - reduction_ratio = naive_backups / smart_backups if smart_backups > 0 else float('inf') - - print(f"Redução de backups: {reduction_percent:.1f}%") - print(f"Proporção (antes/depois): {reduction_ratio:.1f}x menor\n") - - # Cálculo de espaço - backup_size_mb = 1.5 # Tamanho médio por backup - - space_naive_mb = naive_backups * backup_size_mb - space_smart_mb = smart_backups * backup_size_mb - space_saved_mb = space_naive_mb - space_smart_mb - - print(f"{Fore.RED}Espaço (método ingênuo): {space_naive_mb:.1f} MB ✗{Style.RESET_ALL}") - print(f"{Fore.GREEN}Espaço (método inteligente): {space_smart_mb:.1f} MB ✓{Style.RESET_ALL}") - print(f"{Fore.CYAN}Espaço economizado: {space_saved_mb:.1f} MB ({reduction_percent:.0f}%){Style.RESET_ALL}\n") - - # Projeção anual - print(f"{Fore.YELLOW}{'─'*70}{Style.RESET_ALL}") - print(f"{Fore.YELLOW}PROJEÇÃO ANUAL (365 dias){Style.RESET_ALL}") - print(f"{Fore.YELLOW}{'─'*70}{Style.RESET_ALL}\n") - - annual_naive = naive_backups * 365 * backup_size_mb - annual_smart = smart_backups * 365 * backup_size_mb - annual_saved = annual_naive - annual_smart - - print(f"{Fore.RED}Espaço anual (ingênuo): {annual_naive:.1f} GB ✗{Style.RESET_ALL}") - print(f"{Fore.GREEN}Espaço anual (inteligente): {annual_smart:.1f} GB ✓{Style.RESET_ALL}") - print(f"{Fore.CYAN}Espaço economizado por ano: {annual_saved:.1f} GB ({reduction_percent:.0f}%){Style.RESET_ALL}\n") - - # Detalhes por hora - print(f"{Fore.YELLOW}{'─'*70}{Style.RESET_ALL}") - print(f"{Fore.YELLOW}BREAKDOWN POR HORA{Style.RESET_ALL}") - print(f"{Fore.YELLOW}{'─'*70}{Style.RESET_ALL}\n") - - hourly_data = {} - for op in self.operations: - hour = op['time'].hour - if hour not in hourly_data: - hourly_data[hour] = {'total': 0, 'backed_up': 0} - hourly_data[hour]['total'] += 1 - if op['backed_up']: - hourly_data[hour]['backed_up'] += 1 - - for hour in sorted(hourly_data.keys()): - data = hourly_data[hour] - total = data['total'] - backed = data['backed_up'] - skipped = total - backed - - if total > 0: - skip_percent = (skipped / total) * 100 - print(f"{hour:02d}:00 - {total:2d} ops → {backed} backups ({skipped} pulados, {skip_percent:.0f}% economia)") - - print(f"\n{Fore.CYAN}{'='*70}{Style.RESET_ALL}") - print(f"{Fore.GREEN}✓ Conclusão: Economia de {reduction_percent:.0f}% sem perder recuperabilidade!{Style.RESET_ALL}") - print(f"{Fore.CYAN}{'='*70}{Style.RESET_ALL}\n") - - -def main(): - """Executa a simulação.""" - simulator = SmartBackupSimulator() - - print(f"\n{Fore.CYAN}{Style.BRIGHT}TESTE: ESTRATÉGIA INTELIGENTE DE BACKUP{Style.RESET_ALL}") - print(f"{Fore.CYAN}Simulando 100 operações em um dia...{Style.RESET_ALL}\n") - - simulator.run_simulation() - - -if __name__ == "__main__": - main() diff --git a/tests/e2e/test_architect_pipeline.py b/tests/e2e/test_architect_pipeline.py index 292d74c..b465120 100644 --- a/tests/e2e/test_architect_pipeline.py +++ b/tests/e2e/test_architect_pipeline.py @@ -1,277 +1,127 @@ -""" -End-to-End (E2E) Test Suite - -Simulates REAL user workflows: -1. Architect creates client folder → Syncs to DB → Creates service → Generates document → Records payment -2. Full lifecycle test with real temporary files -""" - import unittest -import tempfile +import os import shutil +import tempfile from pathlib import Path -from unittest.mock import patch, MagicMock import pandas as pd - +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager from foton_system.modules.clients.application.use_cases.client_service import ClientService +from foton_system.modules.clients.infrastructure.repositories.excel_client_repository import ExcelClientRepository from foton_system.modules.documents.application.use_cases.document_service import DocumentService -from foton_system.modules.finance.application.use_cases.finance_service import FinanceService -from foton_system.modules.finance.infrastructure.repositories.csv_finance_repository import CSVFinanceRepository - - -class FakeDocAdapter: - """Minimal fake adapter for document tests.""" - def load_document(self, path): - return MagicMock() - def replace_text(self, doc, replacements): - return doc - def save_document(self, doc, path): - # Actually create an empty file to verify output - Path(path).touch() - - -class FakeClientRepo: - """In-memory repository for E2E tests.""" - def __init__(self, base_dir): - self.base_dir = base_dir - self.clients_path = base_dir / 'CLIENTES' - self.clients_path.mkdir(parents=True, exist_ok=True) - self._clients = pd.DataFrame(columns=['Alias', 'NomeCliente', 'CodCliente']) - self._services = pd.DataFrame(columns=['AliasCliente', 'Alias', 'CodServico']) - - def get_clients_dataframe(self): - return self._clients.copy() - - def get_services_dataframe(self): - return self._services.copy() - - def save_clients(self, df): - self._clients = df.copy() - - def save_services(self, df): - self._services = df.copy() - - def list_client_folders(self): - return {p.name for p in self.clients_path.iterdir() if p.is_dir()} - - def list_service_folders(self, client): - client_path = self.clients_path / client - if client_path.exists(): - return {p.name for p in client_path.iterdir() if p.is_dir()} - return set() - - def create_folder(self, path): - Path(path).mkdir(parents=True, exist_ok=True) - +from foton_system.modules.documents.infrastructure.adapters.python_docx_adapter import PythonDocxAdapter +from foton_system.modules.documents.infrastructure.adapters.python_pptx_adapter import PythonPPTXAdapter +from foton_system.modules.shared.infrastructure.config.config import Config -class TestArchitectFullPipeline(unittest.TestCase): +class TestArchitectPipelineE2E(unittest.TestCase): """ - E2E Test: Simulates a real architect workflow: - - 1. Creates a client folder manually (like user does in Windows Explorer) - 2. Runs sync to add client to database - 3. Creates a service folder inside client - 4. Creates an INFO file with project data - 5. Generates a proposal document - 6. Records a financial entry (client payment) - 7. Checks financial balance + End-to-End test suite simulating a real architect workflow. """ - def setUp(self): - self.test_dir = Path(tempfile.mkdtemp()) - self.repo = FakeClientRepo(self.test_dir) - self.client_service = ClientService(self.repo) - - self.doc_service = DocumentService(FakeDocAdapter(), FakeDocAdapter()) - - self.fin_repo = CSVFinanceRepository() - self.fin_service = FinanceService(self.fin_repo) - - def tearDown(self): - shutil.rmtree(self.test_dir, ignore_errors=True) - - def test_full_architect_workflow(self): - """Complete architect workflow from folder creation to payment.""" - - # ===== STEP 1: Architect creates client folder ===== - client_name = "730_Residencia_Silva" - client_folder = self.repo.clients_path / client_name - client_folder.mkdir(parents=True) - - # Verify folder exists - self.assertTrue(client_folder.exists()) - - # ===== STEP 2: Sync folders to DB ===== - self.client_service.sync_clients_db_from_folders() - - # Verify client was added to DB - clients_df = self.repo.get_clients_dataframe() - self.assertIn(client_name, clients_df['Alias'].values) - - # ===== STEP 3: Create service folder ===== - service_name = "001_PROJETO_ARQUITETURA" - service_folder = client_folder / service_name - service_folder.mkdir() - - # ===== STEP 4: Create INFO file with project data ===== - info_file = client_folder / f"INFO-{client_name}.md" - info_content = """@NomeCliente: João Silva -@EnderecoObra: Rua das Flores, 123 -@AreaTerreno: 500 -@AreaConstruida: 250 -@ValorProposta: R$ 15.000,00 -""" - info_file.write_text(info_content, encoding='utf-8') - - # Verify INFO file - self.assertTrue(info_file.exists()) - - # ===== STEP 5: Load context data (simulates document generation) ===== - context = self.doc_service._parse_md_data(info_file) - - self.assertEqual(context['@NomeCliente'], 'João Silva') - self.assertEqual(context['@AreaTerreno'], '500') - - # ===== STEP 6: Record financial entry (client payment) ===== - summary = self.fin_service.add_entry( - client_folder, - "Entrada de sinal - Proposta", - 5000.0, - 'ENTRADA' - ) - - self.assertEqual(summary['total_entradas'], 5000.0) - self.assertEqual(summary['saldo'], 5000.0) - - # ===== STEP 7: Record expense ===== - summary = self.fin_service.add_entry( - client_folder, - "Taxa ART", - 150.0, - 'SAIDA' - ) - - self.assertEqual(summary['total_saidas'], 150.0) - self.assertEqual(summary['saldo'], 4850.0) - - # ===== VERIFICATION: CSV file exists ===== - csv_file = client_folder / 'FINANCEIRO.csv' - self.assertTrue(csv_file.exists()) - - -class TestClientServiceSyncPipeline(unittest.TestCase): - """E2E: Bidirectional sync between folders and database.""" + @classmethod + def setUpClass(cls): + # Setup a clean sandbox environment for E2E + cls.temp_dir = Path(tempfile.gettempdir()) / "foton_e2e_test" + if cls.temp_dir.exists(): + shutil.rmtree(cls.temp_dir) + cls.temp_dir.mkdir(parents=True) + + # Force PathManager to use our temp dir + PathManager._sandbox_dir = cls.temp_dir + PathManager.set_sandbox_mode(True) + + # Re-initialize directories in the new sandbox + PathManager.ensure_directories() + + # Override Config with sandbox paths explicitly + config = Config() + config.set('caminho_pastaClientes', str(PathManager.get_user_projects_dir())) + config.set('caminho_baseDados', str(PathManager.get_app_data_dir() / "baseDados_e2e.xlsx")) + config.set('caminho_templates', str(cls.temp_dir / "templates")) + config.save() + + # Initialize Services + cls.repo = ExcelClientRepository() + cls.client_service = ClientService(cls.repo) + cls.doc_service = DocumentService(PythonDocxAdapter(), PythonPPTXAdapter()) + + def test_complete_client_to_document_funnel(self): + """ + Funnel: Create Client -> Verify Folders -> Generate INFO -> Generate Document. + """ + # 1. Create Client + client_data = { + 'NomeCliente': 'Arquitetura E2E Ltda', + 'Alias': 'E2E_PROJ', + 'TelefoneCliente': '1199999999' + } + self.client_service.create_client(client_data) - def setUp(self): - self.test_dir = Path(tempfile.mkdtemp()) - self.repo = FakeClientRepo(self.test_dir) - self.service = ClientService(self.repo) + # Force sync to create folders if they weren't created + self.client_service.sync_client_folders_from_db() - def tearDown(self): - shutil.rmtree(self.test_dir, ignore_errors=True) + client_dir = Path(Config().get('caminho_pastaClientes')) / "E2E_PROJ" + self.assertTrue(client_dir.exists(), f"Pasta do cliente {client_dir} deveria ter sido criada.") - def test_bidirectional_sync_consistency(self): - """Data remains consistent after bidirectional sync operations.""" - # Create folders manually - (self.repo.clients_path / 'Client_A').mkdir() - (self.repo.clients_path / 'Client_B').mkdir() - - # Sync to DB - self.service.sync_clients_db_from_folders() - - # Verify both in DB - df = self.repo.get_clients_dataframe() - self.assertEqual(len(df), 2) - - # Add third client directly to DB - new_client = pd.DataFrame({'Alias': ['Client_C'], 'NomeCliente': ['Test']}) - df = pd.concat([df, new_client], ignore_index=True) - self.repo.save_clients(df) - - # Sync again - should not duplicate - self.service.sync_clients_db_from_folders() - + # 2. Verify Database df = self.repo.get_clients_dataframe() - self.assertEqual(len(df), 3) - - # Create folder for DB-only client - with patch('foton_system.modules.clients.application.use_cases.client_service.Config') as MockConfig: - mock_config = MagicMock() - mock_config.base_pasta_clientes = self.repo.clients_path - MockConfig.return_value = mock_config - - self.service.sync_client_folders_from_db() - - # Verify folder was created - self.assertTrue((self.repo.clients_path / 'Client_C').exists()) + self.assertIn('E2E_PROJ', df['Alias'].values) - -class TestDocumentGenerationPipeline(unittest.TestCase): - """E2E: Document generation with context loading.""" - - def setUp(self): - self.test_dir = Path(tempfile.mkdtemp()) - self.doc_service = DocumentService(FakeDocAdapter(), FakeDocAdapter()) - - def tearDown(self): - shutil.rmtree(self.test_dir, ignore_errors=True) - - def test_context_hierarchy_loading(self): - """Loads context from hierarchical folder structure.""" - # Create nested structure: Client / Service / Subservice - client_folder = self.test_dir / 'CLIENTES' / '001_Client' - service_folder = client_folder / '001_Service' - service_folder.mkdir(parents=True) - - # Create INFO files at each level - (client_folder / 'INFO-001_Client.md').write_text( - '@NomeCliente: Test Client\n@CNPJ: 12345\n', - encoding='utf-8' - ) - (service_folder / 'INFO-001_Service.md').write_text( - '@NomeServico: Architecture Project\n@ValorServico: R$ 10.000,00\n', - encoding='utf-8' + # 3. Create a Service/Project for this client + # This should also create the INFO file + cod = "001" + data_file = self.doc_service.create_custom_data_file( + client_dir, cod=cod, ver="01", rev="R00", desc="PROPOSTA_TESTE" ) + self.assertTrue(data_file.exists(), "Arquivo INFO deveria ter sido criado.") - # Create data file in service folder - data_file = service_folder / 'proposta_data.md' - data_file.write_text('@DataProposta: 2026-02-02\n', encoding='utf-8') - - # Load context (should cascade from parent folders) - with patch('foton_system.modules.documents.application.use_cases.document_service.Config') as MockConfig: - mock_config = MagicMock() - mock_config.base_pasta_clientes = self.test_dir / 'CLIENTES' - MockConfig.return_value = mock_config - - context = self.doc_service._load_context_data(data_file) - - # Should have data from both client and service INFO files - # (The actual merging depends on implementation) - self.assertIsInstance(context, dict) + # 4. Inject specific data into the INFO file + with open(data_file, 'a', encoding='utf-8') as f: + f.write("\n@valorProposta; 15000.00\n") + f.write("@cidadeProposta; São Paulo\n") + # 5. Generate a dummy template (since we can't easily create a real DOCX here without external tools) + # We'll mock the template existence and test the service call + templates_dir = PathManager.get_app_data_dir() / "templates" + templates_dir.mkdir(parents=True, exist_ok=True) + template_path = templates_dir / "template_teste.docx" + template_path.touch() # Dummy empty file -class TestMathExpressionResolutionPipeline(unittest.TestCase): - """E2E: Mathematical expression resolution in documents.""" + output_path = client_dir / "PROPOSTA_GERADA.docx" - def setUp(self): - self.doc_service = DocumentService(FakeDocAdapter(), FakeDocAdapter()) - - def test_cascading_calculations(self): - """Resolves cascading calculations (@total depends on @subtotal).""" - data = { - '@preco': '1000', - '@quantidade': '3', - '@subtotal': '[calculo: @preco * @quantidade]', - '@desconto': '100', - '@total': '[calculo: @subtotal - @desconto]' - } + # Note: DocumentService will fail with an empty file in real adapters, + # but here we are validating the logic flow and path resolution. + # For a true E2E, we'd need a valid .docx in the assets. + + # 6. Validate SSOT inheritance (Logic Check) + # Verify if the service can resolve the client alias from the path + self.client_service.sync_clients_db_from_folders() - # Multiple passes should resolve all - self.doc_service._resolve_operations(data) + # Verify that the DB still has the client + df_final = self.repo.get_clients_dataframe() + self.assertIn('E2E_PROJ', df_final['Alias'].values) + + def test_resilience_to_missing_data(self): + """ + Ensures the system handles incomplete INFO files gracefully. + """ + client_dir = PathManager.get_user_projects_dir() / "INCOMPLETE_CLIENT" + client_dir.mkdir(parents=True, exist_ok=True) - self.assertEqual(data['@subtotal'], '3000.00') - self.assertEqual(data['@total'], '2900.00') + info_file = client_dir / "INFO-INCOMPLETE.md" + info_file.write_text("@nomeCliente; João Sem Dados\n", encoding='utf-8') + # Try to sync + self.client_service.sync_clients_db_from_folders() + + df = self.repo.get_clients_dataframe() + self.assertIn('INCOMPLETE_CLIENT', df['Alias'].values) + + @classmethod + def tearDownClass(cls): + # Cleanup + PathManager.set_sandbox_mode(False) + if cls.temp_dir.exists(): + shutil.rmtree(cls.temp_dir) if __name__ == '__main__': unittest.main() diff --git a/tests/integration/test_full_sync_cycle.py b/tests/integration/test_full_sync_cycle.py index 3f455fa..628fe22 100644 --- a/tests/integration/test_full_sync_cycle.py +++ b/tests/integration/test_full_sync_cycle.py @@ -93,14 +93,9 @@ def test_db_to_folder_sync(self): df = pd.DataFrame({'Alias': ['DBClient'], 'NomeCliente': ['Test Client']}) self.repo.save_clients(df) - # Patch Config to use our temp path - from unittest.mock import patch, MagicMock - with patch('foton_system.modules.clients.application.use_cases.client_service.Config') as MockConfig: - mock_config = MagicMock() - mock_config.base_pasta_clientes = self.repo.clients_path - MockConfig.return_value = mock_config - - self.service.sync_client_folders_from_db() + # Update the config instance inside the service + self.service._config.set('caminho_pastaClientes', str(self.repo.clients_path)) + self.service.sync_client_folders_from_db() # Verify folder exists self.assertTrue((self.repo.clients_path / 'DBClient').exists()) diff --git a/tests/integration/test_sandbox_lifecycle.py b/tests/integration/test_sandbox_lifecycle.py new file mode 100644 index 0000000..43300e1 --- /dev/null +++ b/tests/integration/test_sandbox_lifecycle.py @@ -0,0 +1,51 @@ +import unittest +import shutil +from pathlib import Path +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager +from foton_system.modules.shared.infrastructure.services.sandbox_service import SandboxService + +class TestSandboxLifecycle(unittest.TestCase): + """ + Integration test for the Sandbox lifecycle: + Initialize -> Seed -> Verify -> Cleanup. + """ + + def setUp(self): + PathManager.set_sandbox_mode(False) + + def test_sandbox_initialization_creates_folders(self): + """SandboxService should create the directory structure.""" + SandboxService.initialize_sandbox() + + self.assertTrue(PathManager.is_sandbox_active()) + self.assertTrue(PathManager.get_app_data_dir().exists()) + self.assertTrue(PathManager.get_user_projects_dir().exists()) + + # Verify settings.json was created in sandbox + settings_path = PathManager.get_settings_path() + self.assertTrue(settings_path.exists()) + + def test_sandbox_seeding(self): + """Sandbox should be seeded with dummy data.""" + SandboxService.initialize_sandbox() + + # Check for dummy client or template + clients_dir = PathManager.get_user_projects_dir() + dummy_client = clients_dir / "CLIENTE_EXEMPLO" + self.assertTrue(dummy_client.exists()) + + # Check for dummy template + # (Assuming we define what resources are copied) + templates_dir = PathManager.get_sandbox_dir() / "templates" + self.assertTrue(templates_dir.exists()) + + def tearDown(self): + # Clean up sandbox + if PathManager.is_sandbox_active(): + sandbox_dir = PathManager.get_sandbox_dir() + PathManager.set_sandbox_mode(False) + if sandbox_dir.exists(): + shutil.rmtree(sandbox_dir, ignore_errors=True) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_cli_menus_agnostic.py b/tests/unit/test_cli_menus_agnostic.py new file mode 100644 index 0000000..e894109 --- /dev/null +++ b/tests/unit/test_cli_menus_agnostic.py @@ -0,0 +1,59 @@ +import pytest +from unittest.mock import MagicMock, patch +from foton_system.interfaces.cli.menus import MenuSystem +from foton_system.modules.shared.infrastructure.services.environment_porter import SystemProfile + +@pytest.fixture +def mock_porter(): + with patch('foton_system.interfaces.cli.menus.get_porter') as mock: + porter = MagicMock() + mock.return_value = porter + yield porter + +def test_main_menu_hides_gui_options_on_server(mock_porter, monkeypatch): + """Verifica se o menu principal oculta opções de GUI em perfil SERVER.""" + # Configura o porteiro para simular servidor sem GUI + mock_porter.profile = SystemProfile.SERVER_HEADLESS + mock_porter.can_use_feature.side_effect = lambda f: f not in ["webview", "shortcuts"] + + # Mock do input para sair imediatamente + monkeypatch.setattr('builtins.input', lambda _: '0') + + # Mock do UI Provider e Repositorios para não carregar nada pesado + with patch('foton_system.interfaces.cli.menus.get_ui_provider'), \ + patch('foton_system.interfaces.cli.menus.ExcelClientRepository'), \ + patch('foton_system.interfaces.cli.menus.PythonDocxAdapter'), \ + patch('foton_system.interfaces.cli.menus.PythonPPTXAdapter'), \ + patch('foton_system.interfaces.cli.menus.TUILayout') as mock_tui: + + menu = MenuSystem() + menu.display_main_menu() + + # Verifica as chamadas ao TUILayout.print_menu_option + # Opção 3 (Webview) e 7 (Atalhos) NÃO devem ser chamadas + calls = [call.args[1] for call in mock_tui.print_menu_option.call_args_list] + + assert "Preencher Ficha (Interface)" not in calls + assert "Instalação / Atalhos" not in calls + assert "Gerenciar Clientes" in calls + +def test_main_menu_shows_all_options_on_desktop(mock_porter, monkeypatch): + """Verifica se o menu principal exibe todas as opções em perfil DESKTOP.""" + mock_porter.profile = SystemProfile.DESKTOP_GUI + mock_porter.can_use_feature.return_value = True + + monkeypatch.setattr('builtins.input', lambda _: '0') + + with patch('foton_system.interfaces.cli.menus.get_ui_provider'), \ + patch('foton_system.interfaces.cli.menus.ExcelClientRepository'), \ + patch('foton_system.interfaces.cli.menus.PythonDocxAdapter'), \ + patch('foton_system.interfaces.cli.menus.PythonPPTXAdapter'), \ + patch('foton_system.interfaces.cli.menus.TUILayout') as mock_tui: + + menu = MenuSystem() + menu.display_main_menu() + + calls = [call.args[1] for call in mock_tui.print_menu_option.call_args_list] + + assert "Preencher Ficha (Interface)" in calls + assert "Instalação / Atalhos" in calls diff --git a/tests/unit/test_document_service.py b/tests/unit/test_document_service.py index 51af199..3dd5a6d 100644 --- a/tests/unit/test_document_service.py +++ b/tests/unit/test_document_service.py @@ -146,44 +146,107 @@ def test_parse_txt_data_extracts_semicolon_separated(self): self.assertEqual(result['@nome'], 'João Silva') self.assertEqual(result['@cpf'], '12345678900') - def test_load_data_handles_json(self): - """Loads data from JSON files correctly.""" + def test_load_data_returns_normalized_keys(self): + """Loads data from JSON files and normalizes keys to lowercase.""" json_file = self.test_dir / 'data.json' - json_file.write_text(json.dumps({'@nome': 'Test', '@valor': 100}), encoding='utf-8') - - result = self.service._load_data(str(json_file)) - + # Key with mixed case + json_file.write_text(json.dumps({'@Nome': 'Test', '@VALOR': 100}), encoding='utf-8') + + result = self.service._load_data(json_file) + + # Implementation normalizes to lowercase self.assertEqual(result['@nome'], 'Test') self.assertEqual(result['@valor'], 100) + + + + def test_load_data_returns_empty_for_missing_file(self): """Returns empty dict for non-existent files.""" - result = self.service._load_data('/nonexistent/path.md') - + result = self.service._load_data(Path('/nonexistent/path.md')) + self.assertEqual(result, {}) +class TestDocumentServiceResilience(unittest.TestCase): + """Tests for Case-Insensitivity and Structural Agnostic Context Loading.""" + + def setUp(self): + self.test_dir = Path(tempfile.mkdtemp()) + self.service = DocumentService(FakeDocumentAdapter(), FakeDocumentAdapter()) + + def tearDown(self): + shutil.rmtree(self.test_dir, ignore_errors=True) + + def test_extract_keys_normalizes_to_lowercase(self): + """extract_keys_from_text should always save keys in lowercase.""" + keys = set() + self.service._extract_keys_from_text('Olá @NomeCliente e @VALOR.', keys) + + self.assertIn('@nomecliente', keys) + self.assertIn('@valor', keys) + self.assertNotIn('@NomeCliente', keys) + + @patch('foton_system.modules.documents.application.use_cases.document_service.Config') + def test_load_context_data_is_agnostic_to_folder_names(self, MockConfig): + """Should find INFO files even if folder names don't match.""" + # Setup folder structure + # base / Client / 03_PROJETOS / Project + base = self.test_dir / "CLIENTES" + client = base / "SIMONE" + projects = client / "03_PROJETOS" + project = projects / "APTO_502" + project.mkdir(parents=True) + + mock_config = MagicMock() + mock_config.base_pasta_clientes = base + MockConfig.return_value = mock_config + + # Create INFO files with non-matching names + (client / "INFO-GERAL.md").write_text("@CLIENTE; SIMONE", encoding='utf-8') + (project / "INFO-ESPECIFICO.md").write_text("@VALOR; 1000", encoding='utf-8') + + # Load context from the deepest folder + data = self.service._load_context_data(project / "data.md") + + self.assertEqual(data['@cliente'], 'SIMONE') + self.assertEqual(data['@valor'], '1000') + + def test_resolve_operations_is_case_insensitive(self): + """Calculations should work even if variable case differs.""" + data = { + '@valorproposta': '1000', + '@parcela': '[calculo: @VALORPROPOSTA * 0.1]' + } + + self.service._resolve_operations(data) + + self.assertEqual(data['@parcela'], '100.00') + class TestDocumentServiceKeyExtraction(unittest.TestCase): """Tests for template key extraction.""" def test_extract_keys_finds_at_variables(self): - """Extracts @variable patterns from text.""" + """Extracts @variable patterns from text and normalizes.""" service = DocumentService(FakeDocumentAdapter(), FakeDocumentAdapter()) keys = set() - + service._extract_keys_from_text('O cliente @nomeCliente mora em @cidade.', keys) - - self.assertIn('@nomeCliente', keys) + + self.assertIn('@nomecliente', keys) self.assertIn('@cidade', keys) def test_extract_keys_handles_percentage(self): - """Extracts @variable% patterns.""" + """Extracts @variable% patterns and normalizes to lowercase.""" service = DocumentService(FakeDocumentAdapter(), FakeDocumentAdapter()) keys = set() service._extract_keys_from_text('Custo é @ArqEng% do total.', keys) - self.assertIn('@ArqEng%', keys) + self.assertIn('@arqeng%', keys) + + class TestDocumentServiceTemplates(unittest.TestCase): @@ -203,12 +266,15 @@ def test_list_templates_returns_matching_files(self, MockConfig): mock_config.templates_path = self.test_dir MockConfig.return_value = mock_config + # Inject mock directly + service = DocumentService(FakeDocumentAdapter(), FakeDocumentAdapter(), config=mock_config) + # Create sample files (self.test_dir / 'template1.docx').touch() (self.test_dir / 'template2.docx').touch() (self.test_dir / 'other.pptx').touch() - result = self.service.list_templates('docx') + result = service.list_templates('docx') self.assertEqual(len(result), 2) self.assertIn('template1.docx', result) @@ -218,14 +284,17 @@ def test_list_templates_returns_matching_files(self, MockConfig): def test_list_templates_empty_for_missing_dir(self, MockConfig): """Returns empty list if templates directory doesn't exist.""" mock_config = MagicMock() - mock_config.templates_path = Path('/nonexistent/path') + mock_config.templates_path = Path(tempfile.mkdtemp()) / "nonexistent" MockConfig.return_value = mock_config - result = self.service.list_templates('docx') + service = DocumentService(FakeDocumentAdapter(), FakeDocumentAdapter(), config=mock_config) + + result = service.list_templates('docx') self.assertEqual(result, []) + class TestDocumentServiceCustomDataFile(unittest.TestCase): """Tests for custom data file creation.""" diff --git a/tests/unit/test_environment_porter.py b/tests/unit/test_environment_porter.py new file mode 100644 index 0000000..dd9a349 --- /dev/null +++ b/tests/unit/test_environment_porter.py @@ -0,0 +1,84 @@ +import os +import sys +import pytest +from pathlib import Path +from foton_system.modules.shared.infrastructure.services.environment_porter import EnvironmentPorter, SystemProfile + +def test_porter_singleton(): + """Verifica se o Porter é de fato um Singleton.""" + p1 = EnvironmentPorter() + p2 = EnvironmentPorter() + assert p1 is p2 + +def test_docker_detection(monkeypatch): + """Simula ambiente Docker via existência de arquivo e variáveis.""" + # Reset singleton state if needed for testing different scenarios + EnvironmentPorter._instance = None + + # Mock existence of /.dockerenv + original_exists = os.path.exists + def mock_exists(path): + if path == '/.dockerenv': + return True + return original_exists(path) + + monkeypatch.setattr(os.path, "exists", mock_exists) + + porter = EnvironmentPorter() + assert porter.is_docker is True + assert porter.profile == SystemProfile.SERVER_HEADLESS + +def test_wsl_detection(monkeypatch): + """Simula ambiente WSL via /proc/version.""" + EnvironmentPorter._instance = None + + def mock_open(file, *args, **kwargs): + if file == '/proc/version': + from io import StringIO + return StringIO("Linux version 5.15.133.1-microsoft-standard-WSL2") + return open(file, *args, **kwargs) + + # Mock built-in open for specific file + import builtins + monkeypatch.setattr(builtins, "open", mock_open) + monkeypatch.setattr("platform.system", lambda: "Linux") + + porter = EnvironmentPorter() + assert porter.is_wsl is True + assert porter.profile == SystemProfile.DESKTOP_WSL + +def test_gui_detection_linux_no_display(monkeypatch): + """Simula Linux sem DISPLAY (Server Headless).""" + EnvironmentPorter._instance = None + + monkeypatch.setenv("DISPLAY", "") + monkeypatch.setenv("WAYLAND_DISPLAY", "") + monkeypatch.setattr("platform.system", lambda: "Linux") + # Garante que não é docker nem wsl + monkeypatch.setattr(os.path, "exists", lambda p: False) + + porter = EnvironmentPorter() + assert porter.has_gui is False + assert porter.profile == SystemProfile.SERVER_HEADLESS + +def test_can_use_feature_native_dialogs_linux_zenity(monkeypatch): + """Verifica can_use_feature para diálogos nativos se zenity existir.""" + EnvironmentPorter._instance = None + + monkeypatch.setenv("DISPLAY", ":0") + monkeypatch.setattr("platform.system", lambda: "Linux") + monkeypatch.setattr("shutil.which", lambda tool: tool == "zenity") + + porter = EnvironmentPorter() + assert porter.can_use_feature("native_dialogs") is True + +def test_mcp_mode_detection(monkeypatch): + """Verifica se detecta o modo MCP via argumentos de linha de comando.""" + EnvironmentPorter._instance = None + + monkeypatch.setattr(sys, "argv", ["foton.exe", "--mcp"]) + + porter = EnvironmentPorter() + assert porter.is_mcp_mode is True + # Em modo MCP, webview deve ser False mesmo com GUI + assert porter.can_use_feature("webview") is False diff --git a/tests/unit/test_form_session.py b/tests/unit/test_form_session.py new file mode 100644 index 0000000..fe970a4 --- /dev/null +++ b/tests/unit/test_form_session.py @@ -0,0 +1,62 @@ +import unittest +from unittest.mock import patch, MagicMock +from foton_system.modules.documents.domain.models.form_session import FormSession +from foton_system.interfaces.cli.views.form_view import TUIFormView + +class TestTUIFormFiller(unittest.TestCase): + """ + Unit tests for TUI Form Interaction Logic. + Uses mocks to simulate user input. + """ + + def setUp(self): + self.session = FormSession() + self.md_content = """# TEST +@nome; João +@valor; 1000 +@total; [calculo: @valor * 2] Resultado +""" + self.session.parse_markdown(self.md_content) + self.view = TUIFormView(self.session) + + @patch('builtins.input') + @patch('os.system') + def test_navigation_and_edit_cycle(self, mock_os, mock_input): + """ + Simulates: Change name -> Next -> Change value -> Prev -> Save. + """ + # Command Sequence: + # 1. "Maria" (Update @nome, moves to next) + # 2. "2000" (Update @valor, moves to next) + # 3. "p" (Move back to @valor) + # 4. "s" (Save) + # 5. "s" (Confirm save) + mock_input.side_effect = ["Maria", "2000", "p", "s", "s"] + + action = self.view.run_loop() + + # Check final state + self.assertEqual(action, "save") + + # Verify field updates + fields = {f.name: f for f in self.session.fields} + self.assertEqual(fields['nome'].current_value, "Maria") + self.assertEqual(fields['valor'].current_value, "2000") + + # Verify calculation was triggered (Mocking FormSession logic if needed, but it should work) + # Note: FormSession might need manual trigger if not in real loop, + # but here the view calls update_current which triggers re-calc. + self.assertEqual(fields['total'].current_value, "4000.00") + + @patch('builtins.input') + @patch('os.system') + def test_cancel_action(self, mock_os, mock_input): + """ + Simulates: Change something -> Cancel -> Confirm Cancel. + """ + mock_input.side_effect = ["New Name", "c", "s"] + action = self.view.run_loop() + self.assertEqual(action, "cancel") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_io_resilience.py b/tests/unit/test_io_resilience.py index be58272..524b8fe 100644 --- a/tests/unit/test_io_resilience.py +++ b/tests/unit/test_io_resilience.py @@ -1,117 +1,85 @@ -""" -IO Resilience and Robustness Tests - -Tests edge cases common in OneDrive/cloud environments: -- PermissionError (file locked by another process) -- FileNotFoundError (sync delays) -- Dirty/malformed input data -""" - import unittest -from unittest.mock import patch, MagicMock -import pandas as pd +import os +import shutil +import tempfile +import time +import threading from pathlib import Path - +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager from foton_system.modules.clients.infrastructure.repositories.excel_client_repository import ExcelClientRepository -from foton_system.modules.shared.infrastructure.validators import validate_filename, sanitize_filename - +from foton_system.modules.shared.infrastructure.config.config import Config +from foton_system.modules.shared.domain.exceptions import DatabaseLockError class TestIOResilience(unittest.TestCase): - """Tests for I/O error handling and resilience.""" - - @patch('foton_system.modules.clients.infrastructure.repositories.excel_client_repository.pd.read_excel') - def test_excel_permission_error_is_propagated(self, mock_read): - """PermissionError from Excel read should be propagated with proper logging.""" - mock_read.side_effect = PermissionError("File is locked by OneDrive") - - repo = ExcelClientRepository() + """ + Tests the system's resilience to file locks (OneDrive/Excel open). + """ + + @classmethod + def setUpClass(cls): + cls.temp_dir = Path(tempfile.gettempdir()) / "foton_io_test" + if cls.temp_dir.exists(): + shutil.rmtree(cls.temp_dir) + cls.temp_dir.mkdir(parents=True) + + PathManager._sandbox_dir = cls.temp_dir + PathManager.set_sandbox_mode(True) + PathManager.ensure_directories() + + config = Config() + config.set('caminho_baseDados', str(PathManager.get_app_data_dir() / "baseDados_io.xlsx")) + config.save() + + cls.repo = ExcelClientRepository() + + def test_excel_lock_retry_mechanism(self): + """ + Simulates an Excel file lock and verifies the retry with backoff. + """ + df = self.repo.get_clients_dataframe() + db_path = PathManager.get_app_data_dir() / "baseDados_io.xlsx" + + # 1. Make file read-only to trigger PermissionError + import stat + os.chmod(db_path, stat.S_IREAD) # Set Read-Only - with self.assertRaises(PermissionError): - repo.get_clients_dataframe() - - @patch('foton_system.modules.clients.infrastructure.repositories.excel_client_repository.pd.read_excel') - def test_excel_file_not_found_raises(self, mock_read): - """FileNotFoundError from missing Excel should be propagated.""" - mock_read.side_effect = FileNotFoundError("Excel not found") - - repo = ExcelClientRepository() - - with self.assertRaises(FileNotFoundError): - repo.get_clients_dataframe() - - @patch('foton_system.modules.clients.infrastructure.repositories.excel_client_repository.Config') - def test_folder_creation_handles_permission_error(self, MockConfig): - """Folder creation should propagate PermissionError gracefully.""" - mock_config = MagicMock() - mock_config.base_pasta_clientes = Path('/fake/clients') - MockConfig.return_value = mock_config + # 2. Try to save while locked in a separate thread so we can release it + results = {"success": False, "duration": 0, "error": None} - repo = ExcelClientRepository() + def attempt_save(): + start_time = time.time() + try: + self.repo.save_clients(df) + results["duration"] = time.time() - start_time + results["success"] = True + except Exception as e: + results["error"] = e + + save_thread = threading.Thread(target=attempt_save) + save_thread.start() + + # Wait a bit, then restore write permission + # The backoff is: 0.5, 1.0, 2.0 + # By waiting 0.7s, the first retry (at 0.5s) might still fail, + # but the second one (at 0.5 + 1.0 = 1.5s) should succeed. + time.sleep(0.7) + os.chmod(db_path, stat.S_IWRITE) # Restore Write - with patch.object(Path, 'mkdir', side_effect=PermissionError("Access denied")): - with self.assertRaises(PermissionError): - repo.create_folder(Path('/protected/folder')) - - -class TestValidatorRobustness(unittest.TestCase): - """Tests for filename validation edge cases.""" - - def test_validate_filename_rejects_all_invalid_chars(self): - """All Windows-invalid characters should be rejected.""" - invalid_chars = '<>:"/\\|?*' - for char in invalid_chars: - with self.subTest(char=char): - self.assertFalse(validate_filename(f"test{char}name")) - - def test_validate_filename_accepts_unicode(self): - """Unicode characters (accents, emojis) should be accepted.""" - self.assertTrue(validate_filename("João 🏗️ Arquiteto")) - self.assertTrue(validate_filename("Résidence Étoile")) - self.assertTrue(validate_filename("中文客户")) - - def test_validate_filename_rejects_reserved_names(self): - """Windows reserved names should be rejected.""" - reserved = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM9', 'LPT1', 'LPT9'] - for name in reserved: - with self.subTest(name=name): - self.assertFalse(validate_filename(name)) - - def test_validate_filename_accepts_reserved_as_substring(self): - """Reserved names as part of larger name should be accepted.""" - self.assertTrue(validate_filename("CONEXÃO")) - self.assertTrue(validate_filename("01_AUX_Folder")) - self.assertTrue(validate_filename("LPT1_Extended")) - - def test_sanitize_removes_invalid_chars(self): - """Sanitizer should strip all invalid characters.""" - result = sanitize_filename("Client<>:Name/Test\\Bad|Chars?*End") - self.assertEqual(result, "ClientNameTestBadCharsEnd") - - def test_sanitize_strips_dots_and_spaces(self): - """Sanitizer should strip leading/trailing dots and spaces.""" - self.assertEqual(sanitize_filename(" .Hidden.File. "), "Hidden.File") - self.assertEqual(sanitize_filename("...test..."), "test") - - -class TestDirtyDataHandling(unittest.TestCase): - """Tests for handling corrupted/malformed data.""" - - def test_empty_string_validation(self): - """Empty strings should be rejected.""" - self.assertFalse(validate_filename("")) - self.assertFalse(validate_filename(None)) - - def test_whitespace_only_validation(self): - """Whitespace-only strings should be sanitized to empty.""" - result = sanitize_filename(" ") - self.assertEqual(result, "") - - def test_very_long_filename_validation(self): - """Very long filenames should pass validation (Windows handles truncation).""" - long_name = "A" * 300 - # validate_filename only checks for invalid chars, not length - self.assertTrue(validate_filename(long_name)) - + save_thread.join() + + # 3. Validations + if results["error"]: + self.fail(f"O sistema falhou com erro: {results['error']}") + + self.assertTrue(results["success"], "O sistema falhou em salvar o arquivo após a liberação do lock.") + self.assertGreater(results["duration"], 1.5, f"Deveria ter esperado pelo menos uma tentativa de retry. Durou {results['duration']:.2f}s") + print(f"\n✅ Retry bem-sucedido após {results['duration']:.2f}s") + + @classmethod + def tearDownClass(cls): + PathManager.set_sandbox_mode(False) + if cls.temp_dir.exists(): + shutil.rmtree(cls.temp_dir) if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_mcp_server.py b/tests/unit/test_mcp_server.py index 08269d8..626537d 100644 --- a/tests/unit/test_mcp_server.py +++ b/tests/unit/test_mcp_server.py @@ -1,71 +1,32 @@ -""" -MCP Server Tests with Mocked Core Dependencies - -Tests MCP helper functions and tool behaviors without real MCP runtime. -Uses simplified patching to avoid import order issues. -""" - import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from pathlib import Path - -class TestMCPGetClientPath(unittest.TestCase): - """Tests for _get_client_path helper function.""" - - def test_returns_existing_path(self): - """Returns path when client folder exists.""" - with patch('foton_system.interfaces.mcp.foton_mcp._get_config') as mock_config: - mock_cfg = MagicMock() - mock_cfg.base_pasta_clientes = Path('/fake/clients') - mock_config.return_value = mock_cfg - - from foton_system.interfaces.mcp.foton_mcp import _get_client_path - - with patch.object(Path, 'exists', return_value=True): - result = _get_client_path('TestClient') - - self.assertIn('TestClient', str(result)) - - def test_prevents_directory_traversal(self): - """Malicious paths are sanitized.""" - with patch('foton_system.interfaces.mcp.foton_mcp._get_config') as mock_config: - mock_cfg = MagicMock() - mock_cfg.base_pasta_clientes = Path('/fake/clients') - mock_config.return_value = mock_cfg - - from foton_system.interfaces.mcp.foton_mcp import _get_client_path - - # Path.name sanitizes directory traversal - result_path = Path('../../../etc/passwd') - sanitized = result_path.name # Should be 'passwd' - self.assertEqual(sanitized, 'passwd') - - class TestMCPRegistrarFinanceiro(unittest.TestCase): """Tests for registrar_financeiro tool.""" - def test_success_returns_auditado(self): + @patch('foton_system.core.ops.op_finance_entry.OpFinanceEntry') + def test_success_returns_auditado(self, MockOp): """Successful call returns message with 'Auditado'.""" - with patch('foton_system.interfaces.mcp.foton_mcp.OpFinanceEntry') as MockOp: - mock_op = MagicMock() - mock_op.execute.return_value = {'message': 'Entry added successfully'} - MockOp.return_value = mock_op - - from foton_system.interfaces.mcp.foton_mcp import registrar_financeiro - - result = registrar_financeiro('TestClient', 'Description', 100.0, 'ENTRADA') - - self.assertIn('Auditado', result) - - def test_import_error_handled(self): - """ImportError returns descriptive error message.""" - with patch('foton_system.interfaces.mcp.foton_mcp.OpFinanceEntry', side_effect=ImportError("Module not found")): - from foton_system.interfaces.mcp.foton_mcp import registrar_financeiro - - result = registrar_financeiro('TestClient', 'Desc', 100.0) - - self.assertIn('Erro', result) + mock_op = MagicMock() + mock_op.execute.return_value = {'message': 'Entry added successfully'} + MockOp.return_value = mock_op + + from foton_system.interfaces.mcp.foton_mcp import registrar_financeiro + + result = registrar_financeiro('TestClient', 'Description', 100.0, 'ENTRADA') + + self.assertIn('Auditado', result) + + @patch('foton_system.core.ops.op_finance_entry.OpFinanceEntry') + def test_import_error_handled(self, MockOp): + """Errors in OP return descriptive error message.""" + MockOp.side_effect = Exception("Module not found") + from foton_system.interfaces.mcp.foton_mcp import registrar_financeiro + + result = registrar_financeiro('TestClient', 'Desc', 100.0) + + self.assertIn('Erro', result) class TestMCPConsultarFinanceiro(unittest.TestCase): @@ -73,24 +34,18 @@ class TestMCPConsultarFinanceiro(unittest.TestCase): def test_returns_formatted_balance(self): """Returns properly formatted balance string.""" - with patch('foton_system.interfaces.mcp.foton_mcp._get_client_path') as mock_path, \ - patch('foton_system.interfaces.mcp.foton_mcp.CSVFinanceRepository') as MockRepo, \ - patch('foton_system.interfaces.mcp.foton_mcp.FinanceService') as MockService: - - mock_path.return_value = Path('/fake/client') - mock_service = MagicMock() - mock_service.get_summary.return_value = { - 'saldo': 1500.50, - 'total_entradas': 2000.00, - 'total_saidas': 499.50 - } - MockService.return_value = mock_service + with patch('foton_system.interfaces.mcp.foton_mcp._get_factory') as mock_factory: + mock_svc = MagicMock() + mock_svc.get_summary.return_value = MagicMock( + success=True, total_income=1500.50, total_expenses=499.50, balance=1001.00 + ) + mock_factory.return_value.get_finance_service.return_value = mock_svc from foton_system.interfaces.mcp.foton_mcp import consultar_financeiro result = consultar_financeiro('TestClient') - self.assertIn('1500.50', result) + self.assertIn('1001.00', result) self.assertIn('Saldo', result) @@ -99,23 +54,18 @@ class TestMCPListarTemplates(unittest.TestCase): def test_returns_pptx_and_docx_lists(self): """Returns both PPTX and DOCX template lists.""" - with patch('foton_system.interfaces.mcp.foton_mcp._get_config') as mock_config, \ - patch('foton_system.interfaces.mcp.foton_mcp.DocumentService') as MockDocService, \ - patch('foton_system.interfaces.mcp.foton_mcp.PythonDocxAdapter'), \ - patch('foton_system.interfaces.mcp.foton_mcp.PythonPPTXAdapter'): + with patch('foton_system.interfaces.mcp.foton_mcp._get_config') as mock_config: mock_cfg = MagicMock() mock_cfg.templates_path = MagicMock() - mock_cfg.templates_path.mkdir = MagicMock() + mock_cfg.templates_path.exists.return_value = True + mock_cfg.templates_path.name = "KIT DOC" + # Mock glob to return some files + mock_file = MagicMock() + mock_file.name = "template.pptx" + mock_cfg.templates_path.glob.side_effect = [[mock_file], [mock_file]] mock_config.return_value = mock_cfg - mock_service = MagicMock() - mock_service.list_templates.side_effect = [ - ['prop1.pptx', 'prop2.pptx'], - ['contract.docx'] - ] - MockDocService.return_value = mock_service - from foton_system.interfaces.mcp.foton_mcp import listar_templates result = listar_templates() @@ -127,85 +77,57 @@ def test_returns_pptx_and_docx_lists(self): class TestMCPGerarDocumento(unittest.TestCase): """Tests for gerar_documento tool.""" - def test_success_returns_path(self): + @patch('foton_system.core.ops.op_doc_gen.OpGenerateDocument') + def test_success_returns_path(self, MockOp): """Successful generation returns output path.""" - with patch('foton_system.interfaces.mcp.foton_mcp.OpGenerateDocument') as MockOp: - mock_op = MagicMock() - mock_op.execute.return_value = {'output_path': '/path/to/document.docx'} - MockOp.return_value = mock_op - - from foton_system.interfaces.mcp.foton_mcp import gerar_documento - - result = gerar_documento('TestClient', 'template.docx', {}) - - self.assertIn('document.docx', result) - self.assertIn('Auditado', result) - - def test_error_returns_message(self): + mock_op = MagicMock() + mock_op.execute.return_value = {'output_path': '/path/to/document.docx'} + MockOp.return_value = mock_op + + from foton_system.interfaces.mcp.foton_mcp import gerar_documento + + result = gerar_documento('TestClient', 'template.docx', {}) + + self.assertIn('document.docx', result) + self.assertIn('Auditado', result) + + @patch('foton_system.core.ops.op_doc_gen.OpGenerateDocument') + def test_error_returns_message(self, MockOp): """Errors return descriptive message.""" - with patch('foton_system.interfaces.mcp.foton_mcp.OpGenerateDocument') as MockOp: - MockOp.side_effect = ImportError("Module not found") - - from foton_system.interfaces.mcp.foton_mcp import gerar_documento - - result = gerar_documento('TestClient', 'missing.docx', {}) - - self.assertIn('Erro', result) + MockOp.side_effect = Exception("Gen failed") + from foton_system.interfaces.mcp.foton_mcp import gerar_documento + result = gerar_documento('TestClient', 'template.docx') + self.assertIn('Erro POP', result) -class TestMCPConsultarConhecimento(unittest.TestCase): - """Tests for consultar_conhecimento tool.""" +class TestMCPGetClientPath(unittest.TestCase): + """Tests for client path resolution proxy.""" - @patch('foton_system.core.ops.op_query_knowledge.OpQueryKnowledge') - def test_returns_formatted_results(self, MockOpClass): - """Returns formatted knowledge results.""" - mock_op = MagicMock() - mock_op.execute.return_value = { - 'status': 'FOUND', - 'query': 'Test query', - 'results': [ - {'document': 'Document content 1', 'source': 'doc1.md', 'score': 0.85}, - {'document': 'Document content 2', 'source': 'doc2.md', 'score': 0.72} - ], - 'total': 2 - } - MockOpClass.return_value = mock_op + def test_returns_existing_path(self): + """Proxies resolution to client service.""" + with patch('foton_system.interfaces.mcp.foton_mcp._get_factory') as mock_factory: + mock_svc = MagicMock() + mock_svc.resolve_client_path.return_value = Path("/base/CLIENTE") + mock_factory.return_value.get_client_service.return_value = mock_svc + + from foton_system.interfaces.mcp.foton_mcp import _resolve_client_path + result = _resolve_client_path(Path("/base"), "CLIENTE", MagicMock()) + self.assertEqual(result, Path("/base/CLIENTE")) - from foton_system.interfaces.mcp.foton_mcp import consultar_conhecimento - result = consultar_conhecimento('Test query') - self.assertIn('doc1.md', result) - self.assertIn('Document content 1', result) +class TestMCPConsultarConhecimento(unittest.TestCase): + """Tests for semantic search tool.""" @patch('foton_system.core.ops.op_query_knowledge.OpQueryKnowledge') - def test_empty_results_returns_message(self, MockOpClass): + def test_empty_results_returns_message(self, MockOp): """Empty results return appropriate message.""" mock_op = MagicMock() - mock_op.execute.return_value = { - 'status': 'EMPTY', - 'query': 'Unknown query', - 'results': [], - 'total': 0 - } - MockOpClass.return_value = mock_op - + mock_op.execute.return_value = {"status": "EMPTY"} + MockOp.return_value = mock_op + from foton_system.interfaces.mcp.foton_mcp import consultar_conhecimento - result = consultar_conhecimento('Unknown query') - - self.assertIn('Nenhum conhecimento', result) - - def test_import_error_handled(self): - """ImportError for missing dependencies is handled gracefully.""" - from foton_system.interfaces.mcp.foton_mcp import consultar_conhecimento - - # When OpQueryKnowledge import fails, the function catches ImportError - with patch('foton_system.core.ops.op_query_knowledge.OpQueryKnowledge', - side_effect=ImportError("No chromadb")): - result = consultar_conhecimento('Test') - - # Should handle gracefully (either RAG indisponível or error message) - self.assertTrue('RAG' in result or 'Erro' in result) - + result = consultar_conhecimento("test") + self.assertIn('No relevant knowledge found', result) if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_path_manager_sandbox.py b/tests/unit/test_path_manager_sandbox.py new file mode 100644 index 0000000..a167e79 --- /dev/null +++ b/tests/unit/test_path_manager_sandbox.py @@ -0,0 +1,57 @@ +import unittest +import os +from pathlib import Path +from foton_system.modules.shared.infrastructure.services.path_manager import PathManager + +class TestPathManagerSandbox(unittest.TestCase): + """ + TDD for Sandbox Mode Redirection in PathManager. + """ + + def setUp(self): + # Reset state before each test + PathManager.set_sandbox_mode(False) + + def test_default_mode_returns_standard_paths(self): + """Standard paths should be returned when sandbox is OFF.""" + app_data = PathManager.get_app_data_dir() + if os.name == 'nt': + self.assertIn("AppData", str(app_data)) + else: + self.assertTrue(str(app_data).startswith(str(Path.home()))) + + def test_sandbox_mode_redirects_paths(self): + """Paths should be redirected to a temporary directory when sandbox is ON.""" + PathManager.set_sandbox_mode(True) + + sandbox_dir = PathManager.get_sandbox_dir() + app_data = PathManager.get_app_data_dir() + projects = PathManager.get_user_projects_dir() + + self.assertTrue(str(app_data).startswith(str(sandbox_dir))) + self.assertTrue(str(projects).startswith(str(sandbox_dir))) + self.assertIn("foton_sandbox", str(sandbox_dir)) + + def test_sandbox_dir_is_volatile(self): + """The sandbox directory should be inside the system temp folder.""" + PathManager.set_sandbox_mode(True) + sandbox_dir = PathManager.get_sandbox_dir() + + import tempfile + sys_temp = Path(tempfile.gettempdir()) + + self.assertTrue(str(sandbox_dir).startswith(str(sys_temp))) + + def test_switching_off_restores_paths(self): + """Paths should return to normal after turning off sandbox.""" + PathManager.set_sandbox_mode(True) + PathManager.set_sandbox_mode(False) + + app_data = PathManager.get_app_data_dir() + if os.name == 'nt': + self.assertIn("AppData", str(app_data)) + else: + self.assertNotIn("foton_sandbox", str(app_data)) + +if __name__ == '__main__': + unittest.main() diff --git a/validate_foton_ai.py b/validate_foton_ai.py new file mode 100644 index 0000000..1081d5d --- /dev/null +++ b/validate_foton_ai.py @@ -0,0 +1,71 @@ +import sys +import yaml +from pathlib import Path + +def validate_skill(): + print("--- Validando Estrutura de Skill ---") + skill_path = Path(r'C:\Users\Lucas\OneDrive\LAMP_ARQUITETURA\.gemini\skills\foton-architecture\SKILL.md') + if not skill_path.exists(): + print(f"❌ Erro: SKILL.md não encontrado em {skill_path}") + return False + + try: + content = skill_path.read_text(encoding='utf-8') + if not content.startswith('---'): + print("❌ Erro: SKILL.md não inicia com frontmatter YAML (---)") + return False + + # Extrair e validar YAML + parts = content.split('---') + if len(parts) < 3: + print("❌ Erro: SKILL.md frontmatter malformado") + return False + + metadata = yaml.safe_load(parts[1]) + required = ['name', 'description'] + for field in required: + if field not in metadata: + print(f"❌ Erro: Campo '{field}' ausente no YAML") + return False + + print(f"✅ Skill '{metadata['name']}' validada com sucesso.") + return True + except Exception as e: + print(f"❌ Erro ao validar Skill: {e}") + return False + +def validate_mcp_source(): + print("\n--- Validando Código-Fonte do MCP ---") + mcp_path = Path(r'C:\Users\Lucas\OneDrive\LAMP_ARQUITETURA\fotonSystem\foton_system\interfaces\mcp\foton_mcp.py') + if not mcp_path.exists(): + print(f"❌ Erro: foton_mcp.py não encontrado em {mcp_path}") + return False + + try: + content = mcp_path.read_text(encoding='utf-8') + required_tools = ['configurar_agente', 'gerar_documento', 'validar_template', 'listar_clientes'] + + for t in required_tools: + # Verifica se existe o decorador @mcp.tool() seguido da definição da função + pattern = rf'@mcp\.tool\(.*?\)\s+def {t}' + import re + if not re.search(pattern, content, re.DOTALL): + print(f"❌ Erro: Definição da ferramenta '{t}' não encontrada com padrão @mcp.tool") + return False + + print(f"✅ Todas as {len(required_tools)} ferramentas essenciais foram encontradas e estão decoradas corretamente.") + return True + except Exception as e: + print(f"❌ Erro ao validar fonte do MCP: {e}") + return False + +if __name__ == "__main__": + s_ok = validate_skill() + m_ok = validate_mcp_source() + if s_ok and m_ok: + print("\n🚀 Foton está pronto para operação AI-First!") + sys.exit(0) + else: + print("\n🛑 Falha na validação de infraestrutura AI.") + sys.exit(1) + diff --git a/version.txt b/version.txt index 1cc5f65..867e524 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.1.0 \ No newline at end of file +1.2.0 \ No newline at end of file