MÓDULO 6.3

💎 Dicas avançadas

As regras que CI te empurra na cara: i18n parity em 12 locales, zero dynamic imports, file size ≤500, controller-only exposure, light mod.rs, event bus owned, webview zero-injection.

7
Tópicos
40
Minutos
CI
Nível
Macete
Tipo
1

🌍 i18n strict — parity em 12 locales

Toda string visível no app passa por useT(). Não existe string hard-coded em JSX, label=, placeholder= ou aria-label=. Adicionar uma chave é mais trabalhoso do que parece — e o CI vai pegar se você esquecer.

⚠️ Workflow obrigatório (uma chave nova)

  1. Adicionar em app/src/lib/i18n/en.ts (source of truth)
  2. Adicionar no chunk correspondente en-N.ts (1 a 5)
  3. Adicionar a MESMA chunk em TODOS os 12 locales (pt-N.ts, es-N.ts, …)
  4. Pode usar valor inglês como placeholder em locales não-inglês — tradutores ajustam depois

Esqueceu um locale? pnpm i18n:check falha no CI.

🗺️ Os 12 locales

ar
bn
de
es
fr
hi
id
it
ko
pt
ru
zh-CN

✓ Correto

const t = useT();
<button aria-label={t('chat.send')}>
  {t('chat.send_label')}
</button>

✗ Errado

<button aria-label="Send">
  Send
</button>
// CI: i18n violation
API
useT()
Source
en.ts + en-N.ts
Locales
12 + parity gate
Check
pnpm i18n:check
2

🚫 NO dynamic imports em produção

import(), React.lazy(() => import(...)) e await import(...) são banidos em app/src de produção. Bundle precisa ser previsível — sem chunk loading runtime que pode falhar offline.

✗ Banido

// ❌ Dynamic import direto
const mod = await import('./heavy');

// ❌ React.lazy
const Heavy = React.lazy(
  () => import('./Heavy')
);

// ❌ Lazy route
const Settings = lazy(
  () => import('./Settings')
);

✓ Aceito

// ✅ Static import
import { Heavy } from './Heavy';

// ✅ Guard em runtime
try {
  Heavy.maybeInit();
} catch { /* opt-out */ }

// ✅ import type sempre OK
import type { Foo } from './types';

💡 Exceções permitidas

  • • Harness Vitest em *.test.ts, __tests__/, test/setup.ts
  • • Ambient typeof import('…') em arquivos .d.ts
  • • Config files (ex.: tailwind.config.js com JSDoc)

Fora dessas, é bug. Para "heavy optional paths" — static import + try/catch ou runtime check no call site.

🤔 Por que essa regra existe

O app é desktop (Tauri). Não tem CDN para chunk lazy. Se a chunk não carregar (corrupção, FS issue), a feature quebra silenciosamente. Static bundle = previsibilidade. Trade-off: bundle maior, comportamento determinístico.

Banido
import() / lazy()
Razão
Bundle previsível
Exceção
tests + .d.ts + config
Sempre OK
import type
3

📏 File size ≤ ~500 linhas

Filosofia Unix do projeto: módulos pequenos com responsabilidade afiada compostos por boundaries claros. Acima de ~500 linhas, dividir. Cada arquivo deve caber na cabeça.

🔧 Padrão de divisão por domínio Rust

src/openhuman/memory/
├── mod.rs        # só declara módulos + re-exports (light!)
├── types.rs      # structs, enums, traits
├── store.rs      # persistência
├── ops.rs        # operações (lógica)
├── rpc.rs        # handlers JSON-RPC
├── schemas.rs    # ControllerSchema
└── bus.rs        # EventHandler subscribers

✓ Bom sinal

  • 200-400 linhas por arquivo
  • Cada arquivo tem 1 responsabilidade nomeável
  • Você abre o arquivo certo sem hesitação

✗ Sinal de alerta

  • 900+ linhas com tudo misturado
  • Nome do arquivo é genérico ("utils")
  • Você usa Ctrl+F antes de ler
Alvo
≤500 linhas
Filosofia
Unix small + sharp
Divisão
ops/store/types
Sinal
Crescimento orgânico
4

🎛️ Controller-only exposure

Para expor features ao CLI e JSON-RPC, registre via controller registry. Branches manuais em src/core/cli.rs ou src/core/jsonrpc.rs são proibidos — reintroduzem o acoplamento que controllers vieram resolver.

📋 Checklist novo controller

  1. Criar src/openhuman/<domain>/schemas.rs com schemas, all_controller_schemas, all_registered_controllers
  2. Adicionar handle_* fns delegando para rpc.rs
  3. Em mod.rs: mod schemas; + re-export com prefixo do domínio
  4. Wire em src/core/all.rs
  5. Remover qualquer branch antigo de src/core/dispatch.rs

📐 Como fica em mod.rs

// src/openhuman/cron/mod.rs
mod ops;
mod rpc;
mod schemas;
mod store;
mod types;
mod bus;

pub use schemas::{
    all_controller_schemas as all_cron_controller_schemas,
    all_registered_controllers as all_cron_registered_controllers,
};

🚫 NÃO faça isso

// src/core/dispatch.rs
match method {
    "openhuman.cron_list" => handle_cron_list(...),  // ❌ NUNCA
    "openhuman.cron_create" => handle_cron_create(...), // ❌ NUNCA
    // ...
}

Acopla core/ ao domínio. Cresce sem limite. Controller registry existe pra remover isso.

Registry
all_controller_schemas
Wire
src/core/all.rs
Banido
branch em dispatch.rs
Bônus
CLI grátis
5

📁 Light mod.rs — só índice

Regra: mod.rs declara módulos e re-exports. Nada mais. Lógica vai em ops.rs, store.rs, types.rs. Novos arquivos em src/openhuman/ root também são proibidos — tudo em subdiretório dedicado.

💡 Por quê

mod.rs é o "índice" do domínio. Quando alguém abre o módulo, quer ver imediatamente o que existe lá dentro. mod.rs de 600 linhas com lógica esconde a estrutura — você tem que ler tudo pra saber o que tem.

✓ mod.rs ideal

// src/openhuman/skills/mod.rs
mod inject;
mod ops_create;
mod ops_discover;
mod ops_install;
mod ops_parse;
mod schemas;
mod types;

pub use types::*;
pub use schemas::*;

✗ mod.rs gordo

mod helpers;

pub struct Skill { ... }     // ❌
pub fn install(...) { ... }  // ❌
async fn do_thing() { ... }  // ❌
// 400 linhas misturadas

⚠️ Layout obrigatório

Nova funcionalidade vai em subdiretório dedicado: openhuman/<domain>/mod.rs + siblings. Não criar *.rs standalone em src/openhuman/ root. dev_paths.rs e util.rs são grandfathered — não viram template.

Conteúdo
só mod + pub use
Lógica
ops/store/types
Layout
subdir por domínio
Banido
.rs em openhuman/
6

📨 Event bus — owned types, não borrows

Tipos passados pelo event bus precisam ser Send + 'static e ownedArc, mpsc::Sender, oneshot::Sender, owned fields. Não Serialize (é in-process, não JSON-RPC), não borrows (lifetimes não vivem em canal).

📋 Native request/response típico

// 1. Tipos owned, sem borrows
pub struct SendMessageRequest {
    pub channel_id: ChannelId,           // owned
    pub text: String,                    // owned
    pub reply: oneshot::Sender<Result>,  // owned
}

// 2. Registrar no startup
register_native_global("channel.send", Arc::new(ChannelSendHandler::new()));

// 3. Chamar de qualquer lugar
let (tx, rx) = oneshot::channel();
request_native_global("channel.send", SendMessageRequest {
    channel_id, text, reply: tx
}).await?;
let result = rx.await?;

✓ Tipos válidos no payload

  • Arc<T> — shared ownership
  • String, Vec<T> — owned
  • mpsc::Sender / oneshot::Sender
  • Trait objects Arc<dyn Trait>

✗ Inválidos

  • &str, &[T] — borrows
  • Qualquer lifetime 'a
  • Tipos não-Send (Rc, RefCell)
  • Esperar Serialize (não é JSON)

💡 Naming convention

Método sempre "<domain>.<verb>" — ex.: "channel.send", "memory.upsert", "skills.install". Subscriber tem name() retornando "<domain>::<purpose>" (ex.: "cron::delivery").

Trait
Send + 'static
Naming
domain.verb
Reply
oneshot::Sender
Não
Serialize / borrows
7

🛡️ Webview zero-injection — sem JS novo

Webviews CEF que carregam origens de terceiros (whatsapp, slack, telegram, discord, browserscan) não recebem nenhum JavaScript novo. Tudo via CDP nativo. Esta regra é intencional — surface de ataque + fragilidade de scraping.

🚫 Proibições

  • • Novos arquivos .js em app/src-tauri/src/webview_accounts/
  • • Append em build_init_script ou RUNTIME_JS
  • • Scripts via CDP Page.addScriptToEvaluateOnNewDocument ou Runtime.evaluate
  • • Plugins que injetam JS por padrão (ex.: tauri-plugin-opener com init-iife.js)

📋 Onde nova lógica vai

  • CEF handlerson_navigation, on_new_window, LoadHandler::OnLoadStart, CefRequestHandler::*
  • CDP nativoNetwork.*, Emulation.*, Input.*, Page.* dos *_scanner/ per-provider
  • IPC Rust-side — nunca cruza pro renderer

⚠️ Plugins Tauri: audite sempre

tauri-plugin-opener injeta init-iife.js com um global click listener. Para desligar:

// app/src-tauri/src/lib.rs
tauri::Builder::default()
    .plugin(
        tauri_plugin_opener::Builder::new()
            .open_js_links_on_click(false)  // ✅ opt-out
            .build()
    )

Qualquer plugin novo: audite se chama js_init_script. Se sim — configure pra não injetar.

💡 Limite real, não excessivo

Se uma feature realmente não dá pra fazer sem injetar (ex.: interceptar clique que o JS da página preventDefaulta), o correto é surface da limitação — não shippar init script. Legado em gmail/linkedin/google-meet é grandfathered: deve encolher, não crescer.

Regra
zero JS novo
Via
CDP nativo
Audite
plugins Tauri
Legado
encolhe, não cresce

🧰 Resumo do Módulo

i18n strictuseT() sempre, 12 locales em parity, pnpm i18n:check.
NO dynamic importsimport()/lazy() proibidos em prod.
File size ≤ ~500 — splitar cedo, ops/store/types.
Controller-only — registry, nada em dispatch.rs.
Light mod.rs — só mod + pub use; subdir por domínio.
Event bus ownedSend + 'static, sem borrows, sem Serialize.
Webview zero-injection — CDP nativo, plugins auditados.

🎉 Trilha 6 completa!

Você passou pelo bestiário, dominou debug logging e absorveu as dicas avançadas. Agora você sabe troubleshoot OpenHuman — e mais importante, sabe como não cair nas mesmas armadilhas.