Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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-N para 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.

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:

  1. Unicode NFD.
  2. Remove diacríticos (̀-ͯ).
  3. Lowercase ASCII.
  4. Substitui qualquer não-[a-z0-9]+ por -.
  5. Colapsa hífens duplos.
  6. 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:

  1. Move o arquivo (e a pasta, no caso de project).
  2. Reescreve id no frontmatter.
  3. 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:

  1. Abre transação SQLite.
  2. Escreve arquivos em *.tmp paralelo aos finais.
  3. Em sucesso: rename atômico de cada *.tmp → final, depois commit SQLite.
  4. 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.

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

  • 0 sucesso.
  • 1 erro de uso (args inválidos, entidade não encontrada, validação).
  • 2 bug interno (panic capturado, erro inesperado).
  • 64+ reservados, semântica BSD sysexits.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 --all ou 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á comando syn 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_entity entre 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 como auto-accepted (distinto de um accepted humano). 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 via SYNAPSE_NO_FRESHNESS=1, --no-freshness, syn ws freshness off <name> ou [freshness] silent_cli = true.
  • MCP: campo freshness no payload de tool results quando status ∈ {behind, diverged, unreachable}. Omitido para up_to_date e unknown.

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:

  1. Embedding: title + body + tags é vetorizado pelo modelo configurado em [enrichment] embedding_model (default BGE-M3 multilingual; opt-down BGE-small-EN-v1.5-Q). O vetor entra na tabela entities_vec da mesma index.db.
  2. 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 (default GLiNER-multi-base-Q; opt-down GLiNER-base-en-Q). Tipos NER são hard-coded por Synapse: person, organization, product, feature_flag, api_endpoint, framework, tool, decision_topic.
  3. Resolução: cada candidato bate contra o índice live. Match conhecido vira link tipado (rel = "mentions" ou rel = "concept:<lemma>"); span NER sem entidade correspondente vira pending de kind create_entity_from_ner para revisão humana (BR-12.3 continua valendo: MCP/CLI não cria entidade sem aceite).
  4. 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).
  5. 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, ou mode = "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 8601 YYYY-MM-DD (validada na captura).
  • headline: opcional, profissão; alimenta syn 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.