Synapse: Business Rules
Documento normativo. Toda implementação que contradiga uma regra deste arquivo está errada e deve ser corrigida (ou a regra reaberta via PR). Cada regra carrega um identificador
BR-Npara citar em commits, testes e PRs.
Status: vigente desde 2026-05-24.
Escopo: v0.x. v1.0 endurece (ver SYN-104).
1. Workspace
BR-1.1: Um workspace é uma pasta com .synapse/ na raiz
Um diretório só é “workspace synapse” se contém .synapse/ como sub-pasta
direta. Workspace::find sobe ancestrais até encontrar; falha com
Error::NoWorkspace ao chegar à raiz do filesystem sem encontrar.
BR-1.2: A descoberta resolve symlinks
Workspace::find canonicaliza o cwd antes de subir, para tratar
corretamente /var ↔ /private/var em macOS e symlinks em geral. Dois
processos no mesmo workspace lógico nunca veem caminhos diferentes do
mesmo arquivo.
BR-1.3: Layout fixo na raiz do workspace
<workspace>/
├── .synapse/ # config + índice; gerenciado pelo CLI
│ ├── config.toml
│ └── index.db
├── projects/<slug>/_meta.md # projeto agrega sub-arquivos
├── people/<slug>.md
├── entities/<slug>.md
├── apis/<slug>.md
├── tasks/<id>.md # global; ou projects/<p>/tasks/<id>.md
└── notes/<slug>.md # global; ou projects/<p>/notes/<slug>.md
Outros diretórios podem coexistir; o CLI os ignora.
BR-1.4: Nada vive só no índice
.synapse/index.db é descartável. Apagar o arquivo + syn reindex
recria estado idêntico. Toda informação do usuário vive em .md
versionável.
BR-1.5: SYNAPSE.md é o cartão de identidade do workspace
Todo workspace possui um arquivo SYNAPSE.md na raiz (não dentro
de .synapse/) com frontmatter (name, description, topics,
tags, language, maintainer, visibility) e corpo livre. É o
manifesto do workspace: a única coisa que viaja com o diretório
quando ele é clonado, sincronizado ou compartilhado.
O workspaces.db global (BR-1.4 / ADR-010) apenas cacheia esses
campos para queries rápidas. Ao apagar o cache + syn ws refresh, o
estado é recuperado da fonte (o arquivo).
syn init bootstrapa o arquivo automaticamente; humanos editam via
syn ws edit; a IA pode propor edições via MCP que entram na fila de
confirmação humana (BR-12.3).
2. Slugs e identidade
BR-2.1: Identidade canônica = slug + tipo
Combinação (type, id) identifica unicamente uma entidade. O id é o
slug (kebab-case ASCII). Dois tipos podem compartilhar o mesmo slug
(projects/alfa e apis/alfa), garantido pela PK composta na tabela
entities.
BR-2.2: Slug é derivado, validado e estável
slugify(input) aplica, em ordem:
- Unicode NFD.
- Remove diacríticos (
̀-ͯ). - Lowercase ASCII.
- Substitui qualquer não-
[a-z0-9]+por-. - Colapsa hífens duplos.
- Remove hífens das pontas.
validate_slug(s) aceita apenas ^[a-z0-9][a-z0-9-]*[a-z0-9]$, com no
mínimo 1 e no máximo 64 caracteres. Slug com 1 caractere deve ser
[a-z0-9]. Inputs que viram slug vazio depois das regras retornam
Error::InvalidSlug.
BR-2.3: Renomear slug é operação explícita
Não há “atualização tácita” via edição manual do arquivo. Renomear é
syn <type> rename old new, que:
- Move o arquivo (e a pasta, no caso de
project). - Reescreve
idno frontmatter. - Atualiza toda referência (memberships, owner, projects[]) em uma
única
Workspace::tx.
BR-2.4: Slugs reservados
Os seguintes nunca podem ser slugs de entidade:
.synapse, _meta, _index, __pycache__, node_modules, target,
qualquer nome iniciado com _.
3. Frontmatter
BR-3.1: Fonte da verdade do frontmatter é YAML
Todo .md começa com bloco YAML entre --- na primeira linha e ---
após. Ausência é erro (Error::MissingFrontmatter).
BR-3.2: Campos comuns obrigatórios em toda entidade
type, id, title, created, updated. Datas em ISO-8601
(YYYY-MM-DD ou YYYY-MM-DDTHH:MM:SSZ).
BR-3.3: id no frontmatter deve casar com o nome do arquivo
Para projects/alfa/_meta.md, id no YAML deve ser alfa. Para
people/maria-silva.md, id deve ser maria-silva. Descasamento é
erro detectado por syn doctor.
BR-3.4: Round-trip preserva ordem dos campos
Reler um arquivo, não modificar, e reescrever deve produzir bytes
idênticos (excluindo eventual ajuste do campo updated, ver BR-3.6).
Diff ruidoso em git destrói confiança no CLI.
BR-3.5: Campos desconhecidos são preservados
Adicionar um campo livre no YAML não causa erro. O parser preserva o campo no round-trip. A validação só falha para campos conhecidos com tipo errado, não para campos desconhecidos.
BR-3.6: updated é gerenciado pelo CLI, created é imutável
Toda operação de escrita atualiza updated para o instante atual em
UTC. created é definido na criação e nunca mais alterado pelo CLI
(usuário pode editar à mão, mas não é incentivado).
BR-3.7: YAML routing único
Toda I/O YAML passa por crate::store::frontmatter. Importar
serde_yaml_ng em qualquer outro módulo viola a regra e quebra o lint.
Justificativa: trocar o crate YAML no futuro deve ser one-file change
(ver ADR-004).
4. Tipos de entidade
BR-4.1: Tipos suportados em v0.1
project, person, entity, api, task, note. Enum EntityType
em crate::model::entity_type é a fonte da verdade desse conjunto.
BR-4.2: Cada tipo tem schema fechado de campos conhecidos
Validação per-type lista campos conhecidos e seus tipos. Campos
desconhecidos passam (BR-3.5). Tipos errados em campos conhecidos
falham com Error::InvalidField { field, expected }.
BR-4.3: Enums fechados por campo
project.status:active | paused | concluded | archived.project.priority:low | medium | high.entity.kind:vendor | customer | internal | partner.api.auth:none | apikey | basic | oauth2 | jwt | mtls.api.status:production | staging | deprecated.task.status:open | in_progress | blocked | concluded.note.kind:decision | runbook | meeting | snippet | reference.
Valores em snake_case inglês no YAML (independente de locale). Tradução vive na camada de apresentação.
BR-4.4: Pessoas usam memberships, não roles
O campo de vínculo pessoa↔projeto chama-se memberships, cada item é
{project: <slug>, role: <free-form>}. role aqui é texto livre
descrevendo a função funcional. O campo roles (do schema original) foi
renomeado em SYN-18: ver ADR-005 e críticas seção 3.3.
title no nível raiz é sempre o nome legível (BR-3.2). O cargo
formal de uma pessoa vai em job_title (“Tech Lead”, “PM”) para evitar
colisão com title.
BR-4.5: Projetos são pastas, não arquivos
projects/<slug>/ é diretório; _meta.md é o manifesto. Permite
agregar tasks/, notes/ específicos do projeto. Os demais tipos são
arquivos diretos.
5. Referências e integridade
BR-5.1: Toda referência usa slug, nunca cópia
Pessoa que atua em três projetos existe uma vez em people/; os
projetos a referenciam por slug. Vendor compartilhado entre projetos
também.
BR-5.2: Foreign keys são reais no índice
Toda conexão SQLite executa PRAGMA foreign_keys = ON. Tabela links
tem FK em from_id e to_id com ON DELETE CASCADE. Tabela tasks
tem FK em project (também CASCADE). Quebra de referência é impedida
na escrita, não detectada depois.
BR-5.3: Deletar entidade reescreve referenciadores
syn person rm maria falha se Maria é dona (owner) de algum projeto
ou API. Não há cascade silencioso na camada de aplicação. O usuário
deve transferir ownership antes; se quiser cascade explícito, usar
--cascade (v0.2).
BR-5.4: syn doctor detecta o que o schema não impede
Inconsistências semânticas (tag órfã, memberships.project apontando
para projeto inexistente quando FK foi desativada manualmente, etc) são
relatadas mas nunca corrigidas automaticamente.
6. Atomicidade e durabilidade
BR-6.1: Toda escrita é all-or-nothing
Workspace::tx(closure) orquestra:
- Abre transação SQLite.
- Escreve arquivos em
*.tmpparalelo aos finais. - Em sucesso: rename atômico de cada
*.tmp→ final, depois commit SQLite. - Em falha: deleta
*.tmp, rollback SQLite.
Não há ponto intermediário onde disco e índice estão divergentes.
BR-6.2: Falha de SQLite após commit do filesystem é proibida
A ordem é: filesystem (rename) → SQLite (commit). Em hardware moderno,
ambos podem falhar; falha no segundo aciona compensação: arquivo é
revertido para versão pré-commit (a *.tmp antiga ou exclusão).
syn doctor detecta órfãos de uma compensação falha.
BR-6.3: Crash mid-tx deixa estado consistente
Se o processo morre entre o rename e o commit, ao reabrir o usuário
observa: arquivos atualizados no disco mas índice antigo. syn doctor
identifica e oferece syn reindex como remediação. Não corrompe o
index.db (SQLite garante).
BR-6.4: syn reindex é idempotente
Rodar duas vezes na mesma árvore produz mesmas contagens, mesmos
updated_at (do índice, não do frontmatter), mesmos hashes. Pode ser
chamado sem medo após qualquer operação suspeita.
7. Índice (SQLite)
BR-7.1: Schema versionado
Toda mudança de DDL passa por uma migration numerada
(migrations/NNNN__nome.sql). Tabela schema_migrations registra
aplicadas com version, applied_at, checksum. Checksum mismatch é
fatal e exige syn reindex --force-rebuild.
BR-7.2: Migrations são forward-only (reversal por descarte)
Migrations são forward-only. Migrations reversas (down) NÃO são
implementadas, por decisão (SYN-105): como o índice é descartável e o
Markdown é a fonte da verdade (BR-1.4 / BR-7.2.1), o mecanismo de reversão é
apagar index.db e rodar reindex no binário desejado: um down-migration
em SQLite seria complexidade sem valor, já que nenhum dado vive só no índice.
Para fazer downgrade: faça checkout do syn anterior, apague index.db,
reindex. Ver docs/MIGRATING.md.
BR-7.2.1: Migrations de índice são expand-only e de baixo risco
O índice SQLite é descartável: o Markdown é a fonte da verdade
(BR-1.4), então o pior caso de um bug de migração ou de um formato de
índice não reconhecido é recuperável com rm .synapse/index.db && syn reindex (ou syn reindex --force-rebuild): nenhum dado do usuário
vive só no índice. Por isso migrations de schema são expand-only:
colunas novas entram como nullable e são populadas pelo reindex,
nunca exigidas no momento da migração (ex.: 0007 adicionou
valid_from/valid_until/supersedes nullable; uma linha indexada
antes da 0007 tem valid_from NULL até o próximo reindex, e o filtro
--as-of trata NULL como “válido desde sempre”, nunca escondendo
linhas legadas). Migrations aplicadas são imutáveis (checksum-locked,
BR-7.1): corrigir uma significa adicionar a próxima, nunca editar a
anterior.
BR-7.3: Tabela entities espelha frontmatter
PK = (type, id). Colunas dedicadas para campos quentes (status,
owner, priority, due). Campos cold ficam no frontmatter
(JSON) com json_extract para filtros raros.
BR-7.4: entities_fts é FTS5 padrão (guarda o conteúdo; alimenta snippet())
Correção (SYN-294): entities_fts é uma tabela FTS5 padrão, não usa
content=. Além do índice invertido, ela guarda uma cópia de title/body/
tags. Essa cópia não é desperdício puro: é o que alimenta
snippet(entities_fts, ...), os trechos destacados retornados pela busca. O
comentário “External-content” no migration 0001__initial.sql está incorreto
e é imutável (checksum-locked, BR-7.1): esta regra é a fonte da verdade.
Medido (dbstat, índice de 2.000 notas curtas): os vetores dominam o índice
(~70%), entities ~9%, e a cópia de conteúdo do FTS (entities_fts_content)
~4%: material apenas para notas longas. O lever de storage real é int8 nos
vetores (SYN-237), não o FTS: converter o FTS para contentless/external
exigiria reimplementar snippet() em Rust + alinhar rowid com entities (a
coluna tags é sintetizada, não existe em entities): refactor com risco de
correção por um ganho dominado pelos vetores. Por isso não convertido.
BR-7.5: links materializa o grafo
Toda relação derivada do frontmatter (owner, memberships, projects,
api uses) tem uma linha em links com tupla (from_id, to_id, rel).
Reindexar reconstrói a tabela inteira.
8. Erros e mensagens
BR-8.1: Display é técnico, fluent_key é localizado
Error::Display é mensagem inglesa curta, estável, usada em logs e
panics. Error::fluent_key() retorna chave Fluent estável renderizada
no idioma do usuário pelo output::print_error. Mudar a string de
Display é breaking (logs de produção), mudar a chave Fluent também
(traduções existentes).
BR-8.2: Toda mensagem visível ao usuário tem chave Fluent
Nenhum println!/eprintln! com texto literal user-facing. Exceções
documentadas (versão --version, output JSON estruturado).
BR-8.3: Códigos de saída padronizados
0sucesso.1erro de uso (args inválidos, entidade não encontrada, validação).2bug interno (panic capturado, erro inesperado).64+reservados, semântica BSDsysexits.h.
9. CLI
BR-9.1: Toda mutação assume confirmação em TTY
rm, rename, link em TTY pedem confirmação. Não-TTY (script) ou
--force pula. Não-TTY sem --force em comando destrutivo é erro.
BR-9.2: --format global
Toda saída estruturada respeita --format text|json. Texto pode usar
cor (apenas em TTY). JSON é sempre canônico (mesmas chaves, mesma
ordem, mesma versão).
BR-9.3: Subcomando sem argumento mostra help, não erra
syn project (sem subcomando) imprime help específico de project,
exit 0. Inconsistência com arg_required_else_help no topo é
deliberada: syn sem nada imprime help também (já configurado).
10. i18n
BR-10.1: Locale por precedência
--lang > SYNAPSE_LANG > sys-locale > en. Resolução acontece
uma vez na inicialização; comando que muda localmente deve passar
explicitamente o novo loader.
BR-10.2: Inglês é fallback obrigatório
Toda chave deve existir em i18n/en/synapse.ftl. Chaves só em
pt-BR (ou outro locale) falham build via SYN-38.
BR-10.3: Argumentos de Fluent usam snake_case
Padrão Fluent. { $entity_id } no .ftl, entity_id = "..." no
código Rust. Não traduzir nomes técnicos.
11. Versionamento e estabilidade
BR-11.1: v0.x: sem garantia
Comandos podem mudar, schema pode mudar, MCP pode mudar. Cada breaking
em commit !.
BR-11.2: v1.0: schema vira semver
A partir de v1.0 (SYN-104), o surface público: schema de frontmatter, surface MCP e CLI: segue semver. Regra precisa do schema de frontmatter: adicionar uma chave opcional é minor; remover ou mudar o tipo de uma chave conhecida é major; chaves desconhecidas são preservadas (BR-3.5), nunca quebram. O índice SQLite NÃO faz parte do surface: é descartável (BR-1.4 / BR-7.2.1), logo mudanças de schema de índice não são breaking.
BR-11.2.1: Surface MCP congelado em v1.0 (SYN-106)
O conjunto de ferramentas MCP é parte do surface semver. Remover ou
renomear uma ferramenta é breaking (major + !); adicionar é minor. O
congelamento é testado: mcp::parity::tests::mcp_tool_surface_is_frozen
compara o router ao vivo contra FROZEN_V1_MCP_TOOLS e falha em qualquer
drift: o surface nunca muda em silêncio.
BR-11.3: Sempre prefira deprecate over remove em v1+
Mudança de nome de campo: aceitar ambos por uma minor; emitir warning
em --verbose; remover em próxima major.
12. Segurança e privacidade
BR-12.1: Synapse é local-only por default
Nenhum dado sai do disco do usuário sem ação explícita (export, publish, share). Telemetria em zero.
BR-12.2: Segredos não pertencem ao workspace
Se o usuário gravar segredos em .md, o arquivo está em git. Não
implementaremos “campo sensível” porque sugeriria proteção que git
não dá. Documentar no tutorial: use gerenciador de segredos externo.
BR-12.3: MCP não escreve sem confirmação humana
Ferramentas de mutação (capture_fact, link_entity, update_status, v0.2) gravam em fila pendente; só são aplicadas após syn pending accept. Princípio: IA propõe, humano sanciona.
Aprovação em lote (SYN-185, ADR-024): sancionar muitas propostas de uma vez (
syn pending accept --allou vários ids) é uma ação humana sobre a fila, não um bypass: a regra continua intacta. Cada proposta aplica atomicamente (SYN-216); o lote é best-effort (falha de uma não bloqueia as outras). Não há comandosyn agent: o loop autônomo é o agente externo via MCP.
Evolução implementada (ADR-022, Accepted 2026-06-07 · SYN-218): este gate é agora uma política em camadas por confiança (
[proposals] auto_apply). O default émanual: todo proposal enfileira, o gate absoluto descrito acima continua valendo por padrão. O opt-in"safe"auto-aplica apenas o tier de baixo risco e reversível (hoje:link_entityentre duas entidades já existentes e não-self; tudo o mais enfileira). O núcleo inegociável é proveniência + reversibilidade: todo auto-apply é carimbado e registrado na trilha comoauto-accepted(distinto de umacceptedhumano). Destrava SYN-185 (syn agent).
BR-12.4: Federation é opt-in por workspace (default OFF)
O registry global (workspaces.db) sabe da existência de todo
workspace que foi syn init-ado nesta máquina, mas só workspaces com
federated = 1 participam de queries cruzadas (--all no CLI,
federated_search no MCP). Default na criação é 0. Habilitação é
explícita (syn ws include <name>, com confirmação em TTY).
Por quê: um workspace pode ser cliente confidencial; default seguro é “não vaza para queries de outros contextos”. O usuário escolhe o que entra no pool federado.
Aplica-se a: todo código que itera múltiplos workspaces. Cada PR de federação deve referenciar SYN-79 explicitamente para o reviewer checar essa regra.
BR-12.5: Freshness check é advisory, nunca bloqueante (ADR-011)
Workspaces com git_remote capturado têm um sinal periódico (default
30min) que compara o tip local com o refs/remotes/origin/<branch>
rastreado. O status (up_to_date | behind | ahead | diverged | unreachable | unknown) é persistido no registry e exposto:
- CLI: linha única em stderr antes do output normal apenas
quando
status ∈ {behind, diverged}. Suprimível viaSYNAPSE_NO_FRESHNESS=1,--no-freshness,syn ws freshness off <name>ou[freshness] silent_cli = true. - MCP: campo
freshnessno payload de tool results quandostatus ∈ {behind, diverged, unreachable}. Omitido paraup_to_dateeunknown.
Phase 1 (SYN-93, v0.2): comparação filesystem-only dos SHAs
em .git/refs/heads/<branch> vs .git/refs/remotes/origin/<branch>:
sem rede, sem subprocess, sem credencial. Emite só
up_to_date, diverged (com behind_count = -1 por não caminhar
o grafo) e unknown. Os estados behind, ahead e unreachable
existem na enum mas só são emitidos pela Phase 2.
Phase 2 (SYN-93b, v0.5): ls-refs real via gix-protocol com
timeout de 3s, unreachable fallback, e walk de commits para
behind_count preciso. Layered sobre o mesmo schema e UI.
Nunca: bloqueia comando, dispara git fetch, armazena
credencial, ou fala se status é up_to_date.
Por quê: workspace compartilhado via git fica obsoleto em
silêncio, e dado obsoleto entregue com confiança é o pior modo de
falha de um second brain. O aviso é o mínimo informativo possível;
ação fica com o usuário (git pull explícito).
BR-13: Enriquecimento no insertion-time, não no query-time
Lands com v0.5 (SYN-160). Brief:
docs/researchs/2026-06-02-v0.5-semantic-layer-and-second-brain.md§1.2.
Toda escrita no workspace (syn project add, syn person add,
syn note add, syn entity add, syn api add, syn task add,
capture_fact via MCP, qualquer aceite de proposta na pending queue
que materialize uma entidade) dispara o pipeline de enriquecimento
síncrono antes do Workspace::tx comitar:
- Embedding:
title + body + tagsé vetorizado pelo modelo configurado em[enrichment] embedding_model(defaultBGE-M3multilingual; opt-downBGE-small-EN-v1.5-Q). O vetor entra na tabelaentities_vecda mesmaindex.db. - Extração de candidatos: varredura heurística (slugs existentes
por texto,
@mention, datas ISO, hostnames de URL, tokens de tag) + passo NER via[enrichment] ner_model(defaultGLiNER-multi-base-Q; opt-downGLiNER-base-en-Q). Tipos NER são hard-coded por Synapse:person,organization,product,feature_flag,api_endpoint,framework,tool,decision_topic. - Resolução: cada candidato bate contra o índice live. Match
conhecido vira
linktipado (rel = "mentions"ourel = "concept:<lemma>"); span NER sem entidade correspondente virapendingde kindcreate_entity_from_nerpara revisão humana (BR-12.3 continua valendo: MCP/CLI não cria entidade sem aceite). - Paralelismo controlado: embedding e NER rodam em threads
distintas dentro da mesma
Workspace::tx; wall-clock émax(embed, ner). Budget de latência: mediana ≤ 500 ms, p95 ≤ 800 ms num corpus de 10k entidades em CPU de referência (Apple M1 Pro). Excedeu o budget = bug de performance, abre tracking ticket. Async / background é v0.6+ (SYN-194 já registrada no brief). - Atomicidade preservada: qualquer falha no pipeline aborta a
escrita inteira. Usuário pode desligar com
[enrichment] mode = "off"para voltar à semântica v0.4 raw, oumode = "heuristic"para pular só o NER mantendo o embedding.
Por quê: princípio. A IA é o co-piloto da memória, não o reranker em tempo real. O custo é pago uma vez, no momento em que o usuário tem o contexto fresco; toda busca futura é barata, determinística e sobrevive offline.
Trade-off explícito. A escrita fica mais cara em latência e o binário cresce ~50 MB + ~2.5 GB de cache de modelos na primeira execução. Em troca: queries indiretas (“flag do Firebase do dia da reunião com a Carina”), filtros estruturais por participantes / tools / decisões, e graph traversal funcionam sem precisar de LLM no caminho da query.
Opt-out. [enrichment] mode = "off" no .synapse/config.toml do
workspace. Aceitável e suportado: usuário que quer FTS5-only e zero
modelos paga zero MB de cache e ~150 ms de embedding evitados por
escrita.
BR-14: Identidade do usuário: capture once, seed everywhere
Lands com v0.6 (SYN-170..173). ADR-020.
O Synapse tem um dono. A identidade desse dono é capturada uma única
vez, no syn setup global, e semeada em cada workspace como a
pessoa canônica self: true. A captura global é a fonte; a pessoa por
workspace é a projeção.
BR-14.1: Setup é compulsório, CI nunca bloqueia
syn init exige config global. Ausente + TTY interativo ⇒ roda o
setup guiado inline antes de prosseguir. Ausente + não-TTY
(CI, pipe, --no-input, SYNAPSE_AGENT) ⇒ cai no default silencioso
sem escrever nem bloquear. Esse fallback é invariante duro: nenhum
caminho de automação pode travar esperando input.
BR-14.2: Profile global é a fonte única
O profile vive em <config_dir>/profile.toml: separado do
config.toml (que descreve comportamento da ferramenta e cascateia
system → global → workspace). O profile descreve quem é o humano,
não cascateia: existe exatamente um, global. Override de path via
SYNAPSE_PROFILE (testes e usuários avançados). Campos:
name: obrigatório; nome vazio = “sem profile”, nada é semeado.birthdate: opcional, data ISO 8601YYYY-MM-DD(validada na captura).headline: opcional, profissão; alimentasyn cv export.employers[]: opcional, ordenado,{ name, role? }.
BR-14.3: Schema canônico da pessoa self
Uma, e só uma: fonte de verdade para a forma da pessoa self
(core, MCP context, cv export e federation dependem dela):
type: person
id: <slug-do-nome>
title: <Nome Completo>
self: true # exatamente um por workspace
birthdate: 1815-12-10 # opcional, ISO 8601
headline: Mathematician # opcional; lido por cv export
affiliations: # opcional, ordenado
- org: analytical-engine-co # slug de um entity(kind=org)
role: Lead Analyst # opcional
memberships: [] # BR-4.4: person→project, distinto de affiliations
created: …
updated: …
affiliations é deliberadamente distinto de memberships: BR-4.4
reserva memberships para vínculos person→project. Empregadores viram
entity(kind=org) materializados no seed; o vínculo person→org rico no
grafo é evolução futura.
BR-14.4: Seed é idempotente e garante exatamente-um
syn init materializa/atualiza a pessoa self a partir do profile.
Reusa o id da self existente (o slug não muda mesmo se o nome mudar),
preserva created/memberships/body ao refrescar, e garante exatamente
uma self por workspace (a invariante que cv/identity export já
exigem). O seed não dispara enrichment (BR-13): roda durante o init,
antes de os modelos existirem, e nunca pode forçar download nem falhar
quando ausentes: a self é embeddada na próxima edição ou syn reindex.
Como mudar uma regra: PR que edita este arquivo + cita ADR que justifica. Mudança silenciosa de comportamento sem mudar a regra é bug.