Pular para o conteúdo
Development

Construindo o Greenhouse: o motor de regras que veio antes de o app existir

Por Victor Da Luz
rusttaurisqlitedev-loggreenhouse

Greenhouse é um gerenciador de projetos criativos, mas não do tipo que só guarda tarefas. Ele impõe um processo: capturar ideias todo dia, alternar entre projetos em vez de hiperfocar em um só, cumprir um resfriamento obrigatório de 7 dias depois de cada sessão de trabalho, nunca apagar nada. O método é adaptado do sistema de processo criativo de Mike Monday em Make Music Your Life, generalizado para além da música, num motor neutro de domínio.

O primeiro dia foi o PRD e a inicialização do repositório. O primeiro dia também foi a primeira issue no board: o motor de regras. Nenhuma UI existia ainda. Nenhuma tela, nenhuma janela, nada pra clicar. Só Rust.

Essa ordem foi deliberada. O PRD que escrevi naquela mesma manhã descreve o motor como “uma calculadora de estado, não um porteiro”: algo que responde “qual é minha situação agora?” e mostra avisos, mas nunca bloqueia uma ação de vez. Se a lógica de resfriamento, os timers de maturação e as regras do vault vivessem atrás de uma UI desde o início, seria fácil demais deixar a interface silenciosamente virar a fonte da verdade em vez das regras. Construir o motor primeiro, sem UI pra se apoiar, forçou cada regra a ser explícita e testável isoladamente. Na issue do Plane que escrevi pra isso, cheguei a marcar como prioridade #1 “a pedido do stakeholder”, que era só eu mesmo, mas escrever assim manteve o sequenciamento honesto.

O commit que entregou isso é um scaffold completo do Tauri v2 envolvendo um núcleo Rust puro:

  • config.rs: os structs StageConfig e Config, com os padrões do template “plant” embutidos: resfriamento de 7 dias, maturação de ideia de 7 dias, maturação de vault de 14 dias, carregados de config.yaml.
  • state.rs: um enum ItemStatus, um tipo Item espelhando a linha do banco, e derive_item_state: uma função pura que recebe um item e um timestamp e devolve seu estado derivado. Sem efeitos colaterais, sem leitura de relógio dentro da função; quem chama passa o “agora” como parâmetro.
  • db.rs: o schema SQLite, duas tabelas (items e touches), CRUD via o build embutido do rusqlite, então não há dependência de SQLite do sistema pra gerenciar.
  • touch.rs: record_touch, a única função que inicia um resfriamento, além de vault_item e promote_idea.
  • daily.rs: derive_daily_state, que consolida tudo num DailyState: a lista de trabalho do dia, quais ideias estão vencendo, o que precisa de uma decisão de revisão de vault, se algo foi capturado hoje, e um aviso de estoque baixo se o pipeline estiver secando.

Vinte e dois testes unitários espalhados por esses cinco módulos, cada um fixado num timestamp constante em vez de chamar o relógio do sistema. Lógica de resfriamento e maturação é exatamente o tipo de código que parece correto e depois falha de forma intermitente seis meses depois porque um teste rodou perto de uma virada de dia. Fixar o “agora” como entrada, e não como um global ambiente, evita esse problema desde o primeiro teste.

Aqui está a parte que eu não esperava escrever: a mensagem do commit termina com “Requires rustup to build/test - Rust not yet installed on dev machine” (precisa do rustup pra buildar/testar, Rust ainda não instalado na máquina de desenvolvimento). Escrevi um motor de regras inteiro, cinco módulos, vinte e dois testes, sem conseguir compilar uma linha sequer. Tudo lendo o PRD e pensando nas transições de estado no papel.

O commit seguinte, 23 minutos depois, se chama “fix(engine): all 22 tests passing” (todos os 22 testes passando), o que já conta como foi. Assim que o Rust foi de fato instalado, duas coisas quebraram na hora. Primeiro, o código de bootstrap do Tauri não compilava para cargo test sem ícones do app existindo em disco, então a própria lib falhava até eu adicionar ícones RGBA de placeholder. Segundo, os helpers de teste do banco de dados abriam um arquivo SQLite real dentro de um TempDir e deixavam o diretório ser descartado enquanto a conexão ainda o usava, uma corrida de tempo de vida que não tem garantia de falhar do mesmo jeito duas vezes. Trocar todos os testes para um banco em memória resolveu de vez, em vez de só remendar uma execução instável. Nenhum dos dois problemas apareceu no design no papel. Os dois só apareceram quando o Rust de verdade rodou os testes.

A seguir: a shell do assistente de onboarding, a primeira tela que alguém vai realmente ver. O motor está ali parado desde o primeiro dia, esperando algo pra chamá-lo.

Leitura relacionada

Development

Construindo o Greenhouse: o vault em disco

O layout de pastas que sobrevive ao app: zonas versus estágios, uma config que se recusa a sobrescrever, um arquivo retrato do banco de dados, e o parâmetro que não fazia nada.

Ler
Development

O scan que se ofereceu para importar a si mesmo

Um scanner de importação que encontrou a própria estrutura interna do app, e depois voltou a oferecer uma pasta que tinha acabado de importar, duas versões da mesma conversa que faltava entre camadas.

Ler

Você também pode achar útil

Airalo

eSIM Airalo

eSIM de dados local para viagens - sem necessidade de trocar um SIM físico.

Este é meu link de indicação da Airalo. Você recebe um desconto no seu primeiro eSIM e eu ganho crédito da Airalo para o meu.

Saiba mais
Proton

Proton VPN

VPN comercial com filtragem NetShield e interruptor de desligamento automático.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais
Proton

Proton Mail

E-mail criptografado de ponta a ponta, com arquitetura de acesso zero.

Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).

Saiba mais