Progresso

154 min de leitura 30.699 palavras Editar no GitHub

Avaliações interativas e revisão por educadores

19/10/2026. Checkpoints no fim dos labs e um simulado final por handbook usam o novo contrato assessment-set/v1, sem depender de motor de fluxos. Escolha simples, múltipla resposta, ordenação, associação e análise de código têm correção determinística. Nota 0–10, pontos, percentual e aproveitamento por objetivo aparecem após a entrega, junto de gabarito, razões dos distratores, fontes e sugestão de revisão prática. Ordenação/associação oferecem arraste e controles equivalentes de teclado/toque. Tentativas entregues ficam travadas; uma nova tentativa preserva histórico local de até 20 registros por versão.

O Studio incorpora assessments.json nas fontes, autosave, briefing e prévia. O educador revisa questões com checklist, nome, notas, estado e digest do conteúdo/fontes/objetivo. Mudanças tornam a aprovação obsoleta. Relatórios de tentativa podem ser abertos e recalculados contra o banco original. Identidade autenticada, turmas e diário compartilhado ainda são fronteiras de serviço.

O Kubernetes tem três questões de checkpoint e sete no final, com os cinco tipos e quatro objetivos. As questões são originais, sustentadas pela documentação Kubernetes CC BY 4.0, e estão explicitamente em revisão. Não foi inventada aprovação humana nem copiado banco externo. Outros handbooks exibem que suas avaliações ainda não foram escritas. Proveniência e licença de itens externos fazem parte do contrato para futuras importações.

handbook-format alpha.4 lê bundles v1 e v2: conteúdo sem avaliações mantém v1 exato; avaliações usam v2, com exemplares congelados de conformidade. O manifesto segue v1; o host alpha.2 monta as mesmas avaliações nas entregas independentes. Resultados dos portões completos ficam registrados na PR. O capítulo Interactive assessments detalha a estrutura e os limites.

Entregas executáveis e contratos de interação

19/10/2026. A integração de Mermaid, SDK independente e Studio foi mergeada na main pela PR #20 após sucesso do CI completo. Esta continuação separa contratos de conteúdo, implementação de lab, host compilado e entrega. handbook-format alpha.3 mantém o bundle v1 original e adiciona lab/v1, host-template/v1 e delivery/v1, sem colocar catálogo de domínio no SDK.

O Studio oferece Delivery: compatibilidade por id, rota e versão exata, recusa de labs desconhecidos e ZIP executável montado localmente. O comando pnpm delivery:build usa a mesma montagem. apps/runner contém três adapters reais: Pod lifecycle (estado), Collector (fluxo, com .mmd canônico incluído) e cardinalidade (manipulação direta). Não é necessário instalar dependências para consumir uma entrega: node serve.mjs verifica os arquivos antes de servir.

A prova fora do checkout confere execução, recusa de conteúdo alterado e arquivos não publicados. Testes de browser baixam a entrega pelo Studio e interagem com os três tipos em desktop/mobile sem acesso a origens externas. Os resultados finais da suíte completa ficam registrados na PR desta entrega. As organizações podem hospedar os arquivos privadamente; autenticação, deploy automático e distribuição geral de novos adapters ainda são próximos passos. O capítulo Runnable handbook deliveries detalha o limite.

Handbook Studio — edição local e autoria assistida

18/10/2026, continuação pedida pelo Luigi. /studio/ entrega uma primeira interface de autoria com fontes, editor, diagnósticos do SDK e prévia do aluno. É possível criar um handbook, abrir bundle v1 ou ZIP de fontes, editar currículo e artigos, criar rascunhos sem inventar bibliografia e exportar o mesmo bundle validado pelo CLI. O exemplo inicial é o Kubernetes Handbook.

Fontes inválidas podem ser salvas localmente e baixadas em ZIP; export de release exige os portões mecânicos. Abertura falha não substitui o projeto, há restauração do anterior em sessão, e armazenamento bloqueado mantém edição e download disponíveis. Labs só ganham link de execução se o host os conhece. A prévia sanitiza Markdown e mantém os degraus textuais e fontes primárias.

O briefing para Claude/outro assistente reúne objetivo, arquivos e diagnósticos, com a jornada prever, alterar, observar, explicar e transferir. É um handoff explícito por arquivo/clipboard, sem chamada de modelo ou envio automático. Ainda não há edição de comportamento de lab, Git sync, colaboração ou publicação.

O SDK handbook-format avança para 0.1.0-alpha.2: o leitor de manifesto passa à entrada sem I/O e ganha escritor v1, conferido por round-trip do bundle congelado. interactive-core permanece em alpha.1. O Studio usa essa entrada no browser; o pacote instalado continua passando no smoke externo. Os resultados finais dos portões são registrados na PR desta entrega.

Autoria independente — SDK e artefatos alpha

18/10/2026, continuação pedida pelo Luigi. A autoria passa a ter uma prova fora do monorepo: pnpm sdk:pack produz tarballs compilados de handbook-format e interactive-core, com versões alpha explícitas e índice SHA-256. Não publica registry, tag ou produção. pnpm sdk:smoke instala os artefatos em um projeto externo, cria, confere, exporta e consome um handbook, além de executar replay e ramificação do runtime sem workspace, Astro ou compilador.

O CLI instalado fornece new, check --json, export, inspect e versions. Seu layout é handbook.yaml + articles/. O gerador e a régua mecânica de artigos são comuns ao comando do repo. O leitor do bundle valida estrutura, versões e conteúdo; um export congelado da PR #17 prova que o wire format v1 continua legível. A saída /contract não importa filesystem.

O bundle leva conteúdo, e labsExigidos explicita as implementações que o host precisa fornecer. Não há distribuição automática de labs, ABI de plugin ou Studio completo. A documentação no índice de autoria registra a arquitetura e o caminho externo, com a mesma direção de experiências guiadas para conceitos. Os resultados dos portões são registrados na PR desta entrega.

POC — Framework interativo e Kubernetes Pod lifecycle

16/10/2026, a pedido do Luigi. O framework passa a declarar explicitamente mais de uma forma de interação: Mermaid é contrato dos cenários do motor de fluxos; estado/reconciliação e manipulação direta não precisam passar por ele. A decisão está no capítulo “Framework interativo e Pod lifecycle”, acessível no índice de autoria. A apresentação da landing e do catálogo foi alinhada à mesma distinção.

Em 17/10, Luigi explicitou a direção de experiência: “um Instruqt para conceitos e teoria”, com inspiração nos diagramas animados do KodeKloud. O capítulo de arquitetura registra a jornada prever, alterar, observar, explicar e transferir, e a regra de que movimento deve corresponder a mudanças do modelo. Studio AI-first e distribuição privada/versionada seguem como direção de produto.

interactive-core é uma biblioteca pequena de eventos, transições, snapshots e histórico limitado, sem dependência de Mermaid, depth-core ou depth-ui. kubernetes-domain é sua primeira POC: interpreta um subconjunto de manifestos Pod/Deployment e deriva reconciliação, binding por requests, início, restart, OOM, exclusão e liberação das reservas. Não é implementação de Kubernetes.

O Kubernetes Visual Handbook entra no catálogo com artigo e lab. A interface, inspirada na árvore de recursos do Argo, mantém manifesto desejado separado do observado e apresenta ownership, node de cada Pod, CPU/memória disponíveis, inspector, eventos, carga sintética e replay. Seis experimentos exercitam os casos; escala é explicitamente do Deployment, restart é do container.

As guardas de fronteira incluem o runtime independente e o novo domínio. Os manifestos da contraparte real são comparados aos iniciais do lab e servidos diretamente dos arquivos do repo, sem cópia divergente no diretório público. Há 28 testes novos de domínio/histórico e testes de browser desktop/mobile.

Limite de verificação: a tentativa de subir kind v0.27.0 / Kubernetes v1.32.2 neste container foi bloqueada pelo kubelet sem /dev/kmsg, antes da inicialização do API server. A contraparte ainda não tem verificação ao vivo; seu README registra o protocolo para fazê-la em um cluster descartável.

O handbook usa o contrato openlabs/handbook/v1 em handbooks/kubernetes/handbook.yaml, com maintainer e CODEOWNERS derivados, aproveitando o caminho de autoria recém-entregue pelo Claude. Não acrescenta outro objeto de catálogo em TypeScript.

O trabalho incorpora os commits da branch de conteúdo até 2fcd287. Os resultados dos portões desta POC são registrados na PR; não houve promoção para produção ou alteração de uma instância Kubernetes externa.

Registro do que já foi feito, sessão a sessão. Atualize este arquivo ao fechar cada sessão, antes do último commit — é daqui que a próxima sessão descobre onde parou, sem reler o histórico do git.

Formato de cada linha: o que ficou pronto, o que ficou pendente, e o que a próxima sessão precisa saber que não está óbvio no código.


Entrega 1 — Fundação e landing ✅

Plano: docs/superpowers/plans/2026-08-28-entrega-1-fundacao-e-landing.md Spec: docs/superpowers/specs/2026-08-28-otel-visual-handbook-design.md

Monorepo pnpm (depth-core, otel-domain, depth-ui, apps/site), design system derivado do canvas, landing com o herói rodando simulação de verdade, guarda de fronteira motor↔domínio no CI, CI e deploy para GitHub Pages.

Estado: 58 testes unitários, 12 smoke Playwright, build verde. Empurrada e mergeada na main.


Marco — as cinco PRs de desenho paralelo, aceitas ✅

Data: 2026-08-28. Merge d3900c0 na main. PRs #1–#5 (kiro/visao-e-stack, kiro/catalogo-de-kinds, kiro/consolidado, kiro/posicionamento, kiro/formato-do-modelo), encadeadas — a #3 continha as outras quatro, então um merge fechou as cinco. 2924 linhas, só docs/, zero conflito com código.

Entraram: DECISIONS.md (ponto de entrada — leia primeiro), VISION.md, kinds.md (catálogo de 19 arquétipos em três ondas), depth.md, model-format.md, why-simulate.md, roadmap.md, stack.md.

Elas contradizem a spec do motor em três pontos, e vencem nos três. A reconciliação está registrada na §17 da spec, com a precedência declarada: docs/DECISIONS.md → spec do motor → plano da sessão. Resumo:

  1. A forma da carga muda só na saída de um transform (substitui a §2.3, que espalhava a transformação pela fronteira de qualquer objeto). Vira property test: com ele verde, o modelo não consegue mentir sobre onde a transformação acontece. sink e channel perdem a transformação. → pousa na S2.
  2. Cinco famílias, não três. Entra controller (relógio, árbitro, supervisor, sonda — não ficam no caminho do dado) e container (pipeline/composite organizam, não processam). → pousa na S1, Task 1c.
  3. Duas espécies de linha: dado (traço grosso) e controle (tracejado fino). A pergunta “por onde o dado passa?” passa a se responder olhando só as grossas. → Wire.line na S1, Task 1c; a fiação a respeita na Task 2.

O catálogo de 19 kinds cresce em ondas (onda 1 na S2: transform, tee, merge, batch, clock, arbiter). A trava de entrada é a régua deles: arquétipo entra pagando em dois alvos. O que a S1 garante é que acrescentar kind seja aditivo — familyOf é um Record<Kind, Family>, então esquecer a família de um kind novo não compila.

Dívida que as PRs abriram e ainda não foi paga: predição antes da revelação, o campo “mal-entendido que este lab desfaz” no teaches, e docs/authoring.md como interface pública do projeto.


Marco — licenciamento ✅

Data: 2026-08-28. Commit 3b75968. Era a urgência nº 1 de DECISIONS.md: repositório público sem licença é todos os direitos reservados, e até então nada aqui podia ser reusado por ninguém — o oposto do projeto.

LICENSE Apache-2.0 (código), LICENSE-content CC BY-SA 4.0 (conteúdo editorial: tudo em docs/, mais prosa, rótulos e teaches dos labs), seção no README, campo license nos cinco package.json. Duas licenças porque o motor existe para ser reusado como biblioteca e o material didático para ser reusado como material didático.


Entrega 2 — Motor composicional 🚧

Spec: docs/superpowers/specs/2026-08-28-motor-composicional-design.md (leia a §17: é onde ela se reconcilia com as PRs aceitas).

Substitui o modelo de quatro níveis fixos por uma árvore de objetos composta de baixo para cima. Sessões planejadas (detalhe na §9 da spec):

  • S1 — Motor composicional. model, tree, wiring, scheduler, world com eventos de parâmetro, meters + superfície pública, tudo testado. O modelo antigo (types.ts, engine.ts) segue exportado como andaime até a S5 migrar a landing — não usar em código novo. Plano: docs/superpowers/plans/2026-08-28-s1-motor-composicional.md. - [x] Task 1 — model.ts e tree.ts (c20806a) - [x] Task 1b — retrabalho da revisão de qualidade (3c9c192) - [x] Task 1b Step 10 — quatro achados da segunda revisão (564d47c): fronteira entry/ exit validada na indexação, não no percurso; isOpenable e entryLeaf param no mesmo predicado; válvulas do invariante cobertas por teste de mutação; byId tipado como AnyObject. 84 testes. - [x] Task 1c — cinco famílias e Wire.line (1d92259) - [x] Task 2 — fiação com encadeamento implícito de pipeline (52edf0e) - [x] Task 3 — stepWorld: um tick como função pura (a98c192) - [x] Task 3b — sorteio endereçável (randomAt, pure-rand), property tests (fast-check) e fio esquecido separado do descarte (dbb76e3) - [x] Task 4 — World com seek exato e parâmetro como evento no tempo (29c8249) - [x] Task 5 — medidores de porta e travessias de fronteira, com property test de que a vista agregada nunca inventa (b78fafb, 813d59f) - [x] Task 6 — superfície pública em index.ts e guarda de fronteira ampliada a protocolo (d273049) - [x] Task 8 — três críticos da revisão final (3b8f652): mensagem entregue a quem não age não some mais, behavior em placa é recusado, World indexa os canais - [x] Task 9 — os três achados que sobreviveram (c9e7b38). A propriedade da vista agregada vira igualdade, não inclusão: provava que ela não inventa travessia e não provava que ela não esconde nenhuma, e dois mutantes passavam em 128 testes — um deles jogava fora todo o L0. O rótulo é recalculado dentro do teste, porque teste que chama a mesma função do código erra junto com ela. setParam passa a valer do tick seguinte. A guarda de fronteira falha quando lê zero arquivo. E a regra do destino que não age muda de substantivo: vive no fio, não no nó, senão recusaria agrupamento decorativo. 160 testes.
  • S1b — Formato do modelo em código (@ovh/model-format). docs/model-format.md deixa de ser só prosa: schema, validação do documento inteiro e compilador para WorldSpec. Detalhe na seção “Entrega 2 · S1b” no fim deste arquivo. Plano: docs/superpowers/plans/2026-08-28-s1b-formato-do-modelo.md.
  • S2 — Arquétipos. Os oito de hoje mais a onda 1 de kinds.md (transform, tee, merge, batch, clock, arbiter): comportamento em depth-core, contrato visual em depth-ui. Aqui também entram o property test do invariante do transform e os três encolhimentos (channel e sink perdem a transformação, buffer perde o agrupamento).
  • S3 — Palco e navegação. Foco por caminho, breadcrumb, selecionar vs abrir, inspector, deep link.
  • S4 — Domínio TracerProvider. Árvore fiel, transformações de mensagem, textos ancorados na spec oficial. Teste 6.
  • S5 — Migração e limpeza. Herói da landing no modelo novo, modelo antigo deletado, guarda de fronteira ampliada.
  • S6 — Acabamento. Modelo estrito de desenho (moldura com recorte, faixas, portas), regime nomeado + log de eventos, perturbações, canal-como-aresta abrível, smoke.

Marco — teoria do motor ✅

Data: 2026-08-29. Commits 4cdd962 e seguinte. docs/theory.md: o motor descrito nos formalismos de que ele é instância, e o que cada um cobra em teste.

Grafo hierárquico com duas espécies de aresta; profundidade é contração, e a vista de fronteira é a imagem do run sob essa contração. Redes de Petri coloridas são o parente mais próximo (kind é a cor; edgeTicks é o carimbo de CPN temporizada; o invariante do transform é preservação de cor em expressão de arco) — com o preço declarado: simulamos, não verificamos. Markov na ordem certa: para semente fixa o mundo é trajetória determinística, e é isso que torna o seek exato; a cadeia aparece ao quantificar sobre a semente. Rede de Markov entra como fronteira, não promessa.

A §5 é a mais importante: o que o motor NÃO é. Em primeiro lugar o relógio global, que ensina de graça uma coisa falsa sobre sistema distribuído. Ou um lab ataca isso de frente, ou o texto declara o limite em voz alta.

E a §7 abriu uma frente: a CPU como prova de genericidade — caminho de dados com assembly de entrada e drill-down até a porta lógica, virando a fase F6 do roadmap.md. Ela tem público (material para as aulas do pai do Luigi), e já nomeou três lacunas reais do motor: linha de controle sem semântica, ler sem consumir, e combinacional contra registrado. Nenhuma se resolve com um kind novo.

Decidido em 29/08/2026: a CPU passa na frente do Kafka e também do OTel. Ordem nova: F1 núcleo → F3 palco → F2 piloto → F2b arquétipos → F6 CPU → F6b ATmega → F4 otel → F5 handbook → F7 kafka → F8 extração. Os identificadores de fase são nomes, não sequência.

Dois argumentos. Contra o Kafka: ele é vizinho do OTel, então reuso entre os dois quase não prova nada, e extrair o motor tendo visto só mensageria produziria um motor de mensageria com outro nome. Contra o OTel: o que falta lá é sobretudo editorial — currículo, texto, procedência — e esse trabalho consome tempo sem pressionar o motor; a CPU é o oposto, quase sem texto e com verdade de campo dura. Ela amadurece o motor por unidade de esforço, e motor maduro é pré-requisito de o OTel ficar bom.

O risco está declarado junto com a trava. O projeto se chama OTel Visual Handbook e acaba de adiar o OTel; se a CPU virar o produto, trocou de identidade sem decidir trocar. Por isso a F6 carrega um critério de reentrada: ela termina quando as cinco mudanças da F1 fecharem e o cpu-domain rodar um programa de verdade sem depth-core ter ganhado uma linha que saiba o que é um registrador. A hora de parar é a hora em que a próxima tarefa da CPU não ensina mais nada ao motor — e isso virou linha na tabela de sinais do roadmap.

Duas mudanças migraram da F6 para a F1, porque são de contrato: semântica da linha de controle (muda a assinatura de Behavior) e fases do tick (muda o significado de “um passo” para todo arquétipo já escrito). A F1 sempre disse ser “a fase que pode revelar que o tick único não aguenta”; a CPU revelou antes de ela começar, e a resposta já é conhecida — acomodação até o ponto fixo, depois entrega do que atravessa aresta registrada, com laço combinacional recusado na construção do mundo.

Depois da CPU genérica vem um ATmega (F6b), e não é repetição: a CPU testa mecanismo, o ATmega testa fidelidade contra um datasheet que não controlamos. Ele traz interrupção — controle assíncrono que preempta o fluxo —, que é o primeiro fenômeno da lista que talvez não caiba nas primitivas atuais, e por isso o mais valioso de tentar.


Entrega 2b — site, documentação navegável e Vercel 🚧

Plano: docs/superpowers/plans/2026-08-28-e2b-site-docs-e-vercel.md

  • Task 1 — caminho-base configurável e vercel.json (b078197). A Vercel serve na raiz, o Pages num subdiretório: quem chama o build declara onde vai servir.
  • Task 2 — documentação como coleção do Astro lendo ../../docs, com taxonomia num manifesto (src/data/docs-index.ts). Os documentos não ganham frontmatter: eles também são lidos no repositório. O teste tem as duas direções — capítulo do índice que não existe, e documento que existe e não está em tema nenhum.
  • Task 3 — páginas de documentação: índice à esquerda, sumário à direita, tempo de leitura com código pesando mais que prosa, anterior/próximo, editar no GitHub.
  • Task 4 — busca estática com Pagefind, indexando só o conteúdo (a navegação leva data-pagefind-ignore, e isso está verificado no índice construído).
  • Task 5 — landing com navegação, os quatro níveis como peça visual, a documentação no roteiro com estado real, moldura bilíngue. Falta a verificação: e2e verde e a auditoria de contraste nos dois temas.

Armadilhas do Astro que custaram tempo e não estão em lugar nenhum da documentação dele: literal de expressão regular dentro de expressão no template quebra o parser (a barra fecha a tag — por isso src/lib/urls.ts existe), e função de seta com corpo em bloco devolvendo marcação também. As duas dão o mesmo erro inútil: Syntax error "a".

Depois da Entrega 2:

  • E3 — Cenários, encaixe tipado e manifesto (spec §14–15). Manifesto é config real (env vars do SDK / config declarativa), com contrato de fidelidade obrigatório. Faseado: exportação de mão única primeiro, manifesto como fonte da verdade depois.
  • E4 — Meter e Logger provider, reaproveitando os Kinds.

Protótipo navegável (validado com o Luigi em 2026-08-28): claude.ai/code/artifact/fc302e68-488b-4e8e-9037-74a0a0352e17. Fonte em scratchpad, descartável — é maquete de decisão, não código de produção. Serve como referência visual e de interação para S2, S3 e S6.

O que a Entrega 2 quebra de propósito

LevelId, Scenario e StepContext.inputs como estão hoje. O modelo antigo pode coexistir dentro de depth-core enquanto a reescrita acontece, para manter main verde — mas precisa estar deletado antes de a entrega fechar. Coexistência é andaime, não arquitetura.

Decisões desta entrega que não estão no código

Todas na spec, mas as que mais custam se forem esquecidas:

  • Só folhas têm comportamento. Objeto composto nunca tem comportamento próprio.
  • Mudar parâmetro não zera o tick. O mundo reage de onde está.
  • Medidor só lê tráfego de porta, nunca estado interno. Não existe caminho de meters.ts para state.nodes — a assinatura das funções nem recebe o que precisaria para espiar.
  • A vista agregada é projeção de fronteira do mesmo run, nunca autorada à parte. scheduler.property.test.ts prova, com property test, que ela nunca inventa travessia que não aconteceu.
  • Fio esquecido é contado em .unwired, separado do descarte deliberado (@drop). Silêncio de fiação e decisão de descartar são coisas diferentes, e o livro-caixa não pode confundir uma com a outra.
  • Sorteio é endereçável por (seed, tick, salt), nunca um fluxo com estado escondido. É isso que torna o seek do World exato em vez de aproximado: rebobinar é reler, não reexecutar do tick 0.
  • Visual pertence ao Kind, nunca ao objeto. Sem essa trava, nada termina.
  • A chamada da API não é filha do TracerProvider, e BatchSpanProcessor é um SpanProcessor. Fidelidade da árvore é o produto.
  • composite ≠ pipeline. O TracerProvider é composite (contêiner sem ordem imposta); só a lista de SpanProcessors é pipeline, porque só ali a ordem importa.
  • Canal é a LINHA, não um bloco. Bloco é processador (age sobre o dado); linha é o que carrega. Desenhar canal como caixa ensina errado.
  • Vocabulário do motor nunca é conteúdo. kind, composite, “tráfego de porta” vivem atrás do modo autor. Se vazam, o handbook explica a si mesmo.
  • Contenção é estrutural. Moldura com clipPath real — não uma checagem que alguém pode esquecer de escrever.
  • O histórico do World é ilimitado, e isso é dívida declarada. Guardar todo estado desde o tick 0 é o que torna o seek exato; um limite qualquer transformaria “exato” em “exato dentro de uma janela”, em silêncio. A saída certa quando doer é checkpoint mais re-simulação até o alvo — preserva a exatidão e limita a memória. Não invente um teto antes disso.
  • Controlador não fica no caminho do dado. Árbitro, relógio e supervisor influenciam quem está no caminho, mas não recebem a carga. Tratá-los como processador obrigaria a inventar um fluxo que não existe — o mesmo erro que a placa evita.
  • Escala de tempo declarada (1 tick = 100 ms) e valor real ao lado de todo controle. Tick abstrato é pior: o leitor inventa a correspondência e a gente não pode corrigir.

Entrega 2 · S1b — Formato do modelo em código ✅

Data: 2026-08-29. Plano: docs/superpowers/plans/2026-08-28-s1b-formato-do-modelo.md

Pacote novo @ovh/model-format, entre o motor e o conteúdo. Ele conhece kind, porta, fio e parâmetro — vocabulário do motor — e por isso entrou na guarda de fronteira junto com depth-core e depth-ui.

  • schema.ts — porta, parâmetro e fio em Zod, todos .strict().
  • modelet.ts — parseModelet: YAML → Modelet validado, com as regras que só valem olhando o documento inteiro.
  • compile.ts — compileModelet/compileSource: Modelet → WorldSpec que o World roda.
  • behaviors.ts — o comportamento mínimo de source, buffer e sink, que é o que falta para um WorldSpec compilado ser executável.

42 testes no pacote (216 no repositório).

Decisões desta sessão que não estão óbvias no código

  • O compilador recusa kind de onda futura em vez de fingir, dizendo em que onda ele chega (clock → onda 1). Um lab que roda errado é pior que um lab que não roda. kind fora do catálogo é erro de digitação, e a mensagem lista os disponíveis.
  • Porta órfã e parâmetro morto são erro, não aviso. Porta declarada que nenhum fio usa aparece no desenho sem fazer nada; parâmetro que nenhum filho referencia vira controle que não controla.
  • As regras são de igualdade, não de inclusão. Todo fio aponta para porta que existe e toda porta é usada; todo { param: x } cita parâmetro que existe e todo parâmetro é citado. Provar só um lado deixaria passar metade dos desenhos que mentem.
  • As pontas dos fios saem do parse já resolvidas (Endpoint discriminado). Depois do parse ninguém parte "queue.out" em dois, então ninguém pode partir errado — e o nome recusa . e : no próprio nome, não em quem o usa.
  • A tabela CONTRATOS é o coração do compilador: por kind, as portas de entrada, as de saída e os argumentos que ele de fato implementa. Emissão numa porta que ninguém entrega e argumento que o autor configura sem nada ler são a mesma mentira silenciosa, vista de dois lados; a tabela torna as duas impossíveis.
  • Parâmetro enum não entra em WorldSpec.params. Params do motor são números, e um nome não tem número honesto — indexar a lista inventaria uma correspondência que ninguém declarou. Fica em ParamInfo, com os valores. Duração vira milissegundos, com a unidade ao lado.
  • A fronteira é alimentada por contorno (model-format.md §1.2): porta de entrada ganha fonte sintética, porta de saída ganha consumo sintético, porta de descarte vai para DROP. Os nós de contorno usam o prefixo @, que nenhum nome do formato pode ter.

O que ficou de fora, e por quê

  • Cinco dos oito kinds de hoje não compilam como filho, cada um com a sua mensagem: composite/pipeline (contêiner não é folha), channel (canal é aresta), router (precisa de política de rota, que o formato não declara) e static (precisa de conteúdo, que o formato não declara). Compilam: source, buffer, sink.
  • Fio de controle que toca um filho é recusado. Nenhum kind de hoje tem porta de controle — clock e arbiter chegam na onda 1. Fio de controle entre portas da fronteira vale, chega ao WorldSpec com line: "control" e a fiação de dado o ignora. Dívida: o escalonador não entrega nada por linha de controle; hoje ela é declaração, não caminho. Quando o clock chegar, isso precisa mudar junto.
  • Duas linhas de dado saindo da mesma porta são recusadas — resolveTarget segue só a primeira, então a segunda seria desenho sem percurso. Replicar carga é o tee, da onda 1.
  • modelet aninhado dentro de modelet não existe, e o .model (a camada de cima, §4 de model-format.md) não foi tocado. O compilador só monta um modelet isolado.
  • A posição do erro no YAML não é usada. O pacote yaml preserva linha e coluna, e as mensagens ainda apontam para o caminho do campo, não para a linha do autor.

Sessão — OpenLabs: o projeto passa a ser a casa dos handbooks

Data: 2026-08-29. Decisão: docs/DECISIONS.md, seção “O enquadramento: OpenLabs”.

O projeto vira OpenLabs; o handbook de OTel e o da CPU viram dois .model sobre o mesmo motor. No site: catálogo em apps/site/src/data/handbooks.ts, cartão (HandbookCard.astro), índice em /handbooks/ e página por handbook em /handbooks/[id]/ com a anatomia roadmap · artigos · labs. O mapa interativo saiu da landing e foi para a página do handbook de OTel — é dele, não da capa; o do RISC-V ainda não tem mapa, e a página diz isso em vez de desenhar um caminho não andado.

Estado: 253 testes unitários (11 novos, do catálogo), 46 e2e (5 novos), build, typecheck e guarda de fronteira verdes.

Pendente e não óbvio: o plano do Bloco 1 da CPU (docs/superpowers/plans/2026-08-29-f6-b1-fases-do-tick-e-controle.md) está escrito e ainda não executado — é a próxima coisa a fazer. E as fases do RISC-V no catálogo são desenho editorial, não a spec: quando o cpu.model existir, os labs de lá viram available um a um.


Bloco 1 da CPU — as duas fases do tick e a linha de controle

Data: 2026-08-29. Plano: docs/superpowers/plans/2026-08-29-f6-b1-fases-do-tick-e-controle.md. Commit do coração: 8b0c662 (as duas fases em stepWorld).

O motor ganhou o que a CPU exige e o OTel não pedia — e nenhum arquivo do motor menciona o assunto:

  • Duas fases por tick: acomodação (propaga dentro do tick) e confronto (onde o estado muda e nascem as mensagens que custam tick). ctx.phase diz em qual delas o comportamento está rodando
  • O padrão da aresta é clocked, então nenhum mundo escrito antes mudou de comportamento — foi o que manteve os 242 testes anteriores verdes
  • A acomodação percorre um DAG em ordem topológica porque o laço combinacional é recusado na construção. Some a iteração e some o teto de rodadas: cada ator roda uma vez só, com o conjunto completo das entradas dele. A alternativa (teto de iterações) transformaria “não converge” em “converge errado”, em silêncio
  • A profundidade topológica é o atraso de propagação, e virou WorldState.substeps. Conta o nível de quem recebeu, não o de quem emitiu: o último elo de um caminho combinacional costuma ser um elemento de memória, que na acomodação não emite nada
  • Quem acomoda não guarda: o state devolvido na fase de acomodação nem chega a ser lido. A diferença entre lógica combinacional e elemento de memória virou estrutural, não disciplina
  • Emitir na fase errada lança, dizendo qual fase aquela porta espera. Uma porta é de um regime só, e isso é recusado na validação
  • O eixo sigin: é separado de in:/out: porque o medidor de porta lê só os eixos de carga — ele portanto não consegue enxergar sinal, e a pergunta “quanto dado passou aqui?” continua tendo resposta
  • A guarda de fronteira passou a vigiar dois domínios, com duas listas e um só mecanismo de busca. Ela já pegou três comentários meus que diziam “registrador”

Estado: 291 testes unitários, typecheck, boundaries e build verdes.

Pendente e não óbvio: desenhar os subpassos na tela é do Bloco 3 (depth-ui); fan-out de dado, multiplicidade e atalho com equivalência provada são o Bloco 2. Duas correções que fiz contra o plano escrito, e que valem para quem for reler: substeps conta receptores (o plano contava emissores e o próprio teste dele exigia o contrário), e a asserção do eixo de carga em control.test.ts fecha por conservação (chegou + em voo == emitido) em vez de ignorar o atraso da aresta.


Bloco 2 da CPU — fan-out, multiplicidade e atalho provado

Data: 2026-08-29. Plano: docs/superpowers/plans/2026-08-29-f6-b2-fanout-multiplicidade-atalho.md.

Fecha o contrato do motor. Continua sem uma linha de CPU em lugar nenhum:

  • Leque de carga é nativo da porta. n fios entregam n cópias; out: conta uma emissão e cada destino conta o seu in:, e as duas divergirem é informação — é quanto a saída se espalhou. resolveTarget virou resolveTargets(...)[0], um mecanismo só. Há teste provando que o leque é igual nas duas fases: a acomodação já percorria todos os fios, e mudar só o confronto faria o mesmo desenho entregar diferente conforme o regime da porta
  • Caiu a recusa de f281ece, no motor e no compilador — ela existia porque o motor percorria só o primeiro fio
  • tee saiu do catálogo (docs/kinds.md), riscado com o motivo em vez de apagado, e com a dívida que ele levou junto: política de falha parcial (“exige todas, ou basta uma”) pertence ao regime da aresta, e volta com backpressure
  • ×N e /N existem e são validadas. replicas: N exige N filhos de fluxo do mesmo kind — a marca diz “desenhe um destes N” e os N existem de verdade. width: N é marca de desenho e não conta nada: há teste comparando o livro-caixa inteiro com e sem ela
  • ObjectSpec.shortcut: o contêiner age e a subárvore dele não roda. Roda sempre, nunca “quando ninguém está olhando” — condicioná-lo ao que o leitor abriu faria a resposta do modelo depender da navegação
  • A prova, em shortcut.ts: shortcutDisagreement roda os dois caminhos e compara a projeção de fronteira (estado e livro-caixa de quem está de fora), não o interior nem os ids de mensagem — os emissores são outros por construção, e exigir id igual reprovaria um atalho correto

Estado: 308 testes unitários, typecheck, boundaries e build verdes.

Achado que vale para quem for escrever atalho: um atalho só empata com a composição se a cadeia interna acomodar. Com aresta cronometrada por dentro, a composição responde um tick depois, e o atalho estaria mentindo sobre latência mesmo acertando o valor. A fixture de teste mostra isso — e é o caso real, porque atalho existe para cadeia que fecha dentro do ciclo.

Pendente: desenhar ×N, /N e os subpassos é o Bloco 3 (views, em depth-ui). Expandir réplicas automaticamente a partir de um modelet continua fora: quem instancia os N é quem escreve o modelo, e o motor cobra que eles existam.


Blocos 4 e 5 da CPU — o caminho de dados executa programas de verdade

Data: 2026-08-29. Pacote novo: packages/cpu-domain (@ovh/cpu-domain) — o único lugar do repositório que pode saber o que é um registrador.

O que existe e roda:

  • isa.ts — a tabela do subconjunto RV32I, e o único lugar que sabe como uma instrução vira 32 bits. Montador e caminho de dados leem dela; duas tabelas fariam um erro de codificação aparecer como erro de execução, no lugar errado. Palavra fora do subconjunto decodifica para null, nunca para “a mais parecida”
  • assembler.ts — assembly com rótulos e comentários, erro com linha e coluna em português. Acusa rótulo inexistente, rótulo repetido, deslocamento fora de 0–31, forma errada de acesso à memória e instrução fora do subconjunto
  • reference.ts — intérprete direto, sem motor nenhum, para discordar do modelo
  • datapath.ts — o caminho de dados como composição no motor: relógio, PC, memória de instruções, decodificador, unidade de controle, banco, mux de operando, ULA, memória principal, mux de escrita e unidade de desvio

Uma instrução por tick, e as duas fases do tick são o relógio. Busca, decodificação, leitura, ULA e memória fecham na acomodação; PC, banco e memória escrevem no confronto. O que atravessa a borda de relógio viaja em aresta clocked, e a mensagem em voo é o valor esperando o flanco — é por isso que o laço pc → … → pc não é laço combinacional. Trocar essa aresta para acomodada faz o motor recusar o mundo na construção, e há teste sobre isso.

O bloco 2 apareceu inteiro sem ser forçado: a saída da ULA alimenta dois destinos pelo leque nativo, e as cinco linhas de controle são contadas em sigin:, fora da conta de carga.

O diferencial roda instrução a instrução e compara x0–x31, o pc e a memória tocada: aritmética com estouro, lógica e deslocamentos com e sem sinal, desvio tomado e não tomado, lw/sw com deslocamento, jal/jalr, jalr com alvo ímpar, x0 como destino, e um laço que termina. Verificado por mutação — tirar a guarda de x0 mata 2 testes, trocar sub por add mata 1, e tirar o zeramento do bit 0 do jalr não matava nenhum até o programa de alvo ímpar entrar.

Estado: 327 testes, typecheck, boundaries e build verdes.

Pendências declaradas, que não são detalhe:

  1. O emulador de terceiro ainda não entrou. O intérprete de referência é meu, e compartilha isa.ts com o modelo: isto prova execução, não codificação, e um mal-entendido meu sobre o que uma instrução faz apareceria igual dos dois lados. A spec §7 pede um emulador independente com licença permissiva verificada — continua pendente, e está escrito dentro de reference.ts para ninguém confundir com feito
  2. A unidade de controle é kind: "router" porque o catálogo não tem kind da família controller — clock e arbiter são onda 1. A família está certa no papel
  3. Código automodificável diverge: a memória de instruções é lida da imagem inicial e a principal é outro objeto. Está em §9 o que não é modelado
  4. Falta a fatia vertical (bloco 6: somador aberto → somador completo → portas → transistores) e as views (bloco 3). O que existe hoje roda e é conferível; ainda não é bonito

Bloco 3 e 7 — as views, e o caminho de dados na tela

Data: 2026-08-29. Página: /labs/cpu/.

O mecanismo (depth-ui):

  • view.ts — uma view é a disposição inicial dos objetos para um foco. O invariante: ela decide onde e como, nunca o que existe nem o que se liga a quê. O teste é igualdade: viewDisagreement recusa view que inventa objeto, que desenha alguém de fora do foco, que repete um objeto, que dá área zero, que corta na moldura — e, principalmente, que esconde sem declarar collapsed. Esconder é legítimo; esconder calado não é
  • Stage.tsx — a view desenhada com o estado por cima. Nada se move sozinho: toda animação é disparada por uma diferença no livro-caixa entre dois ticks, então um objeto parado na tela é um objeto que de fato não fez nada naquele tick. As caixas vêm da view, as linhas vêm dos fios, o movimento vem do livro-caixa
  • A família escolhe a forma e o gesto: processor tem engrenagem, e ela só gira quando ele agiu; container é moldura e nunca se mexe; controller fica na cor do sinal. E uma correção que vem do modelo e não da view: quem só emite por linha de controle é controlador, tenha o kind que tiver — enquanto o catálogo não tem kind dessa família, o desenho lê o fato onde ele está escrito, nos fios
  • O pulso ao longo do fio acende todos os fios que saem da porta, porque o leque é nativo; mostrar só o primeiro seria voltar a mentir sobre o percurso

O lab (/labs/cpu/): editor de assembly, montar-e-reiniciar (programa não é parâmetro: programa novo é mundo novo), rodar/pausar, um ciclo por clique, compasso do relógio, duas views (sistema e processador), painel de registradores e a instrução atual.

Estado: 345 testes unitários, 56 e2e, typecheck, boundaries e build verdes.

O que ficou para a próxima iteração (o Luigi já disse que vamos iterar):

  • Roteamento dos fios é ortogonal simples e ainda cruza caixa quando o caminho é longo. Falta desviar de obstáculo
  • Portas lógicas acendendo com a combinação certa depende do bloco 6 (a fatia vertical até o transistor) — hoje não há porta nenhuma modelada
  • A esteira transformando a carga aparece por cor de kind; falta a forma mudar
  • ×N e /N são marcas de canto; falta o contorno empilhado e o feixe desenhado
  • substeps já é contado e mostrado em número; falta andar dentro do tick, que é o que transforma o atraso de propagação em algo que se vê

Bloco 6 — a fatia vertical: o somador aberto até a porta lógica

Data: 2026-08-29. Página: /labs/gates/. Código: packages/cpu-domain/src/gates.ts.

A codificação que faz uma porta lógica caber no motor sem primitiva nova:

A presença da mensagem é o bit em alto. Não chegar nada é zero.

Uma porta que recebe só zeros não roda — e não rodar é exatamente o que uma porta faz quando a saída dela é zero: nada acontece na linha. Por isso a porta acesa na tela não é um efeito: é a saída dela, lida do livro-caixa (data-alto = emitiu neste tick).

A consequência honesta, escrita no arquivo: not não é expressável assim e por isso não existe ali. Com entrada zero ela nunca rodaria e precisaria emitir um. XOR, AND e OR bastam para um somador completo, então a fatia fecha; no dia em que precisar de not, a codificação é que muda.

O que existe: porta (xor/and/or), somadorCompleto (5 portas), somadorWorld(bits) — cascata de vai-um com replicas: bits de verdade (os N somadores existem; a marca só diz que são iguais). Property test: soma todo par de 4 bits com vai-um, e a profundidade de um somador de 8 bits é maior que a de 2 — atraso de propagação medido, não afirmado.

Achado que vale registrar: o somador completo não declara atalho, e o motivo é uma lacuna real do motor. Um atalho substitui o interior, e quem entrega passa a entregar ao contêiner — que tem uma folha de entrada. Um somador completo tem três entradas distintas (a, b, vai-um) que caem em portas diferentes. Enquanto a fronteira de um contêiner não tiver porta nomeada como a linha de controle tem, atalho e composição não aceitam a mesma fiação — e declarar um ali seria escrever código que nenhum teste de equivalência consegue exercitar.

O que mais entrou nesta rodada

  • WorldState.substepOf — em que subpasso cada objeto rodou. Fica no estado, não na tela, porque é resposta do modelo: sem isso, mostrar a acomodação acontecendo dentro do tick exigiria que o desenho adivinhasse a ordem, que é o mesmo que inventá-la. É ele que escalona a animação, e é por isso que a onda atravessa o desenho na ordem em que o sinal atravessa o circuito
  • Roteamento que desvia de obstáculo — uma linha que atravessa uma caixa parece entrar nela, e o leitor passa a ver uma ligação que não existe. Mentira de desenho custa o mesmo que mentira de número
  • ×N desenhado como contorno empilhado, e a marca vem do modelo (ObjectSpec.replicas), não da view — escrevê-la à mão seria um rótulo sem nada por trás. As marcas ×32 e /32 que eu tinha posto na view da CPU eram exatamente isso e foram removidas; em troca, os fios de dado do caminho de dados agora declaram width: 32, que é verdade
  • /N desenhado ao longo do fio

Estado: 355 testes unitários, 66 e2e, typecheck, boundaries e build verdes.


Bornes, atalho provado e profundidade navegável

Data: 2026-08-29.

O contêiner ganhou bornes — entrada e saída nomeadas. Era a lacuna que a fatia vertical tinha achado, e ela era mais séria do que parecia: sem nome, um objeto com três entradas distintas (as duas parcelas e o vem-de-trás) precisava de fiação diferente aberto e fechado — e aí as duas versões deixavam de ser o mesmo modelo, e o teste de equivalência do atalho perdia o sentido. Com inlets e outlets, a fiação de fora é a mesma, e agora há property test rodando as cinco portas contra a conta direta em qualquer um dos bits.

expandPorts abre esses fios uma vez, na entrada do tick: daí para baixo todo fio liga duas coisas que agem, e nem a ordem topológica nem o livro-caixa precisam saber que existe contêiner com bornes.

Três defeitos que só apareceram porque a fiação passou a ser a mesma nos dois casos:

  1. validateWorld dizia que um contêiner com atalho “não age” — o atalho é o comportamento dele
  2. o regime de uma porta sem fio próprio agora sobe para o fio do pai, que é por onde a emissão de fato sai; antes uma folha dentro de um contêiner era cobrada por um regime que não era o dela
  3. a expansão precisa acontecer antes da ordem topológica, senão o grafo de acomodação fica com o contêiner no lugar de quem emite, e a entrega cai numa caixa já processada — em silêncio, que é o pior jeito

Profundidade virou caminho, e não promessa. autoView monta uma vista para qualquer foco: as desenhadas à mão dão o enquadramento bonito dos lugares que importam, e a montada na hora cobre todo o resto. Dois cliques entram num objeto; a trilha de migalhas mostra onde você está e volta. Quem tem interior sai marcado collapsed — o mesmo invariante de sempre: nem inventa, nem esconde calado.

Estado: 363 testes unitários, 68 e2e, typecheck, boundaries e build verdes.


A ULA aberta: a fatia vertical encosta no caminho de dados

Data: 2026-08-29. Código: packages/cpu-domain/src/alu.ts.

A ULA deixou de ser folha. Por dentro dela:

ULA
  dispersor            o número vira 32 linhas
  somador de 32 bits   ×32 somadores completos, cada um com 5 portas
  pesos                ×32 — uma linha vale o que a posição dela vale
  coletor              as 32 linhas viram número
  unidade lógica       o que não é soma (ainda folha, e está declarado)
  mux de operação      escolhe, e fala pela ULA inteira

Do sistema até a porta lógica são seis níveis, e dá para descer todos clicando: sistema › CPU › processador › lógica combinacional › ULA › somador de 32 bits › bit7 → XOR, AND, XOR, AND, OR. Tudo vivo, tudo rodando o programa que está no editor.

O que isso custou de verdade, e é a coisa mais bonita daqui: um ciclo da CPU passou de 8 para 75 subpassos. Não é medida inventada — é a cascata do vai-um de 32 bits, que é exatamente o motivo de somadores reais não serem em cascata. O modelo agora cobra o que antes só afirmava.

Duas peças que existem por causa de uma verdade que o desenho esconde: um barramento de 32 vias é 32 linhas. O dispersor e o coletor são o /32 do fio dito em voz alta — não são adaptador de conveniência.

O diferencial continua passando instrução a instrução com o somador feito de portas. E shortcutDisagreement prova, dentro da CPU inteira, que o caminho rápido da ULA concorda com as duzentas peças de dentro dela: o que se compara é o que o mundo fora da ULA enxerga — registradores, pc e memória. O lab roda aberto, porque descer até a porta e encontrar coisa parada seria o mesmo tipo de mentira que o projeto persegue.

Um defeito achado no caminho: a view montada sozinha rebentava com fio para o descarte — @drop é destino e não é objeto, e perguntar onde ele está na árvore lança. Foi assim que a primeira descida até a ULA morreu.

Estado: 370 testes unitários, 72 e2e, typecheck, boundaries e build verdes.

O que falta da fatia: o transistor abaixo da porta lógica, e a unidade lógica aberta (hoje folha, declarado no arquivo). A fatia desce por um caminho, e esse caminho é a soma — que é a operação que a máquina mais faz.


Entrada e saída: dois endereços que não são memória

Data: 2026-08-29. Código: packages/cpu-domain/src/datapath.ts, apps/site/src/components/CpuLab.tsx.

O programa agora conversa com o mundo, e sem nenhuma instrução nova. Guardar em 0x1000 é falar; ler de 0x1004 é ouvir o botão. É a mesma sw e a mesma lw — o que muda é que aquele endereço não é memória. É assim que máquina pequena sempre teve teclado e tela, e cabe em duas linhas de assembly.

O botão é parâmetro declarado, não recomeço. params: { entrada: 0 } no WorldSpec, e girar chama setParam — o programa não reinicia, ele lê outro número na próxima vez que olhar. Foi isso que decidiu a forma: entrada como evento no tempo já era contrato do motor, e o dispositivo só precisou ler ctx.params.

Guardar no endereço de saída não guarda. Se guardasse, o lw do mesmo endereço devolveria o eco em vez de zero, e a memória cresceria com números que ninguém escreveu ali — exatamente a classe de mentira silenciosa que o projeto persegue. O diferencial agora compara também a fala, palavra por palavra, contra a referência.

Três mutantes mortos: apagar a guarda do endereço de saída, apagar a resposta do dispositivo de entrada, e apagar a emissão para o dispositivo de saída. Os três quebram o diferencial.

A view pegou o defeito antes de mim. Ao entrar entrada e saida na árvore, a VIEW_SISTEMA passou a esconder dois objetos, e viewDisagreement derrubou o build com o nome de quem faltava. O invariante de “nem inventa, nem esconde calado” funcionou como alarme, e não como documentação.

No lab: um campo para o botão, e um painel com o que o programa já falou. O programa de partida deixou de somar 1..5 fixo e passa a somar 1..n, com o n vindo do botão.

Estado: 373 testes unitários, 74 e2e (2 novos), typecheck, boundaries e build verdes.


A mensagem passa a carregar o bit, e not existe

Data: 2026-08-29. Código: packages/cpu-domain/src/gates.ts, packages/depth-core/src/scheduler.ts, packages/depth-ui/src/Stage.tsx.

A codificação mudou: toda linha carrega o bit dela, alto ou baixo, no lugar de “presença é um, ausência é zero”. Foi decisão do Luigi, e o motivo é o transistor.

Por que a codificação bonita não chegava lá. Uma porta CMOS é uma rede de pull-up (PMOS, puxa para 1) e uma de pull-down (NMOS, puxa para 0). Sob presença, puxado para zero e não puxado são o mesmo estado — a rede de pull-down some, e some justamente a metade que faz CMOS ser CMOS. O caminho barato era desenhar meia porta e chamar de transistor; é exatamente a mentira silenciosa que o projeto persegue. not — que estava declarado como preço da codificação antiga — também volta, e com ele NAND e NOR, que são as portas que o silício tem.

O que isso cobrou do motor, e é o achado da vez: a acomodação inteira acontecia e desaparecia. O livro-caixa guardava quantas mensagens saíram de cada porta, nunca o que elas diziam — e com toda porta emitindo todo tick, “emitiu” deixou de distinguir um de zero. Ler a contagem acenderia o circuito inteiro, sempre. WorldState.settled é a resposta: o que saiu de cada porta neste tick, por "id.porta", o par acomodado do flight. O motor continua sem saber o que é um bit — ele só parou de jogar fora a resposta que ele mesmo calculou.

E a tela parou de deduzir. alto() lia o livro-caixa; agora recebe altos do domínio, que é quem sabe ler data.bit. Sem altos, nada acende — não acender é a resposta honesta para “não me disseram”.

O que a mudança revelou de verdadeiro: 0 + 0 antes deixava o circuito morto e substeps em zero. Agora as portas rodam e dizem zero, e o atraso é o mesmo de um caso que acende tudo. É o que um somador em cascata faz: o atraso é da forma dele, não do número que passa. Virou teste.

Três mutantes mortos em settled (gravar também no confronto, nunca gravar, e o resíduo do tick anterior). O diferencial da CPU passou sem tocar em nada: o somador de 32 bits feito de portas continua conferindo instrução a instrução contra a referência.

Estado: 379 testes unitários, 74 e2e, typecheck, boundaries e build verdes.


O transistor: a porta lógica aberta até o silício

Data: 2026-08-29. Código: packages/cpu-domain/src/transistors.ts, packages/depth-core/src/{model,settle,validate,scheduler}.ts.

NOT, NAND e NOR agora existem como rede de transistores, com a tabela-verdade como atalho provado. Cada uma é o par complementar de sempre: PMOS puxa para 1 em cima, NMOS puxa para 0 embaixo, e o que separa NAND de NOR é só quem está em série e quem está em paralelo.

Duas espécies de linha, e é isso que faz o nível caber sem primitiva nova. bit é valor lógico e chega no terminal de porta — o terminal que decide e não conduz, o que é verdade no silício. corrente é o que atravessa o canal, e carrega o nível junto com um conduz.

conduz é campo, e não ausência de mensagem — de propósito. Seria mais curto um transistor cortado não emitir nada. Mas então um nó de saída flutuando (rede mal montada, ninguém puxando) também não rodaria, e o defeito passaria calado: a porta não emitiria, e o de baixo leria isso como zero. Com o campo, o nó sempre roda e sempre pode recusar — flutuação e curto, os dois com o nome do nó e o que fazer. É a regra de sempre: mover a validação para onde a violação vira impossível.

E há um teste que mostra por que isso importa: um NAND com a rede de baixo solta do nó responde certo para três das quatro entradas, e para a quarta ele acertaria zero por acaso sob a codificação antiga. Acertar por acaso, calado, é o defeito.

Lacuna do motor que o trilho achou — a quarta desta fase. Um Vdd não recebe nada e precisa dirigir a linha dentro do tick. A acomodação pulava quem não recebeu, então o trilho nunca agiria e o circuito ligado nele ficaria morto em silêncio. ObjectSpec.drives é o conceito que faltava: fonte constante da acomodação. Não é a source de sempre — aquela emite no confronto e custa uma borda de relógio. validateWorld recusa as duas maneiras de a promessa ser falsa: declarar drives em quem tem entrada acomodada (não seria constante) e em quem não tem saída acomodada (não dirigiria nada).

Um detalhe que só a fatia acha: um transistor sem comando nenhum não é um transistor cortado, é um que ainda não tem opinião — é o circuito antes da primeira entrada chegar. Confundir os dois faz o nó acusar flutuação numa porta que está certa.

Três mutantes mortos: ignorar drives na acomodação, trocar as redes de NAND e NOR, e o nó deixar de recusar flutuação.

O que falta da fatia: ligar as portas do somador nas redes de transistores (hoje as duas coisas existem e se provam, mas o somador ainda usa a porta folha), a view do nível de transistor, e a unidade lógica aberta.

Estado: 390 testes unitários, 74 e2e, typecheck, boundaries e build verdes.


A fatia encosta: o somador roda em transistores

Data: 2026-08-29. Código: packages/cpu-domain/src/{transistors,gates,views}.ts, packages/depth-core/src/{model,wiring,validate}.ts.

As duas metades se tocaram. O lab das portas roda o somador de 4 bits com cada porta feita de transistores: 457 objetos, 433 fios, 43 subpassos. Dá para descer circuito › bit0 › XOR › NAND › PMOS e encontrar silício comutando — oito níveis.

XOR, AND e OR não são portas do silício, então elas se compõem das que são:

AND = NOT(NAND(a,b))               6 transistores
OR  = NOT(NOR(a,b))                6 transistores
XOR = NAND(NAND(a,g), NAND(b,g))  16 transistores, g = NAND(a,b)

Que o XOR custe quase três vezes o AND é metade do motivo de um somador ser caro, e agora isso aparece sozinho na contagem de subpassos.

Lacuna do motor, a sexta desta fase: bornes não compunham. expandPorts abria uma camada e parava, e entryLeaf atravessava o contêiner de dentro — um borne apontando para um filho que também tem bornes entregava no terminal errado, calado. Agora a expansão vai até o fundo, o borne pode nomear a porta do filho ({ node, port }), e validateWorld recusa o nome pelado quando o filho tem bornes. Sem isso a fiação do somador não tinha como dizer qual terminal do XOR ela alimenta.

Três defeitos que só apareceram porque a porta abriu de verdade:

  1. O somador nunca acionou o vai-um do primeiro bit. Sob presença, “ninguém acionou” e “zero” eram o mesmo estado, então amarrar em zero era o mesmo que esquecer. Deixaram de ser: uma porta que recebe uma linha morta não sabe o que responder, e a rede de transistores dela acusou. O fio sempre faltou, e o somador passava em todos os testes por ninguém perguntar. Virou nivelFixo("cin0", 0).
  2. O atalho de cada porta respondia pelos transistores. Eles existiam na árvore e nunca rodavam — descer encontraria coisa parada, que é a mentira que o projeto persegue. O teste da contagem de subpassos pegou: o “somador de transistores” custava exatamente o mesmo que o de portas.
  3. Um PMOS cortado no Vdd relatava nível alto, e a tela o desenhava aceso justamente por não estar conduzindo. Cortado não carrega nível nenhum.

O nó de saída aprendeu a separar três coisas que antes se confundiam, e cada uma tem resposta diferente: rede ainda não acionada (o circuito aquecendo — não responde), fio faltando até o nó (erro, com a contagem de ramos), e rede mal montada (flutuação ou curto). Para isso o transistor relata todo tick, com comandado como campo: sumir do relato apagaria a diferença entre “não acordou” e “falta fio”.

Cinco mutantes mortos: expansão de bornes de uma volta só, nó ignorando falta de comando, nó ignorando a contagem de ramos, cin0 removido, e as redes de NAND e NOR trocadas.

O que falta: a unidade lógica aberta (hoje folha, declarado), e o bloco 7 (views da CPU e acabamento). A ULA da CPU segue com portas-folha de propósito — 32 bits em transistores são ~1600 deles, e a fatia desce por um caminho.

Estado: 402 testes unitários, 76 e2e, typecheck, boundaries e build verdes.


Acabamento: a caixa recolhida, e a codificação provada contra a spec

Data: 2026-08-29. Código: packages/depth-ui/src/Stage.tsx, packages/cpu-domain/src/encoding.test.ts.

A ULA era desenhada parada com um somador de 32 bits girando lá dentro. Um contêiner não emite — quem emite são os filhos —, então o livro-caixa não tinha nada no nome dele e a caixa recolhida ficava inerte. Neste desenho, parado quer dizer “não fez nada”: era o desenho afirmando o contrário do que o modelo dizia. Agora caixa recolhida responde pelo interior dela, que é para isso que ela existe. Caixa aberta segue sendo moldura e não se mexe — ali o leitor vê os filhos agirem, e animar a moldura contaria a mesma coisa duas vezes. Foi achado olhando o lab, não rodando teste; virou e2e, e o e2e falha sem a correção.

A segunda pendência declarada da §7 fechou, por outro caminho. O diferencial confere o modelo contra o intérprete de referência, e os dois compartilham isa.ts: isso prova execução e não codificação. Medido: trocando o funct7 do sub, o diferencial passa nos dez programas e ninguém percebe.

encoding.test.ts é a metade que faltava — as vinte e sete instruções do subconjunto, com a palavra escrita à mão a partir do layout de campos do RV32I e a conta no comentário. As vinte tabelas de isa.ts bateram de primeira; os dois mutantes de opcode morrem só aqui. Um teste de cobertura recusa instrução nova que entre sem passar pela tabela.

O que continua descoberto está declarado na §7.1 da spec: a tabela prova a palavra, não a semântica dela. Canto de comportamento que a spec descreve em prosa — estouro em deslocamento, sinal em slt, jalr zerando o bit baixo — nós dois podemos ter lido errado do mesmo jeito, e é isso que um emulador de terceiro daria.

Estado: 484 testes unitários, 78 e2e, typecheck, boundaries e build verdes.


Refino da CPU: o que o leitor vê

Data: 2026-08-29. Código: packages/depth-ui/src/Stage.tsx, packages/cpu-domain/src/{views,labels,transistors}.ts, os dois labs.

Tudo daqui saiu de olhar as telas, não de rodar teste. Foram quatro coisas.

A caixa recolhida era desenhada como moldura. A correção anterior deu data-ativo à ULA recolhida e eu disse que estava resolvido — não estava: contêiner é moldura, moldura não tem engrenagem nem gesto, e a ULA continuava parecendo morta com um somador de 32 bits girando lá dentro. O atributo certo, o desenho errado. Agora uma caixa recolhida toma a família do que ela guarda, porque recolhida ela não é moldura: ela é a coisa. O e2e passou a cobrar o desenho (data-familia, engrenagem) e não só o atributo — e o mutante, que antes sobrevivia, morre.

A vista da ULA era inútil. A montada na hora enfileirava as seis peças numa linha só, cortada no topo, com meia tela vazia embaixo. Virou vista à mão: o somador é a peça larga do meio porque é a cara, e a unidade lógica corre por baixo — as duas respostas chegam no mesmo mux e uma custa trinta e duas vezes mais.

A porta CMOS era desenhada como fila. Correto e inútil: o que separa NAND de NOR é série contra paralelo, e isso é propriedade da forma, que some quando tudo vira uma linha. Agora é esquemático — alimentação em cima, terra embaixo, nó no meio, e dois transistores lado a lado querendo dizer paralelo. Os transistores também passaram a dizer qual entrada os comanda: dois PMOS iguais num NAND fazem coisas diferentes, e sem o nome o desenho mostrava a forma certa e deixava o leitor sem saber qual é qual. São setenta e seis vistas, geradas — escrever à mão garantiria uma esquecida sem ninguém notar, e viewDisagreement confere todas.

Os labs estavam em português num site em inglês. O leitor lia um parágrafo numa língua e mexia num painel noutra. Decisão do Luigi: inglês agora, camada de idioma depois — e por isso todo rótulo de modelo passa por labels.ts, um mapa só, para a tradução futura ser trocar um arquivo em vez de caçar string em vinte. Comentários e identificadores seguem em português: não são lidos por quem visita o site. Uma guarda por caractere acentuado impede o português de voltar sozinho num rótulo novo.

Estado: 489 testes unitários, 78 e2e, typecheck, boundaries e build verdes.


O mapa do RISC-V, e três coisas que o site dizia errado

Data: 2026-08-29. Código: apps/site/src/data/{roadmap,roadmap-riscv,handbooks}.ts, apps/site/src/components/Roadmap.{tsx,css}, packages/depth-core/src/inlets.test.ts.

O handbook do RISC-V ganhou o mapa. Ele não tinha, e o motivo estava escrito no código: desenhar o caminho antes de o modelo existir seria prometer o que não foi andado. O modelo existe — executa RV32I e abre até o transistor —, então a razão caiu. O componente do mapa deixou de conhecer só o do OTel: a espinha, as fases e os anexos viram um RoadmapMap que se passa a ele.

Três defeitos, e os três da mesma família: a página afirmando o que não é.

  1. O catálogo anunciava como “coming” um lab que já estava no ar. Ninguém mentiu de propósito — havia duas listas escritas à mão para o mesmo fato, e elas divergiram. A correção não foi acertar a lista: foi juntar as fontes, e os labs do RISC-V passaram a sair do mapa, como os do OTel já saíam. Três testes travam o resto: item pronto tem para onde levar, item não escrito não leva a lugar nenhum, e mapa e lista contam a mesma história.
  2. Um nó do mapa do OTel estava marcado como pronto com link #. Ele abria como link e não levava a nada. A Entrega 2 mudou de rumo e aquele lab nunca foi construído — então ele é caminho declarado, como os outros.
  3. A proporção do mapa estava escrita no CSS com a medida do OTel. Um mapa de outra altura fazia o desenho e as coordenadas dos nós escalarem diferente, e as linhas paravam longe das caixas: fio ligado a lugar nenhum. A proporção passou a vir do mapa.

E um quarto, achado por acidente, que é o mais interessante. A suíte reprovou uma vez num property test dos bornes — o tipo de coisa que se reexecuta e se esquece. Reexecutar passou quatro vezes seguidas. Mas sorteio que falha raro quer dizer contraexemplo raro, não instabilidade: o teste tirava trinta pares de mil seiscentos e um. A varredura completa achou o único par que discorda, a=0, b=0, e o defeito é a conflação de sempre — o atalho guardava a === 0 && b === 0 e devolvia nada, enquanto a composição entregava um zero. Não chegar nada é diferente de chegar zero, e era o atalho que estava errado. O sorteio virou varredura: o espaço é pequeno o bastante para não sobrar sorte.

Estado: 497 testes unitários, 84 e2e, typecheck, boundaries e build verdes.


Entrega 1 do refino: o que a tela afirma passa a ser verdade

Data: 2026-08-30. Desenho: docs/superpowers/specs/2026-08-30-refino-grafico-e-pedagogico-design.md. Código: packages/depth-ui/src/{stage.css,Stage.tsx,kinds.ts}, packages/depth-core/src/model.ts, packages/cpu-domain/src/{labels.ts,carga.ts,datapath.ts,gates.ts,alu.ts,transistors.ts}, apps/site/src/components/Explorer.tsx.

Isto saiu de uma auditoria agêntica em três frentes: um agente nas telas reais pelo navegador, um no modelo da CPU, um na ficha do objeto. As três acharam coisas que nenhum teste pegava, e a primeira delas é a razão de esta entrega existir.

A porta acesa não existia. O lab promete por escrito que “uma porta acesa é uma porta cuja saída é 1”. O agente mediu fill e stroke com a=b=0 e com a=6,b=7: o mesmo valor. Três causas, e as três da mesma família — aceso era desenhado como acontecimento, não como estado. (a) O aceso vinha inteiro de uma transição cujo primeiro quadro era a tinta do apagado; com a simulação rodando, a peça reiniciava a transição a cada tick e vivia perto do quadro zero, saindo mais escura que a porta em 0 nos dois temas. (b) O halo que o olho lê como “acesa” pertencia a data-ativo — a quem rodou, não a quem disse um —, então a porta em 0 brilhava e a em 1 não. São as duas coisas que a troca de codificação foi feita para separar, recoladas pelo desenho. (c) Na moldura, fill-opacity: 0 anulava o preenchimento e o contorno de acesa tinha a mesma cor do de apagada: sobrava meio pixel.

Agora o repouso já é aceso e a animação só pulsa por cima — parte de mais aceso e assenta no aceso, então nenhum quadro dela mente. O nível alto ganhou token próprio: misturado na cor do dado, “acesa” caía na mesma tinta de “isto é linha de dado”, e a mistura escurecia a superfície nos dois temas.

Duas armadilhas de teste caíram junto, e valem mais que o conserto. A checagem de permanência trocava o tema e esperava 1,2s, e o site restaurava o tema no meio: a segunda amostra media outro tema em vez de outro instante. E o laço “nos dois temas” não trocava tema nenhum — nem escrevendo o atributo, nem emulando a preferência do sistema. Passava medindo o mesmo caso duas vezes. Regra que fica: teste que varre dois casos prova, no fim, que os dois casos aconteceram.

A ficha sobrevivia ao enquadramento. Ler a ficha do NAND e voltar ao topo deixava o painel explicando peça que não está mais desenhada. A correção não é limpar no clique da trilha — isso é regra sobre um botão, e sobraria todo outro caminho de navegação. A seleção passa a carregar o enquadramento em que foi feita, e quem lê descarta o que não é dali: não há limpeza a esquecer. Conferir contra o que o palco desenhou não serve, porque o interior aparece ou não conforme o nível de detalhe, que é conta do palco.

Os sinais de controle vazavam em português. ler, escrever, nada, ula, mem, pc4 iam crus para a linha tracejada de um site em inglês — e nem no idioma certo seriam os nomes certos: o aluno procura MemRead, MemWrite, ALUSrc, MemToReg. A guarda de idioma não pegou, e não por descuido: ela varre acento, e nenhuma daquelas palavras tem. Quem pega agora é cobertura: todo par campo/valor que a tabela de controle emite, para todo o ISA, tem de ter tradução — com a tabela rodada de verdade, porque listar os valores à mão seria a segunda fonte. O mapa é por campo e valor: nada quer dizer “não acessa memória” num sinal e “não escreve no banco” noutro. Caiu junto uma duplicação que era a causa de a varredura não fechar: a tabela dizia acesso/escrita/desvio e o payload dizia modo/fonte/tipo.

A chave entrou no catálogo do motor. O transistor era router: herdava o trapézio do mux e a ficha o explicava com o texto do mux. Símbolo errado e explicação errada, no nível mais fundo do modelo. Uma chave não escolhe entre entradas — deixa passar ou não, e quem manda é o terminal de porta. É objeto que faltava no catálogo, não remendo de CPU: válvula e relé são o mesmo objeto. Ela não ganha engrenagem, porque deixar passar não é processar. A guarda de fronteira pegou a primeira versão, em que eu havia escrito “transistor” nos comentários do motor — e estava certa.

Rótulo igual ao id não é rótulo. Trinta e dois objetos se chamavam bit0…bit31 na tela. Virou guarda geral.

E a ULA declara a segunda simplificação: sub também não desce pelo somador. Estava escrito que a lógica bit a bit fica de fora, e quem lia só isso concluía que o resto desce pelo silício. Meia declaração é meia mentira.

Um achado da auditoria NÃO virou conserto. A ordem “bit1, bit2, bit3 … bit14, bit13, bit12” no somador da ULA foi lida como bagunça e é serpentina deliberada — a linha ímpar volta ao contrário para o vai-um não pular a largura toda. O defeito ali é outro: a dobra é invisível, então quem olha lê como desordem. Fica para a entrega do desenho. Vale como método: relatório de agente é evidência, não veredito.

Estado: 668 testes unitários, 145 e2e, typecheck, boundaries (74 arquivos) e build verdes.


Entrega 2 e a arrumação: a cor diz o que a coisa é, e o espaguete vira número

Data: 2026-08-30. Desenho: docs/superpowers/specs/2026-08-30-refino-grafico-e-pedagogico-design.md.

O catálogo da linguagem visual. Cor e forma passam a morar num lugar só, lidos por sentido e nunca por valor, com scripts/check-catalogo.mjs reprovando tinta escrita fora dele. A disciplina já funcionava — os três labs escolheram bem, cada um por sua conta — mas disciplina não é sistema: nada impedia o quarto lab de escolher diferente, e no dia em que escolhesse ninguém seria avisado.

Duas tintas, dois registros: no diagrama de blocos preta é dado e vermelha é controle; no esquemático vermelha é alimentação e preta é terra. Não é ambiguidade — são duas linguagens de desenho, nunca aparecem no mesmo quadro, e a trilha diz em qual o leitor está. O que era ambíguo era o sentido ficar implícito. A vista declara o registro; adivinhar pelo conteúdo erraria calado no dia em que um esquemático não tivesse chave nenhuma.

A guarda achou o que ninguém procurava. O CSS do MOTOR tinha seletores data-kind="instrucao", "escrita", "guardar" e "pulso". A guarda de fronteira não via porque só varria TypeScript — uma fronteira que vigia meia linguagem vigia meia fronteira. E a correção não foi pôr as palavras na lista: a regra certa é sobre o substantivo. O kind de uma mensagem é palavra do domínio pela definição do próprio motor, então a guarda passou a perguntar o inverso — está no vocabulário do motor? —, lendo a lista do model.ts na hora, porque copiada ela envelheceria em silêncio.

Um defeito que só a tela pegou. Separei os trilhos por source contra sink, e no modelo os DOIS são source: saíram os dois vermelhos. Passou em typecheck, fronteira, catálogo e 149 e2e. O que de fato os separa é o nível que cada um dirige, que é a física da coisa.

A leitura do somador estava invertida, e discordava de três coisas ao mesmo tempo: do texto do lab (“watch the carry climb”), do número escrito — de cima para baixo dava 1011 onde a caixa dizia 1101 — e do próprio desenho, com o vem-de-trás saltando a figura inteira. Nada disso quebrava teste.

Espaguete virou número, medido nos path que a página pintou. Achou 21 sobreposições no lab da CPU e 56 no das portas, todas com ponta em comum: leque e convergência desenhados como N linhas empilhadas, que se leem como uma linha só. Daí saíram o pontinho de junção (o T ganha, o X não — e a ausência dele é que diz que dois fios não se falam) e a memória do roteador, que desviava de caixas e ignorava fios.

E os pesos do roteador passaram a ser a hierarquia dos defeitos escrita como número: atravessar caixa é mentira e custa cem, repetir reta é ambiguidade e custa um, sair do centro é estética e custa um décimo de milésimo. Na mesma ordem de grandeza, bastavam duas retas repetidas para ele preferir cortar uma caixa.

A dívida que a medida expôs se fechou com uma regra. O somador cruzava 28 contra 16 do caminho de dados inteiro. Doze eram um leque sem ordem. Agora a saída mira o destino e a entrada mira a origem — regra local com efeito global: duas pontas ordenadas pelo que está do outro lado não se cruzam, e o feixe abre como pente. Cruzamentos: somador 28 → 4, caminho de dados 16 → 15, pilha 4.

Regra de método que saiu daqui: teste que varre dois casos prova, no fim, que os dois aconteceram. Dois testes meus fingiam cobrir dois temas e cobriam um; um terceiro comparava .first() de um conjunto que muda, que não é um elemento — passava sozinho e reprovava na suíte cheia.

Estado: 687 testes unitários, 155 e2e, typecheck, boundaries (80 arquivos), catálogo e build verdes.

2026-08-30 — O microprocessador genérico

Um modelo que existia fora daqui — desenhado à mão por um professor, em slides — foi reconstruído no motor, roda, e abre onde o slide não pode abrir. Deck de referência: Princípio de Funcionamento de um Microprocessador, de Prof. Filippo Valiante Filho (prof.valiante.info), usado com permissão dele.

O que faltava e agora existe: o ciclo de instrução como tempo. O caminho de dados do RISC-V abre dizendo “um ciclo por tick” — a instrução inteira acontece dentro de um tick, e busca, decodificação e execução existem só como profundidade topológica da acomodação. No micro, um tick é um micro-passo: uma transferência entre registradores. Vinte e nove deles para o programa do slide 16.

O motor ganhou kind: "sequencer", o primeiro da família controller — que existia desde o começo e não tinha nenhum. Ele guarda a fase entre ticks e emite só por linha de controle; validateWorld recusa aresta de dado saindo dele, e a mutação da guarda mata exatamente um teste. router não servia porque não lembra do tick passado, store mentiria na forma. A UC do RISC-V também passou a usá-lo: família que só um mundo usa é kind disfarçado.

A tabela do slide 43 virou oráculo, e as onze linhas bateram célula por célula, na primeira execução. É um documento que não controlamos decidindo quando estamos errados. O deck tem duas granularidades da mesma execução — 22 quadros de animação e 11 transações de barramento — e nós produzimos as duas de um livro-caixa só, que é a tese do projeto provada num caso que não inventamos.

A profundidade vai a nove níveis, dois a mais do que o desenho previa: sistema › cpu › processador › ula › somador › bit0 › xor › nand › vdd, 930 objetos. O deck desenha a ULA como duas caixas e para ali, porque slide não abre. E o reuso não custou uma linha de domínio nova: gates.ts e transistors.ts saíram da tarefa sem uma modificação. A largura da ULA virou argumento com 32 como padrão, e o RISC-V não mudou.

Três defeitos que só a tela pegou, e dois deles estavam no plano escrito antes:

  1. A tabela preenchia coluna por diferença, e o segundo ADD recarregava o IR com 8B, o byte que já estava lá — célula vazia, tabela afirmando que a instrução não foi buscada naquele ciclo. Transferência real, invisível. Preenche-se por qual ordem disparou, não pelo delta: reescrever o mesmo valor é evento, e diff não vê evento.
  2. Ao vivo, a coluna de instrução mostrava STORE 0000 antes de os bytes do endereço serem buscados — completava com zero o que a máquina ainda não tinha lido. Passava em todos os testes, porque o oráculo compara a execução completa. Fica em branco até os bytes existirem.
  3. A ficha descrevia um objeto só em vocabulário do motor: quem clicava em MAR lia o que é um buffer. Nasceu DESCRICOES, por id, com teste bidirecional que tira a lista de ids da própria árvore — lista à mão é a fonte duplicada de sempre.

Um erro no material de referência. O slide 44 dá o código de máquina do 8085 como 21 20 00 para LXI H,2000h. O 8085 guarda imediato de 16 bits com o byte baixo primeiro (8080/8085 Assembly Language Programming Manual), então o correto é 21 00 20 — como está no deck, o programa escreveria em 0020h. O artigo da ponte usa a forma certa e faz da divergência um dos dois pontos em que as duas máquinas discordam.

O handbook passou a se chamar pelo que é: RISC-V Visual Handbook → CPU Visual Handbook (o campo model já dizia cpu.model antes de nós). O mapa reordenou — o genérico na fase 4 (The instruction cycle), o RISC-V na 5 (The datapath, all at once) — e o vazio “The control lines of one opcode” foi absorvido, não substituído: uma UC multiciclo é as linhas de controle de um opcode, desenroladas no tempo. Nenhum placeholder novo nasceu; um morreu.

Método: um agente reportou pnpm typecheck limpo quando não estava, e a quebra foi commitada — Kind tem duas tabelas exaustivas, a FAMILY do motor e a KINDS do depth-ui, e é a segunda que impede um kind de aparecer na tela sem ninguém saber dizer o que ele é. Quem pegou foi o agente seguinte, rodando o comando de verdade. Relatório de agente é evidência, não veredito.

Espaguete do lab novo: 7 cruzamentos, 0 sobreposições cegas, teto cravado no medido.

Estado: 773 testes unitários, 179 e2e, typecheck, boundaries (81 arquivos), catálogo (10 arquivos) e build (29 páginas) verdes.


Entrega 4 — O lab dos provedores, o primeiro do otel.model ✅

Data: 2026-08-31/09-01. Branch main. Spec: docs/superpowers/specs/2026-08-31-provedores-otel-design.md Plano: docs/superpowers/plans/2026-08-31-lab-provedores-otel.md (os dois vieram das PRs kiro/lab-provedores-otel e kiro/curriculo-otel).

O que ficou pronto. packages/otel-domain/src/providers/ — a árvore host → process → {tracer,logger,meter}-provider, com o collector filho do host e não do processo. labs/providers no ar, na fase 3 do mapa, pareado com o artigo who-owns-the-pipeline.

A tese, provada por teste e não por prosa: o envelope do OTLP é a árvore de objetos do SDK. envelope.test.ts cobra o invariante nas duas metades — (⊆) o envelope não inventa campo, (⊇) nenhuma placa deixa de aparecer nele — e ele passou por teste de mutação: zerando o recurso sobre o qual o exportador fecha, três testes caem.

Nenhum kind novo nasceu. O catálogo de onze cobriu o SDK inteiro, e há teste cobrando isso. O Resource é static — placa não tem porta e não é fiada, e por isso ele chega ao envelope porque a fábrica do exportador fecha sobre ele, que é o que o SDK faz.

O risco nº 1 do plano caiu: validateWorld aceita a linha de controle entre o sampler do tracer-provider e o trace-gate do logger-provider. É a única linha do lab que cruza fronteira de provider, e é a que ensina que baixar a amostragem de traces apaga logs.

Um tick é um segundo neste mundo. É o que faz os padrões da spec caberem na tela sem serem reescritos: scheduledDelayMillis 5 000 são 5 ticks, exportIntervalMillis 60 000 são 60, e o 12× de F4 é exportInterval / scheduledDelay — número da spec, não número escolhido para a demonstração.

Cinco fenômenos, nenhum roteirizado, todos otelWorld mais um parâmetro. O que vale guardar de F4: a fila cheia recusa e o span morre; o banco de pontos colapsa e a soma é conservada. Mesmo problema — memória finita —, duas mentiras diferentes, e cada uma tem forma própria no desenho.

Três defeitos que só a tela pegou

Auditoria visual depois de o lab estar verde em tudo. Nenhum dos três tinha teste que os pegasse, e o primeiro é da classe que o projeto trata como inaceitável.

  1. O descarte não era desenhado. @drop não está na árvore, então o fio inteiro sumia do palco — e isso faz descarte deliberado e fio esquecido virarem o mesmo desenho. Um amostrador de três saídas aparecia com duas. Consertado no motor (Stage.tsx), com terminal próprio, sem seta — seta aponta para um destino, e descarte é a ausência de um. Nenhum outro domínio fia para DROP, então o desenho dos outros três labs não mudou.
  2. A linha de controle entre dois providers contornava o desenho inteiro pela direita. A folga entre as molduras era 8, e o roteador só desce pela borda com mais de 8. Passou a 16.
  3. Não havia como alternar entre as três views de provider sem saber clicar duas vezes na caixa certa — e a comparação por superposição (R3) é o que entrega F4.

Mais dois, e os piores: a carga não era desenhada

Achados na segunda auditoria de tela, com o lab já verde em tudo.

  1. Vinte e um fios na tela e zero bolinhas. O motor entrega na folha de entrada de um contêiner; o desenho conhece as caixas da vista. Ninguém traduzia de um para o outro, então a chave do trilho nunca casava e a carga simplesmente não era desenhada. Só funcionava em vista cujos objetos são todos folha — que é o caso dos três labs antigos, e por isso o defeito viveu escondido. InFlight ganhou port para o descarte saber por qual terminal a carga morre.
  2. A cor por espécie nunca aplicou. O data-especie está no grupo; o seletor pedia o atributo na própria carga. As cinco espécies existiam no DOM e o desenho pintava todas da mesma cor, calado, desde que a regra nasceu. Testado por mutação: com o seletor antigo, o teste de tinta cai.

A carga muda de forma no caminho

Pedido do Luigi, e ele nomeia o que o lab existe para mostrar: “a bolinha vai se modificando no caminho — passa por um batch processor e vira um conjunto de bolinhas, passa no exporter e vira um file”.

A forma sai de três fatos que o motor já tem, e de nenhum vocabulário de domínio:

Fato do motorFormaO que ela afirma
o fio declara largurabarraN vias em paralelo — a notação do esquemático
a aresta é um canaldocumentoo que atravessa um canal sai serializado: é o envelope
weight > 1punhado de pontosvárias coisas viajando juntas porque alguém decidiu
nada dissopontouma coisa

Um lote não é um círculo maior — isso diria “um span mais gordo”. É vários pontos, que é a lição inteira do processador em lote. E o ×N ao lado dá a conta exata, porque sete é o teto do que se lê como punhado.

A espécie foi remapeada pela régua do catálogo, que diz que o número é proeminência e não categoria: o descarte vem primeiro (num lab sobre quem decide o que sai, a carga que morre é o que não se pode perder de vista), e span e lote dividem a tinta, porque um lote é spans e a forma já os separa — a cor que sobra vai para o envelope, que dali para a frente não é mais spans.

Um conserto de medida, e não de folga

O espaguete passou a medir por escopo. Um interior é um espaço de coordenadas próprio, então dois fios de caixas diferentes podem ter o mesmo d e não se tocarem na tela. As três views de provider compartilham moldura de propósito — é R3 —, então os interiores saem com paths idênticos, e a medida sem escopo acusava dezoito sobreposições cegas onde não há uma. Os quatro tetos antigos foram remedidos com a mudança e não mexeram (15, 4, 4, 7). O do lab novo é 1 cruzamento, 0 sobreposições cegas, cravado no medido.

Dois defeitos herdados das PRs do Kiro

Nenhum dos dois era visível enquanto o handbook de OTel não tinha lab publicado — que é o jeito ruim de achar defeito.

  1. landing.spec.ts cravava "0 of 13" e a trilha cresceu para 18. O total passa a sair do próprio mapa: uma fonte só por fato.
  2. OTEL_LABS não copiava o href do mapa. O teste de “item pronto tem para onde levar” só tem o que cobrar quando existe um item pronto — é a mesma classe que a lista escrita à mão do lado da CPU já tinha tido.

O que ficou pendente

  • Bloco E — a contraparte real (labs/providers/ com compose e fixture OTLP). É o princípio 3 da spec do handbook, e sem ele o lab não está pronto pela régua do projeto. O modelo está honesto contra a spec escrita; falta a prova mecânica contra tráfego real.
  • Task 16 — docs/authoring.md continua desatualizado, mandando escrever Scenario<S>, que o depth-core marca como andaime proibido em código novo.
  • As duas saídas do amostrador que vão para o mesmo destino se desenham por cima. sampled e recorded entram ambas no span-processors, e o roteador dá o mesmo caminho para as duas. Não é sobreposição cega (compartilham as duas pontas) e a contagem do painel separa as três decisões — mas no desenho a porta do meio só se distingue pelo terminal de descarte da terceira. Resolver pede borne nomeado na pipeline, ou o roteador sabendo separar fios irmãos com o mesmo par de pontas.
  • O LOD tem um teto para três molduras empilhadas: min(w/W, h/H) não passa de ~0,30 com três caixas na vertical, então o interior delas aparece a ~22% na vista do processo. A comparação de verdade é pelos botões de enquadramento.

A rodada da auditoria visual do Luigi

Ele olhou o lab rodando e a lista que saiu dali é o que este registro existe para guardar.

A costura API ↔ SDK virou desenho. O app era uma caixa só. Agora há uma moldura application — o código e a API que ele chama, os dois do mesmo lado da costura — e uma moldura sdk com os três provedores. A API é channel, porque ela transporta e nunca altera: é literalmente o que ela faz. Trocar tudo o que está à direita não muda uma linha do que está à esquerda, e é por isso que uma biblioteca pode ser instrumentada sem escolher o SDK de ninguém — e é a explicação do silêncio de F5: sem SDK, a moldura da direita não existe, e do lado de cá ninguém sabe disso.

Contrapressão, e ela é uma aresta. Um parâmetro corta o canal para o Collector. O exportador exporta um lote por vez e espera; com o outro lado mudo ele não termina, e avisa a fila por uma linha de controle que anda contra o fluxo — a única do lab. A fila para de entregar, enche, e a partir daí recusa. A perda começa três peças antes de onde o problema está, que é por que ela surpreende. Nem o ForceFlush passa por cima. E há a distinção que o painel mostra com número dos dois lados: o que o exportador segura está preso, não perdido, e volta a andar quando o canal volta; o que a fila recusou não volta.

A fila enche e esvazia na tela (fills), e o banco de pontos usa a mesma barra contra o limite de cardinalidade — é ali que o contraste de F4 fica físico: um transborda e recusa, o outro colapsa.

A carga de geração é modulável (spans, logs e medições por segundo), que é o que permite passar do que o pipeline drena e ver a contrapressão acontecer.

O descarte virou o terra do esquemático — três traços que encurtam. É o símbolo mais universal que existe para “aqui acaba, e não continua em lugar nenhum”, que é o que o descarte é. A palavra fica junto porque isto é material de ensino.

A porta e o fio, e o preço de dizer a verdade

Achado dele: “as setas dos fluxos não estão se conectando à entrada deles”. Estava certo, e era estrutural: as portas eram distribuídas em alturas iguais pela borda e o roteador escolhia a altura por conta própria. O desenho mostrava uma entrada e uma linha que não a tocava.

A correção passou por três tentativas, e as duas primeiras ensinam:

  1. desenhar a porta onde o fio chega — conserta o alinhamento e esconde o defeito de baixo: três portas diferentes ligando as mesmas duas caixas miram o mesmo ponto, saem na mesma altura, e viram uma linha só. O teste de junção do espaguete pegou;
  2. a porta manda no fio — quebrou o somador (4 → 27 cruzamentos), porque a mira ordena o leque e foi ela que levou aquele desenho de 28 para 4. Regra nova não revoga regra que pagou;
  3. desvio em torno da mira, e só quando o par de caixas tem mais de uma porta — que é a única situação em que há empate a desfazer. A mira continua mandando na ordem; o desvio só separa quem ela empatou.

O somador foi de 4 para 5 cruzamentos, e o número subiu porque o desenho passou a dizer a verdade: as duas parcelas que entram em cada bit eram desenhadas uma por cima da outra, e o leitor via uma ligação onde existem duas. Um cruzamento a mais, quatro ligações que existiam e não podiam ser vistas. Os outros três labs não mexeram (15, 4, 7), e o lab dos provedores foi para 0.

A rodada do fluxo entre níveis, e o que a investigação salvou

Ele disse que os fluxos entre níveis, as animações, as setas e os fios ainda estavam errados, e autorizou mexer no motor. A investigação veio antes de qualquer conserto, e o primeiro achado foi um falso positivo: medindo na aba de automação, a carga aparecia congelada em offset-distance: 0% com a animação “running” e currentTime parado. Parecia defeito grave. Não era: requestAnimationFrame não dispara naquela aba, porque ela não pinta — a linha do tempo do documento está parada. Um “conserto” ali teria mexido em código que funciona. Screenshot força pintura; medição de tempo naquela aba não vale.

O defeito real, e ele é da pior espécie do projeto: a vista de fora discordava do próprio interior. Medido na tela: uma linha chegava na moldura do SDK em y≈817, e lá dentro nasciam três entradas, em 734, 818 e 902. Duas nasciam do nada, a oitenta e oito unidades de qualquer linha existente.

A causa era a regra de agregação: duas folhas dentro das mesmas duas caixas viravam uma aresta só. A regra é certa — trinta e dois filhos idênticos não viram trinta e duas linhas —, mas a chave dela não distinguia travessias diferentes. Confirmado nos dois labs, e o segundo caso só se prova no modelo: as quatro linhas de controle que entram na lógica combinacional do caminho de dados pousam em quatro peças diferentes (ula, mux-operando, mux-escrita, desvio), e eram desenhadas como uma. Latente, porque naquele zoom o interior está fechado.

Duas tentativas erradas antes da certa, e as duas ensinam:

  1. pôr a porta na chave — desagrega os três sinais, e leva o caminho de dados de 15 para 26 cruzamentos, porque desagrega também o que não precisava;
  2. a porta mandar no fio (rodada anterior) — quebrou o somador de 4 para 27, porque a mira ordena o leque e foi ela que o levou de 28 para 4.

A resposta é deixar o nível de detalhe responder, como ele já responde por todo o resto do palco: com o interior aberto, cada travessia é uma linha e pousa na peça em que de fato entra; com o interior fechado, uma linha só, marcada como feixe de N — a mesma notação que o esquemático usa para um barramento. De longe o desenho informa a conta em vez de esconder a diferença; de perto o feixe se abre nos fios que o compõem. Os quatro labs antigos não mexeram um cruzamento (15, 5, 4, 7).

E o que fecha a continuidade: a linha de fora passou a mirar a peça em que ela entra lá dentro. Os dois pontos já eram conhecidos — a travessia os usava para continuar a linha — enquanto o traço de fora era roteado sem saber deles. Duas metades da mesma ligação desenhadas por dois critérios diferentes. Agora as três linhas terminam exatamente em 734, 818 e 902, que é onde as três entradas do interior começam.

A seta apontava a cor errada em todo fio de controle. O conteúdo de um <marker> não herda do elemento que o referencia — ele herda do <defs> —, então currentColor resolvia uma tinta só para o desenho inteiro. Toda linha vermelha terminava numa ponta da cor do dado, e fio aceso não acendia a ponta. context-stroke é o mecanismo que o SVG tem para isso, com currentColor antes como reserva.

O invariante virou apps/site/tests/travessia.spec.ts, e ele cobra geometria: cada linha que chega numa moldura aberta termina onde uma entrada do interior começa, e as duas contagens batem. Provado por mutação — voltando a agregar, ele cai dizendo qual moldura e quantas linhas. E o lab dos provedores é obrigado a ter moldura aberta, senão os dez casos pulariam e o invariante ficaria sem guarda, calado.

O lab dos provedores foi de 0 para 1 cruzamento: o leque de três precisa cruzar uma vez. Um cruzamento pelo preço de duas ligações que existiam e não podiam ser vistas.

O palco cabe o leitor — e o defeito que a mudança criou

Pedido dele: “o bloco dos fluxos precisa ser redimensionável, possível de abrir em tela cheia, os textos estão pequenos”.

O diagnóstico não era o óbvio. O texto do palco é medido em unidades da vista, então ele chega na tela multiplicado pela escala da projeção — e a escala era 0,62 num monitor de 2560, porque o palco estava preso pela largura: 744 pixels de desenho espremidos entre a ficha (320) e o painel do lab (384). Altura sozinha não aumenta texto nenhum.

Pior no caso que importa: num laptop de 1280 o mesmo rótulo chegava com 4,5 pixels. A primeira medição foi no monitor grande e teria fechado a questão errado; foi o teste, rodando no viewport de 1280, que mostrou.

O que entrou: altura própria e redimensionável pelo canto (resize: vertical), botão de tela cheia no Explorer inteiro — trilha e ficha vão junto, porque em tela cheia a pessoa continua precisando saber onde está —, ficha e painel do lab descendo até 96rem em vez de 70rem, e o tipo do rótulo de 11 para 12 unidades. Resultado medido: 4,5 → 8,4px no laptop, ~10,4px no monitor grande, e ~19px em tela cheia.

E a mudança criou um defeito, que o teste do zoom da CPU pegou na mesma rodada. Com altura própria, o preserveAspectRatio passa a deixar faixa vazia dos dois lados do desenho — e as contas do zoom e do arraste mapeavam a caixa inteira linearmente para o viewBox. Enquanto a altura saía da proporção, as duas formas eram iguais e a conta estava certa; com altura própria, deixou de estar, e a roda do mouse passou a ampliar um ponto em que o cursor não está. O somador da ULA parava em 7% de abertura.

A conta estava escrita duas vezes — uma no zoom, outra no arraste — e havia ainda uma terceira cópia em noDesenho, que não era usada por ninguém. Agora é uma só (encaixeDoQuadro), e as três viraram uma.

A legibilidade virou número em apps/site/tests/palco-legivel.spec.ts, e o piso é honesto sobre o que a página entrega sozinha: numa tela de 1280 por 720 uma vista de 1200 por 740 ocupa quase o monitor inteiro, e nenhum arranjo põe o rótulo em nove pixels ali dentro. Oito é o que a página dá; a tela cheia leva o mesmo rótulo a dezoito.

O que ficou pendente desta rodada

  • Arrastar processadores para a pipeline e ver a ordem de registro importar. É a decisão “parâmetro vs composição” da spec do handbook §4 levada a sério, e é rodada própria: mexe no palco (arrastar), no modelo (mundo que muda de forma sem recomeçar) e no currículo (a pipeline nasce com um filho, e ordem só ensina com dois).
  • A estampa da compressão mudando a forma do item. A forma hoje sai de três fatos que o motor já tem — largura declarada, aresta que é canal, peso maior que um — e nenhum deles é compressão. O caminho honesto é uma primitiva nova e neutra: Message ganhar quanto ela pesa na linha, separado de quantos itens ela leva. Aí comprimir é o mesmo ×N num pacote menor, e vale para qualquer domínio. Meia medida aqui seria o domínio escolhendo forma, que é a porta dos fundos que o catálogo existe para fechar.

Estado: 911 testes unitários, 201 e2e, typecheck, boundaries (81 arquivos), catálogo (12 arquivos) e build (33 páginas) verdes.

Entrega 5 — Os exercícios de instrumentação, e a contraparte real ✅

Plano: docs/superpowers/plans/2026-09-01-exercicios-de-instrumentacao.md, desenho em docs/superpowers/specs/2026-09-01-exercicios-de-instrumentacao-design.md. Executado em 08/09/2026 na entrega-4/lab-provedores-otel.

O handbook tinha duas coisas, e as duas são receptivas: o mecanismo rodando e a teoria ao lado. Faltava a terceira — a pessoa decidir e descobrir que estava errada, e não sobre o assunto, e sim no meio do código em que a decisão é tomada.

A contraparte real veio primeiro, e ela roda

labs/providers/: um checkout em Java instrumentado à mão contra o SDK fixado no pom.xml (BOM 1.65.0; opentelemetry-semconv 1.43.0, que é a exceção fora do BOM), um Collector imprimindo, e um compose.yaml de duas peças. Rodado de verdade antes de qualquer linha do exercício: 56 spans, em lotes de cinco segundos, com o service.name: checkout no bloco de recurso — uma camada acima dos spans — e InstrumentationScope checkout.http entre os dois. É o envelope da tese, impresso por um programa de terceiro.

O README daquele lab diz também o que não dá para ver ali: a fila enchendo, e o lote partindo por tempo em vez de tamanho. As duas são o que o lab da tela mostra e o terminal não, e é o argumento do projeto inteiro em duas linhas.

A resposta certa não é escrita: ela é extraída

O Checkout.java carrega marcadores (<handbook:trecho id="…">, <handbook:lacuna>), e apps/site/src/lib/exercicios.ts recorta no build — o frontmatter do Astro roda em Node, a ilha recebe o resultado pronto e nunca toca no sistema de arquivos. O tipo DefinicaoDeExercicio não tem campo para o código certo: não existe como escrevê-lo errado, nem como ele envelhecer em silêncio. Mude o arquivo e o exercício muda junto.

Teste de mutação, feito à mão: apagando a linha // <handbook:lacuna> do arquivo Java, três testes caem nomeando o exercício e dizendo o que falta (o trecho "onde-mora-o-service-name" não tem lacuna); restaurada, sete voltam a passar. Sem essa verificação, “a resposta é extraída” seria promessa.

Três defeitos que só rodar de verdade achou

  1. As variáveis do compose.yaml não faziam nada. OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_BSP_SCHEDULE_DELAY e OTEL_TRACES_SAMPLER_ARG são lidas pelo módulo de autoconfiguração — que este lab não usa de propósito, porque o assunto é o provedor montado à mão. O app exportava para localhost:4317 e não alcançava o Collector, com as três variáveis declaradas ao lado. Um arquivo prometendo o que não faz é a mentira silenciosa de sempre, agora em YAML. Agora o Java as lê à mão, e o que o compose diz é o que acontece.
  2. O volume não montava sob SELinux (permission denied em /etc/collector.yaml, com o Collector morrendo na largada): faltava o sufixo :z.
  3. A raiz do repositório contada por pastas quebrava no build. import.meta.url menos quatro níveis funciona no vitest e aponta para apps/labs/ no astro build, porque lá o módulo está empacotado em dist/chunks/. A âncora passou a ser um arquivo que só existe na raiz (pnpm-workspace.yaml), com erro que diz o que procurava.

Duas mudanças conscientes em cima do plano

  • Os distratores do E2 mudaram de forma. O plano dava span.setAttribute("service.name", …) e otel.getTracer("checkout") como blocos — sentenças inteiras para uma lacuna que é uma expressão dentro de Resource.getDefault().merge(…). Um distrator que não compila naquele lugar entrega a resposta pela forma, que é exatamente o que a §4.1 da spec proíbe. Os dois mal-entendidos continuam nomeados, agora dentro da forma da lacuna: deixar o recurso no padrão (e pôr o nome no span), e nomear o serviço com o nome do escopo.
  • O teste “o lab existe no mapa” mora em apps/site, e não no pacote de domínio: o mapa é do site, e otel-domain importando apps/site/src inverteria a dependência dos projetos do TypeScript e furaria o rootDir do pacote.

O placar

ovh:placar:v1, chave própria — ovh:progress:v1 guarda o que o leitor já leu, e mexer nela apagaria isso. Guarda acerto de primeira, e nada mais: contar acertos totais premiaria tentar até ficar verde, que é o hábito que este handbook não quer ensinar. O nó do mapa mostra n/m first try, e só onde há exercício — 0/0 se lê como “você não fez”, e a pessoa não deixou de fazer nada. Quantos exercícios um lab tem sai da lista de exercícios, e de nenhum outro lugar.

O que fica de fraqueza, declarado

O veredito é escrito à mão (D1 da spec). A resposta certa foi tirada disso — ela vem do arquivo —, e toda âncora é obrigatória e testada. Sobra que as explicações são autorais e podem envelhecer sem ninguém ser avisado. Aceito, e escrito aqui para não ser esquecido.

Uma observação de suíte: numa rodada completa do e2e, o teste “os dois handbooks contam o progresso separado” falhou uma vez e passou em vinte reexecuções seguintes, incluindo uma varredura de cinco rodadas dirigida a ele. A hipótese é a corrida de hidratação que o próprio teste documenta, sob carga de dois workers. Fica anotado em vez de dado por resolvido.

Estado: 933 testes unitários (22 novos), 212 e2e (6 novos), typecheck, boundaries (81 arquivos), catálogo (13 arquivos) e build (33 páginas) verdes. A contraparte real rodou à mão; a CI não compila Java, e isso é decisão.

Entrega 6 — Três artigos, e a fase 1 fechada

08/09/2026, mesma branch. O currículo de 31/08 previa 14 artigos e tinha três escritos; a teoria estava adiantada só onde havia lab. Entraram três, todos com fonte primária e âncora nos dois sentidos (fonte listada é citada, citação aponta para fonte que existe):

  • the-seam-between-signals (fase 1) — fecha a fase de entrada. A tese é que a dificuldade não está em nenhum dos três sinais e sim na junta, e que junta é um campo concreto que precisa existir e ser escrito igual dos dois lados: o TraceId no registro de log, o exemplar (o único caminho mecânico do agregado para a requisição), e o recurso, que é a junta que sai de graça e é a mais quebrada à mão. Tempo não é junta — é palpite com cara de gráfico.
  • a-trace-is-a-tree-nobody-owns (fase 2) — ninguém aloca id, ninguém guarda a árvore, e não existe evento de “trace completo” porque o protocolo agrupa por recurso e escopo, nunca por trace. Daí sai o resto: o trace degrada em vez de falhar (órfão que parece completo), a decisão de amostragem tem de viajar, e o link existe porque paternidade nem sempre é a verdade.
  • context-is-the-product (fase 2) — o Context é definido sem uma palavra de telemetria dentro, e é por isso que serve aos três sinais. Contexto segue a execução só onde alguém o carrega; e o baggage não vai para a telemetria sozinho, o que é decisão de privacidade e não esquecimento.

Estado: 954 testes unitários, 212 e2e, typecheck, boundaries, catálogo e build (36 páginas) verdes. Faltam oito artigos do currículo; a fase 1 está inteira.

Entrega 7 — Os túneis no cruzamento ✅

08/09/2026. Spec: docs/superpowers/specs/2026-09-08-tuneis-no-cruzamento-design.md; plano: docs/superpowers/plans/2026-09-08-tuneis-no-cruzamento.md.

O Luigi, olhando os labs: “estamos tendo quebras e sobreposição de faixas, assim como Factorio, podemos resolver isso com figuras como ‘túneis’”. E a razão de fundo, que ele escreveu depois: “grande parte da atratividade do nosso projeto é a visão gráfica e bonita que por sua natureza ensina”.

O desenho já tinha metade da resposta — o T ganha pontinho e o X não —, e essa metade é muda por ausência: quem não conhece a convenção não tem como saber que a falta do ponto significa alguma coisa. O túnel troca a ausência por uma figura. Onde dois fios se cruzam, um mergulha: some antes, reaparece depois, com uma boca em cada ponta apontando para o lado do fluxo — entra no chão andando e sai andando.

A decisão que sustenta o resto: o buraco é máscara

A implementação óbvia é partir o caminho do fio que mergulha em dois path. Ela limparia a tela e cegaria a medida: meada() lê os d que a página desenhou, e dois trechos que não se tocam não se cruzam. O caminho de dados teria ido de quinze cruzamentos para perto de zero sem uma linha ter melhorado, e o teto viraria a descrição de um estrago que ninguém mais vê.

Então o d continua inteiro e o vazio é uma <mask>. A medida lê o mesmo de antes, e não existe caminho pelo qual o túnel encoste no número — validação no ponto onde a violação é impossível, e não numa checagem que alguém tem de lembrar de escrever. Prova: os cinco tetos passaram sem mudar um dígito (15, 5, 4, 7, 1) com os túneis no ar.

Os pontos de túnel saem da mesma função que acha os cruzamentos (cruzamentos(), que meada() também usa), pela mesma razão de sempre: um segundo detector discordaria do primeiro no dia em que um dos dois ficasse errado.

Quem mergulha, e a regra que o plano previu e não existe

Duas regras: o fio mais estreito passa por baixo do mais largo (o barramento é a linha que o leitor está seguindo), e, empatados na largura, mergulha o em pé.

O plano previa uma terceira — a ordem das chaves, para o empate também na orientação. Ela não existe: um cruzamento é, por construção, um trecho deitado com um em pé, que é o que seCruzam exige. A segunda regra sempre resolve, e a terceira teria sido código morto fingindo decidir alguma coisa.

Um túnel pode cobrir dois cruzamentos

Cruzamentos vizinhos no mesmo trecho do mesmo fio viram um túnel, e não dois buracos colados — é o que o belt subterrâneo faz. Dois buracos colados se leem como fio picotado, que é exatamente a “quebra” que abriu o round. A spec falava em “um par de bocas por cruzamento”; o invariante implementado é o mais forte: todo cruzamento coberto, e nenhuma boca órfã.

O que a suíte cobra, e o que a mutação mostrou

Dois invariantes novos por lab, em apps/site/tests/espaguete.spec.ts, ambos por escopo (um interior é espaço de coordenadas próprio, e uma boca de dentro não explica um cruzamento de fora): nenhum cruzamento fica nu, e boca vem em par.

Teste de mutação, feito e conferido: apagando as bocas, cai o primeiro nomeando o ponto e o lab (cruzamento nu em 375,385 de o caminho de dados inteiro; 400,700 de o somador de quatro bits); desenhando uma boca por lacuna em vez de duas, cai o segundo nomeando o túnel (o túnel de |controle.op->logica tem 1 bocas). Restaurados, os vinte voltam a passar.

Túneis no ar: 14 no cpu, 7 no micro, 5 no gates.

Estado: 966 testes unitários, 222 e2e, typecheck, boundaries (83 arquivos), catálogo (13 arquivos) e build (36 páginas) verdes.

O que fica para a rodada seguinte, e é onde está o ganho

Com o cruzamento legível, cruzar deixa de ser um defeito caro — e os pesos do roteador (atravessar caixa 100, repetir reta 1) podem ser reequilibrados para ele organizar corredores em vez de fugir deles, que é o que produz os contornos longos parecidos com quebra. Ficou separado de propósito: com o túnel entrando sozinho e os tetos parados, dá para ver o que o túnel fez.

Aprovadas junto, para depois: carga na esteira item a item, esteira entupida (contrapressão visível), alerta na máquina, e a camada de circuito para a linha de controle. A régua vale para as quatro — toda figura tem de sair de um fato que o modelo já tem, senão é enfeite, e enfeite ensina errado.

Entrega 8 — As quatro figuras do Factorio ✅

08/09/2026. Spec: docs/superpowers/specs/2026-09-08-quatro-figuras-do-factorio-design.md.

A régua da rodada, e ela é o que separa isto de enfeite: toda figura tem de sair de um fato que o modelo já tem. Nenhuma das quatro precisou de kind novo, e em nenhuma delas o domínio escolhe forma — ele entrega números, como já fazia com especieDaCarga e conteudo.

O circuito virou plano

As arestas de controle saíram do laço das esteiras e foram para um grupo próprio, desenhado depois — e é a ordem no documento, e não a cor, que as põe por cima. Traço mais fino, porque comando não tem volume, e uma sombra curta, que é como o olho lê “está mais perto”.

Antes disso a diferença entre as duas redes era só a cor, que é um canal só, e o pior deles para quem tem dificuldade com vermelho e preto. Metade do diagrama de blocos da CPU é vermelha; agora essa metade tem altura própria.

A consequência, e ela está escrita para não passar calada: cruzamento entre planos deixa de precisar de túnel — dois planos que se cruzam não se confundem. O invariante “nenhum cruzamento fica nu” passa a valer entre iguais, e a contagem não muda: meada() continua contando todo cruzamento e os cinco tetos continuam onde estavam. Um cruzamento entre planos continua sendo um cruzamento; ele só não é uma ambiguidade.

apps/site/tests/planos.spec.ts segura a altura: nenhuma linha de controle fora do circuito, nenhuma esteira dentro dele, e o grupo do circuito depois do das esteiras no documento. Sem esse último, uma reordenação apaga a figura inteira sem quebrar teste nenhum.

O feixe anda em fila

Peso maior que um era uma roseta: até sete bolinhas agrupadas no mesmo ponto. Ela dizia “são vários” e mais nada. Agora eles andam em fila, ao longo da direção de viagem, e é o offset-rotate: auto — ligado só para o feixe — que os deita no fio. Um envelope ou uma barra girando chegariam de cabeça para baixo do outro lado de um cotovelo, e não ganhariam nada com isso.

O ganho não é estético: fila tem comprimento, e comprimento é o que torna “a esteira encheu” uma figura possível. Na tela, o lote de quatro saindo da fila agora é quatro coisas andando juntas, e não uma marca com ×4 ao lado.

formaDaCarga saiu de dentro do componente porque duas pessoas precisam da resposta — quem desenha o item e o grupo que o carrega, que só gira quando a carga é feixe. Calculada duas vezes, uma das duas ficaria para trás.

A fila com casas, e a máquina que avisa

A barra de nível respondia quanto; ela não respondia quantos. Com a capacidade declarada (capacidades, números, sem vocabulário) o palco troca a barra por casas — uma por item — e casa cheia é item, um por um, como numa esteira parada. Acima de 24 a barra continua: mil casas viram textura.

data-cheia endurece o contorno da caixa no limite. É a causa do descarte que sai ao lado; até aqui o palco mostrava só o resultado, que é a mesma diferença que o README da contraparte real aponta entre o terminal e o lab.

E o alerta: um triângulo sobre a máquina que está perdendo dado agora, no canto de cima à esquerda (o da direita é da engrenagem, e duas figuras no mesmo canto se estorvam). Sai do mesmo lugar da animação — a diferença do livro-caixa entre dois ticks, a mesma que já acende a aresta de descarte.

Desvio consciente da spec: ela previa uma propriedade nova ocupacao(id). Não foi preciso: fills já entregava o nível, e o que faltava era só de quantos cabe. Uma propriedade a menos, e o número que já existia não passou a ser escrito em dois lugares.

apps/site/tests/fila-cheia.spec.ts anda o caminho inteiro: no padrão da spec (2048) não há casa nenhuma e a barra responde; com a fila em 4 as casas aparecem, ela enche, o alerta surge — e some quando a fila volta para 2048. Esse último é o que impede o alerta de virar decoração de fundo: alerta que fica depois que o problema passou é a porta acesa por um valor que já foi.

Estado: 976 testes unitários, 230 e2e, typecheck, boundaries (83 arquivos), catálogo (13 arquivos) e build (36 páginas) verdes.

O que sobrou da rodada

Soltar os pesos do roteador continua sendo a próxima — e agora com mais razão: com o circuito noutro plano e o cruzamento tunelado, cruzar é barato de verdade.

Entrega 9 — As vistas fundas, e o nó órfão que ninguém media ✅

08/09/2026. O Luigi mandou uma captura de uma vista funda com a pergunta certa: “eae irmão, isso é entregue para você?”. Não era. O que o desenho mostrava ali era uma mancha branca de peças amontoadas na borda, fio atravessando caixa e um monte de coisa cortada.

A primeira descoberta é sobre a medida, e não sobre o desenho: espaguete.spec.ts mede a vista de abertura de cada lab. A bagunça foi morar exatamente onde ninguém olhava. Medido: a vista da ULA tinha 342 cruzamentos e 2345 sobreposições enquanto o lab dela passava com teto quinze.

Três causas, e nenhuma delas era o roteador

1. O interior invisível recebia linha uma a uma. O interior do somador de 32 bits era desenhado a sete por cento de opacidade — um borrão — e mesmo assim recebia trinta e duas linhas individuais, uma por bit, pousando em entradas que ninguém enxerga. A pergunta que o código fazia era “há interior?”; a certa é “dá para ler o que tem lá dentro?”, e ela já tinha resposta na rampa do nível de detalhe. Nasceu o LIMIAR_LEGIVEL: abaixo dele a ligação é uma linha marcada com o feixe de N, que é a notação do barramento e a mesma que o palco já usava com o interior fechado.

2. O interior invisível desenhava os próprios fios. Noventa e seis deles, a sete por cento, cruzando-se centenas de vezes. Agora uma camada abaixo do limiar é fantasma: desenha as formas e cala os fios. Forma antes de linha é a ordem em que o olho lê.

3. E a que explica a mancha: a chave do fio era do PAR DE CAIXAS. Com trinta e dois bits agregados numa caixa só, as trinta e duas ligações nasciam com a mesma key do React — e duas crianças com a mesma chave fazem a reconciliação errar: os nós velhos não saem, e cada tick empilha mais um. A vista de um bit da ULA tinha noventa e nove fios no documento para desenhar oito. O grupo dizia 8 e tinha 55 filhos.

Este é o defeito mais puro da coleção: o desenho dizendo uma coisa que o modelo não disse, em silêncio, e invisível para toda medida existente — porque todas mediam o que o modelo mandou desenhar, e não o que sobrou na tela. A chave passou a ser do fio, e há teste cobrando que nenhum fio apareça duas vezes no mesmo grupo, com o relógio andando (o defeito era cumulativo: uma foto do primeiro quadro não o via).

O roteador, que era o pedido

Duas mudanças, e as duas com a mesma disciplina — a rota canônica é tentada primeiro, e as alternativas só entram quando ela repete a reta de alguém:

  • a volta para trás era a única espécie de caminho que não olhava para os outros fios: coluna fixa, altura fixa. Duas caixas lado a lado mandando para trás produziam dois caminhos idênticos — sete sobreposições cegas na vista do processador do micro, e nenhum teste falando delas, porque teto de cruzamento não fala de sobreposição;
  • o corredor ganhou trilhos (±7), tomados só quando o do meio já está ocupado.

Duas tentativas foram medidas e descartadas, e valem o registro: abandonar a mira quando ela custa alguma coisa levou o lab das portas de 5 para 26 cruzamentos — a mira é o que ordena o leque, e ela não se toca; e oferecer os trilhos de saída, em vez de como segunda tentativa, mexia em fios sem motivo e trocava sobreposição por cruzamento.

O que a suíte passa a cobrar

apps/site/tests/profundidade.spec.ts, novo: cinco vistas fundas medidas por escopo e por plano, com teto de cruzamento e sobreposição cega em zero — sem teto, porque ambiguidade não se orça; a marca de feixe conferida (o somador de 32 anuncia os 32); e o teste do nó órfão acima.

VistaAntesDepois
ULA169 fios · 342 cruz. · 2345 sobrep.11 fios · 2 cruz. · 0 cegas
um bit da ULA99 fios no documento para 88 fios
processador do micro28 cruz. · 7 cegas42 cruz. · 0 cegas
caminho de dados (abertura)1515
sistema do genérico (abertura)75 (teto baixado)

O número do micro subiu e isso é honesto: as sete sobreposições cegas escondiam fios inteiros, e fio escondido não cruza nada. É o próximo alvo, e está declarado.

Estado: 984 testes unitários, 247 e2e, typecheck, boundaries, catálogo e build verdes.

O que fica

A porta lógica é desenhada com o trapézio do seletor, e ela não seleciona nada — combina duas entradas numa saída. A forma está dizendo a coisa errada, e o conserto não é um kind novo: é a forma seguir o leque real (uma entrada e várias saídas abre; várias entradas e uma saída fecha). Fica nomeado para a próxima rodada.

Entrega 10 — A forma segue o leque ✅

08/09/2026, fechando o que a Entrega 9 deixou nomeado. Pedido dele: “a forma tem que seguir o leque”.

O trapézio era desenhado sempre do mesmo jeito — largo à esquerda, estreito à direita — para todo router, com a descrição do mux no catálogo: muitas entram, uma sai. Só que metade dos router do acervo é o espelho disso. O dispersor da ULA recebe uma palavra de 32 e entrega bit a bit: uma entrada, sessenta e quatro saídas. Nele a forma afirmava o contrário do que o modelo diz — e forma afirma antes de qualquer rótulo ser lido.

Agora são três, e saem do modelo:

  • fecha — várias entram, uma sai: o mux, o coletor, a porta lógica;
  • abre — uma entra, várias saem: o dispersor;
  • reto — não há leque, e o trapézio afirmaria um que não existe. A logic unit da ULA era um trapézio e virou caixa: naquele nível ela tem uma entrada e uma saída.

O leque é contado nas ligações, e não nos nomes de porta. O coletor recebe trinta e dois fios numa entrada anônima só: pelos nomes ele não teria leque nenhum; pelo que o leitor vê, trinta e duas linhas chegam e uma sai. Foi o primeiro teste a falhar, e ele estava certo.

E a porta do bico acompanha: quem fecha entrega no bico da saída (era assim), quem abre recebe no bico da entrada — espalhar a entrada pela borda alta desmentiria a forma que a caixa acabou de afirmar.

O que a forma não diz, e é deliberado: se a caixa escolhe ou combina. As duas convergem, e a porta lógica é a prova — um XOR fecha igual a um mux. Quem separa é a linha de controle: quem escolhe é comandado, e desde a Entrega 8 ela é desenhada noutro plano, por cima. O texto do catálogo foi reescrito para dizer isso, e a guarda de fronteira pegou a primeira versão dele citando vocabulário de domínio — corretamente.

cpu-zoom.spec.ts cobra os dois sentidos, e cobra geometria: no dispersor, o lado direito do trapézio tem de ser mais alto que o esquerdo. Um atributo data-leque não bastaria — foi o CSS que não pintava, uma vez, com o atributo certo.

Estado: 990 testes unitários, 247 e2e, typecheck, boundaries, catálogo e build verdes.

Entrega 11 — A identidade de cada handbook, declarada ✅

08/09/2026. Pedido dele, junto com a virada para o OTel: “prepare para cada handbook ter uma estilização personalizável; parte do projeto é estilizar por handbook”.

Havia tema por domínio desde o começo — data-domain no <html> e um arquivo CSS por handbook — e ele tinha dois buracos:

  1. era uma segunda lista. O handbook morava no catálogo; a identidade dele, num arquivo CSS que ninguém obrigava a existir. Um handbook novo nascia com a cor da casa, parecendo outro handbook, e a descoberta era abrindo a página;
  2. ele parava na porta do palco. Pintava acento e os quatro sinais da página; o desenho, que é noventa por cento da tela, saía igual nos três.

Agora o tema é campo do handbook (tema: { claro, escuro }) e o CSS sai dele. Um handbook novo nasce com identidade só de ser declarado.

O conjunto do que ele pinta é fechado, e é isso que separa “estilizável” de “tema livre”: o acento, os quatro sinais, e do palco só o que é identidade — a família que processa, a que transporta, e as quatro espécies de carga.

O que fica de fora, e cada um por uma razão:

  • fundo e tipografia são da casa. Identidade que mexe no papel vira banner, que é o anti-objetivo escrito na spec do handbook;
  • a tinta viva (controle, alimentação, a família controladora) é convenção, não identidade: num diagrama de blocos a seta vermelha é controle em qualquer assunto, e o livro-texto da CPU manda nisso tanto quanto o do OTel;
  • o nível alto é o valor que saiu da peça, pelo mesmo motivo.

Três testes seguram isso (lib/tema.test.ts): o CSS gerado só escreve token da lista fechada; nenhum tema encosta em papel, tinta ou tipografia; e dois handbooks não têm o mesmo acento — numa aba só, o acento é tudo o que o leitor vê.

E o contraste passou a ser cobrado nos três handbooks, nos dois papéis: contrast.test.ts lia o CSS do OTel e só dele. Identidade não pode custar legibilidade, e a dos outros dois não era verificada por ninguém. São 67 pares agora, e um handbook novo entra na tabela sozinho.

Na tela: o handbook da CPU ficou verde-silício (as caixas eram azuis), o do OTel segue azul, e a linha de controle continua vermelha nos dois — que é exatamente o desenho da regra.

Estado: 1042 testes unitários, 249 e2e, typecheck, boundaries, catálogo e build verdes.

Entrega 12 — three-pillars, o primeiro lab da apostila v1 ✅

08/09/2026. Desenho: docs/superpowers/specs/2026-09-08-apostila-v1-espinha-otel-design.md §4.1. É o lab da fase 1, e com ele a espinha tem dois dos cinco nós.

A tese, e ela vira run: um sinal não é um tipo de dado; é a decisão sobre o que jogar fora na escrita — e o que se joga fora ali não se pede depois.

O desenho carrega o argumento

Uma fonte, um serviço e três caixas do mesmo tamanho na mesma coluna. O mesmo tamanho é afirmação, não estética: ninguém é o principal, e os três veem o mesmo evento. Se cada um tivesse a própria fonte, a demonstração seria um truque de montagem — e há teste cobrando os três tamanhos iguais e a mesma coluna.

O serviço tem uma entrada e três saídas, então o trapézio dele abre — a regra do leque, entregue na rodada passada, paga na primeira vista nova sem ninguém escrever uma linha.

Cada caixa mostra o que guarda, linha a linha: o tracer, uma requisição por linha com a duração; o medidor, baldes com contagem (e a ausência dos itens ao lado da contagem é o descarte desenhado); o registrador, as frases — e said nothing: 34, que é o tráfego sobre o qual o código não disse nada.

A pergunta é a peça, e as respostas saem do estado

Quais requisições passaram de 250 ms? As três respostas são derivadas: o tracer responde porque tem as linhas; o medidor não responde porque só tem contagens; o registrador responde se o código escolheu falar. Nenhuma é texto dizendo ao leitor o que concluir — data-responde é fato do modelo, e o e2e cobra que o medidor nunca responda. Se um dia o modelo mudar e a métrica passar a guardar identidade, o teste cai, que é o que se quer.

E há um degrau a mais que o modelo entrega de graça: perguntar por 300 ms faz o medidor perder até a contagem — 300 cai dentro de um balde, e balde é contagem, não lista. A diferença entre “não sei quem” e “não sei nem quantos” é o histograma inteiro.

Dois defeitos de desenho que este lab achou

  1. o rótulo da caixa saía por cima do conteúdo. Com linhas dentro, o meio da caixa é onde elas estão — e as duas coisas ficavam ilegíveis ao mesmo tempo. O rótulo foi para o rodapé, que é o único lugar que o conteúdo não ocupa;
  2. os baldes saíam fora de ordem, na ordem em que a primeira medição de cada um chegou. Um histograma fora de ordem vira tabela de números soltos, e a forma da distribuição — a coisa que ele existe para mostrar — desaparece. A ordenação é do modelo, com teste.

A contraparte real roda

labs/three-pillars/: um programa Java que registra o mesmo evento nos três, com a mesma função de latência do lab da tela — os dois lados precisam contar a mesma história, senão a contraparte não é contraparte de nada. Rodado: 180 spans, histograma cumulativo com ExplicitBounds, e registros só das falhas (com PILLARS_LOG_EVERYTHING desligado). As duas variáveis do compose são as duas decisões que o lab oferece.

O README diz o que não dá para ver ali: o instante em que a identidade é descartada, e quantas requisições o log não mencionou — a ausência de uma linha não é impressa.

Estado: 1062 testes unitários, 259 e2e, typecheck, boundaries, catálogo e build (37 páginas) verdes. A espinha da v1: three-pillars ✅ · anatomy-of-a-trace · providers ✅ · manual-spans · head-vs-tail-sampling.

Entrega 13 — anatomy-of-a-trace, a fase 2 da apostila ✅

08/09/2026. Desenho: docs/superpowers/specs/2026-09-08-apostila-v1-espinha-otel-design.md §4.2. Três dos cinco nós da espinha estão de pé.

A tese: a árvore não existe durante o run. Cada span carrega uma aresta só — a que aponta para o pai — e cada processo exporta por conta própria; a árvore é o fecho transitivo dessas arestas, calculado depois por quem as recolheu.

O desenho é a tese, antes do texto

Quatro serviços numa fileira: a chamada anda para frente, de um para o outro, levando o traceparent; o span de cada um sai para baixo, sozinho, até o backend. Quatro fios descendo em paralelo dizem “ninguém manda árvore para lugar nenhum” sem uma palavra.

E os três primeiros serviços são trapézios que abrem — uma entrada, duas saídas: o trabalho continua por um lado, a telemetria sai pelo outro. O ledger, que é o fim da cadeia, é um retângulo. A regra do leque, de novo, dizendo a verdade sozinha.

O que o lab cobra são as conclusões ERRADAS

É incomum e é o ponto: os testes exigem que derrubar um cabeçalho não dê erro. Tem de dar duas árvores completas e plausíveis para uma requisição, cada uma com sua raiz, nenhuma se sabendo metade de alguma coisa. E um serviço sem instrumentação não pode faltar na árvore: ele repassa o cabeçalho, o filho se pendura no avô, e a árvore fecha com um salto a menos parecendo completa. Se um dia isso deixar de acontecer, o lab parou de ensinar o que acontece de verdade.

O achado da rodada: não existe evento de fim de trace

O painel precisava escolher qual requisição desenhar, e a mais recente está sempre com um span no fio — desenhá-la mostraria uma árvore pela metade e acusaria um defeito que não existe. Tentei duas regras erradas antes de ver que a resposta certa já estava no assunto: o backend espera e desiste.

Não existe evento de “trace terminou”, e não poderia existir — emiti-lo exigiria alguém que conhecesse a árvore inteira, que é justamente o que não há. Então o backend carimba a chegada de cada span e dá o trace por encerrado depois de dois ticks de silêncio. É a mesma coisa que um backend de verdade faz, e é a razão de amostragem de cauda precisar de janela e de teto de memória. O que era um detalhe de painel virou uma afirmação do lab, com teste.

A contraparte real: quatro processos de verdade

labs/anatomy-of-a-trace/: o mesmo programa rodando quatro vezes com nomes diferentes — numa cadeia real ninguém é especial —, falando HTTP entre si, com o propagador W3C de verdade. Rodado: 480 spans, 61 de cada serviço, e um Trace ID aparecendo em quatro ResourceSpans separados, um por processo. O Collector é o primeiro lugar do sistema onde os quatro se encontram, que é exatamente o que a tese diz.

As duas variáveis do compose são os dois defeitos do lab: ANATOMY_STRIP_HEADER no checkout e ANATOMY_UNINSTRUMENTED no payments.

Estado: 1076 testes unitários, 271 e2e, typecheck, boundaries, catálogo e build (38 páginas) verdes. Os tetos de espaguete dos dois labs novos entraram medidos: 0 no three-pillars, 4 na anatomia — e os quatro são estruturais, porque quatro serviços exportando para um backend que fica embaixo de todos é um leque que converge.

Espinha: three-pillars ✅ · anatomy-of-a-trace ✅ · providers ✅ · manual-spans · head-vs-tail-sampling.

Entrega 14 — Seguir a carga, e o que cada parada muda ✅

08/09/2026. Pedido dele, olhando os dois labs novos: “acho que a abordagem do herói seria melhor — clicar dentro do item e ver do lado o conteúdo dele, e conseguir acompanhar no trajeto ele sendo enriquecido; essa é a visão da landing, muito mais adequada”.

Ele está apontando para a ideia nº 7 do DECISIONS.md — “seguir a carga” —, que existia só no herói da landing e nunca tinha entrado num lab. O que faltava não era desenho: era identidade. O motor dá um id novo a cada emissão, e está certo, porque cada salto é uma mensagem nova; quem sabe que duas mensagens são a mesma coisa é o domínio.

Agora clicar num item abre o painel dele: o corpo agora, o trajeto até aqui, e o que cada parada mudou — com o campo alterado marcado no lugar em que ele mora, pelo mesmo Inspector do herói, que estava escrito desde o primeiro dia e nunca tinha sido usado num lab.

Na anatomia, o trajeto É o mecanismo

edge → gateway        first sighting
gateway → checkout    traceparent
checkout → payments   traceparent
payments → ledger     traceparent

O cabeçalho reescrito a cada serviço, acontecendo. E quando um proxy o derruba, a mudança aparece como o campo virando — no header on the wire —: a ausência é dita, e não omitida — um campo que some da tela sem explicação é o defeito que o lab existe para mostrar.

Nos pilares, o trajeto é a tese vista de dentro de um item

requests → service       first sighting
service → trace-store    kept_by
service → metric-store   kept_by, request, duration_ms
service → log-store      kept_by, request, route, duration_ms

O tracer não perde nada; o medidor perde a identidade e o número exato; o log perde quase tudo. Os três corpos têm a mesma forma de propósito, com o valor trocado por “descartado” — formas diferentes fariam o diff acusar a forma, e não a perda, que é o assunto.

Três decisões que o round obrigou

  1. Produto não é parada. Cada serviço exporta um span, e span não continua o caminho da requisição. Misturado, cada linha do trajeto acusava “mudou o traceparent E o span”, porque o corpo alternava entre duas formas — ruído no lugar do mecanismo. O leitor do domínio passou a dizer o que é parada;
  2. O diff é contra a CHEGADA no nó, e não contra a parada anterior. Num caminho reto as duas coisas são a mesma; num leque não são: comparar o segundo braço com o primeiro respondia “o que o log tem de diferente da métrica”, pergunta que ninguém fez. Contra a chegada, cada braço responde a certa — o que ele guardou da mesma coisa;
  3. O item precisa de área de clique. Ele tem sete unidades de raio, anda, e é recriado a cada tick: acertá-lo era pontaria, não gesto. Um alvo invisível do dobro do tamanho, com fill: transparent (e não none, que não recebe ponteiro).

E três testes que fingiam guardar

A suíte vinha falhando raro, sob carga, sempre em testes diferentes — e a causa era uma só, repetida: guarda que não guarda.

  • o teste do mapa esperava o contador dizer “0 de N” antes de clicar. Esse texto já está no HTML do servidor: esperar por ele não prova que a ilha hidratou, e o clique caía num botão que ainda não escutava ninguém;
  • a primeira versão do conserto esperava a hidratação sem rolar a página — e a ilha é client:visible, então ela esperava por uma coisa que ninguém tinha pedido para acontecer. Ficou vinte segundos pendurada e reprovou o teste que existia para salvar;
  • o teste do herói lia o inspetor uma vez depois de mexer na linha do tempo, e às vezes pegava o payload anterior — ou nenhum.

Os três viraram espera de verdade. A suíte inteira: 275 e2e, zero falhas, com a máquina carregada.

Estado: 1081 testes unitários, 275 e2e, typecheck, boundaries, catálogo e build verdes.

Entrega 15 — O foco no item, e o herói no motor novo ✅

08/09/2026. A rodada começou como “migrar a landing” e virou investimento no pilar, por duas correções dele. A primeira: o depth-core não é andaime, é pilar — um motor de grafos com profundidade que não existe assim no mercado. A segunda, e é a que mudou o desenho:

“Mesmo que se encaixe na nossa lógica de fluxos, o foco é o item e não a fábrica. Ou melhoramos o foco pro item, ou criamos um lab mais PhET.”

Um trace é um fluxo com camadas e mesmo assim o motor o servia mal, porque o protagonista dele é a fábrica. Viraram DECISIONS.md §9, com a formulação que decide sozinha: o tema do lab nomeia o protagonista, e não se monta uma rua para explicar uma porta.

A Trilha

O trajeto virou o desenho: uma estação por ponta, e o que mudou entre as estações. Duas decisões que a implementação obrigou:

  • as estações são uma a mais que as paradas — uma parada é um salto, e N saltos tocam N+1 lugares. Uma estação por parada faria o item nascer no meio do caminho;
  • o rótulo do que mudou é do salto, e é lido na estação de partida. Pendurado na chegada, ele diria “o destino tem o campo” — verdade que não é o assunto, porque o assunto é quem acrescentou.

seguir()/Parada subiram para o depth-core: identidade e trajeto de um item são conceito do motor, e viviam em apps/site como utilitário de página. E o painel dos labs passou a desenhar a Trilha — a lista de paradas tinha dois desenhos possíveis e agora tem um.

O herói

Mundo próprio (service → collector → backend), sem controle nenhum, abrindo já seguindo um span. Não é o lab de ninguém de propósito: providers mora dentro do processo, anatomy entre quatro processos, e o herói é o oleoduto visto de fora.

A seção dos quatro níveis mortos saiu da landing. No lugar, os três gestos que valem para uma CPU e para um trace — e a segunda metade do pilar, que não estava em lugar nenhum do site: quando o assunto não é fluxo com camadas, o lab é outro, feito para ele.

O que os testes novos acharam, e que não estava no plano

Os cinco testes do herói entraram vermelhos em três, e cada um era defeito de verdade. Vale registrar porque a rodada só valeu o preço por causa disto:

  1. O Inspector mostrava um corpo que não era o corpo. Ele redesenhava o rótulo cortando o caminho no último ponto, então toda chave com ponto aparecia truncada — e chave com ponto é a norma do OTel (service.name, http.method). A peça que o projeto usa para dizer “este é o corpo, campo por campo” mentia em todo lab, desde sempre, e ninguém tinha visto porque nenhum teste usava chave com ponto. O conserto passa a chave pela recursão em vez de rederivá-la; o path ficou byte a byte igual, porque é ele que o diff casa.

    E ao consertar apareceu o limite por baixo, agora lacuna declarada em diff.ts: o caminho é codificação com perda — {a:{b:{c}}} e {a:{"b.c"}} dão o mesmo "a.b.c". Fica não consertado de propósito, pelo mesmo trato que o arquivo já fazia: falso positivo em vez de falso negativo silencioso.

  2. O herói prometia descer e o mundo não tinha nada dentro. As três caixas eram leaf, o duplo clique não fazia nada, e o docblock afirmava o contrário. O collector ganhou interior — receiver → processor → exporter —, escolhido por ser a única das três cujo dentro nenhum lab mostra: providers mora dentro do processo, não do collector.

    Quem carimba o atributo é o processador, e a razão é do domínio: num Collector de verdade resource, attributes e resourcedetection são todos processors — receiver e exporter são transporte. Carimbar numa ponta seria transporte que transforma, e isso esconde onde o dado mudou. As duas pontas são conduíte, o miolo é processador, e a forma das caixas diz onde o campo nasce antes de qualquer rótulo ser lido. O teste mede os dois fios de dentro, e não um: ausente antes do processador, presente depois. Medir só a saída deixaria passar um receiver que carimbasse.

    Achado de desenho que vale para o motor inteiro: fio declarado to: "collector" roteia certo e some da vista interior, porque as duas pontas caem fora do quadro. Ligado às folhas, o mesmo fio desenha nas duas vistas.

  3. Duas sobras da escada morta: um comentário no lab dos provedores, e um teste que ainda cobrava que a landing mostrasse quatro níveis. Esse voltou sem contagem escrita — número no teste é segunda fonte do mesmo fato; ele cobra agora o gesto que a rodada pôs lá, e cai se “Follow one item” sumir da landing.

Mais dois defeitos que só apareceram porque alguém olhou: o herói sem movimento mostrava a trilha vazia dizendo “The first span is on its way” para um leitor que nunca veria o span chegar — agora o mundo adianta de uma vez e para, com trajeto de verdade para ler; e o número de ticks desse quadro parado ficou falso no instante em que o collector ganhou interior, e subiu junto.

E uma armadilha de suíte: o Vitest daqui não registra o cleanup do testing-library, então arquivo de teste que renderiza mais de uma vez lê o DOM do teste anterior. Cinco testes da Trilha falhavam por isso, e nenhum por defeito no componente. Está contornado nos dois arquivos que precisam; um setup compartilhado resolveria de vez e não foi feito.

Estado: 1093 unitários (93 arquivos), 283 e2e (21 puladas, zero falhas), typecheck, boundaries, catálogo e build verdes — os seis rodados na árvore final, depois de tudo pousar.

Entrega 16 — O mermaid pousa, e o medidor que ninguém lia ✅

13/10/2026. A rodada era fechar o merge do esqueleto Mermaid (PR #16) e virou duas coisas: o merge, e o buraco que o merge deixou à vista.

O que o CI já dizia, e ninguém tinha lido

A PR #16 estava vermelha desde o terceiro commit, e morria em 27 segundos — em pnpm boundaries, antes de chegar ao typecheck, ao test, ao build e ao e2e. A causa era de uma palavra: o commit que pôs mermaid-skeleton na lista dos agnósticos escreveu, no mesmo pacote, uma fixture chamada otel-anatomy. A guarda de fronteira passou a ler o pacote e achou “otel” dentro do motor.

A guarda estava certa, e o conserto foi na fixture: ela é teste do motor, e a topologia dela não é de domínio nenhum. Vale anotar porque é a tentação óbvia — a guarda acusa, e o caminho curto é afrouxar a guarda.

E o novo pacote não estava nas referências do tsconfig da raiz: tsc -b construía sete projetos e nenhum deles era o do parser e do compilador. Mil linhas fora do pnpm typecheck, verdes porque ninguém olhava.

O medidor que existia e ninguém lia

A pergunta que abriu isto era de confiança no formato: o .mmd declara a porta de cada fio num rótulo escrito à mão (call:request, export:span). O que acontece se alguém errar uma letra?

Medido antes de ser respondido. Com cal:request no lugar de call:request:

  • o parser aceita;
  • o compilador aceita — um nó com override não tem porta validada, porque o compilador não pode saber por quais portas uma função emite;
  • validateWorld aceita, e o mundo é válido;
  • o run roda, e o livro-caixa marca out:gateway.call.unwired: 11.

O motor já media. Onze emissões no vácuo, contadas, com nome e porta — e nenhum leitor. O que caía era um teste de comportamento três camadas abaixo, cobrando quatro spans e recebendo um; e a primeira suspeita de quem lê isso é do comportamento, que está certo.

O que se perde sem isso não é o desenho: é o sentido. A anatomia com esse erro de uma letra continuava rodando, continuava válida, e passava a ensinar que um trace tem um span.

Então o conserto não foi medir — foi ler o medidor: emissoesPerdidas(state) em depth-core/meters.ts, ao lado de portCount, e uma guarda por domínio que varre os mundos e cobra lista vazia. Com o typo, ela falha dizendo gateway.call: 19 emissão(ões) sem fio — nó, porta e contagem, na altura em que o erro foi cometido.

Os nove mundos do repositório passaram de primeira, e isso é o que torna a guarda instalável sem exceção nenhuma: ela não documenta dívida, ela trava o próximo erro.

Três distinções que o livro-caixa não faz sozinho

A guarda só vale se não gritar com o que é legítimo, e são três coisas parecidas que não são a mesma:

  • descarte declarado (to: DROP) não é perda — o autor pediu que caísse, e o buffer emitindo por drop é o projeto cumprindo a regra de que descarte invisível é mentira;
  • porta de controle que entrega sinal não é perda: o escalonador só marca unwired quando não houve destino de sinal tampouco;
  • fio nenhum é perda, e é a única das três.

A assimetria da chave, que inverteu a implementação

A chave é out:<nó>.<porta>.unwired, e partir isso parecia detalhe. Não é:

  • indexTree recusa id com . ou : na construção — então o nó é garantidamente de um pedaço só. (Um teste cobra isso de dentro de perdas.test.ts, e não só de tree.test.ts: se um dia a regra afrouxar, a partida da chave tem de ser repensada junto.)
  • validateWorld só valida porta que aparece num fio. A porta deste balde é, por definição, a de uma emissão sem fio — a única do livro-caixa que nunca passou por validação nenhuma, e pode trazer um ponto dentro.

Escrito primeiro com lastIndexOf, pela analogia com o Inspector da entrega 15. Está errado: com porta a.b daria nó gen.a e porta b, as duas inventadas. É indexOf, e a razão é a assimetria — o lado garantido é o do nó, não o da porta.

Duas sobras que o typecheck pegou e o vitest não

vitest passou verde com comTransistores: true num mundo cuja opção se chama transistoresNaUla: propriedade desconhecida é ignorada em silêncio, e a variante “CPU aberta até o transistor” testava a CPU comum duas vezes. Rodava em 3ms e parecia ótimo; agora roda em 1370ms, que é o preço de abrir trinta e dois bits até o silício.

É o argumento do typecheck em teste, e não só em código de produção: um teste que não typecheca mente com cara de verde.

A outra metade: o fio que não carrega nada

A guarda de cima cobre uma direção — o comportamento fala e ninguém ouve. A inversa é um fio que sai de porta onde nada é emitido: o desenho mostra uma ligação que não existe. No typo de uma letra as duas acontecem juntas, e a primeira bastava. Separadas, não, e foi medido: acrescentando ao mmd uma aresta gateway -- "metrics:foo" --> backend, sem comportamento nenhum emitindo por metrics, sem-perda.test.ts passa 8/8 e só a guarda nova acusa. São complementares, e é por isso que são duas.

Para um material visual essa é a pior das duas: o leitor não tem como saber que aquela linha nunca carregou nada, e o desenho é o argumento.

A guarda é local à anatomia de propósito, e isso foi medido antes de decidido. portCount lê out:<nó>.<porta>, e a chave de um fio que sai de COMPOSTO pertence às folhas de dentro dele. Na CPU, 67 dos 68 fios que essa conta chamaria de mortos saem de composto e carregam carga normalmente — uma guarda geral assim seria fábrica de falso positivo, que é como se ensina gente a ignorar guarda. Quem resolve a cadeia de bornes é emissoesPorPorta, por tick, e isso já estava resolvido uma vez para a camada de vista. A anatomia escapa porque o único composto dela é a raiz sistema e nenhum fio sai dela; a premissa é cobrada em teste, e cai primeiro no dia em que ela ganhar um subgraph.

No providers, 6 dos 30 fios ficam mudos no cenário padrão e todos acordam no cenário que o lab existe para ensinar — descarte, fila cheia, record-only, collector caído. Nenhum fio do lab é decoração.

E um susto que era erro de medida meu, anotado porque é o tipo de coisa que viraria “achado”: o caminho de métricas parecia morto em todos os cenários. Não estava — export-interval padrão são 60 ticks, e a sonda rodava exatamente 60.

A terceira perna: o rótulo que mente

O parser lê call:request como porta call e kind de mensagem request, e guarda os dois no SkeletonEdge. O compilador usa a porta e descarta o kind — WorldSpec.wires não tem campo para ele, e quem decide o kind do que viaja é ctx.emit(kind, …), no comportamento.

Então o mmd pode dizer call:request enquanto o comportamento emite ctx.emit("pedido"), e nada acusa. Não perde emissão: o fio existe e carrega. Não há fio morto: ele tem tráfego. O que se perde é só o que o rótulo promete — e a §3 do design é explícita em dizer que esse rótulo é o kind da mensagem no vocabulário do domínio.

Num material em que o desenho é o argumento, essa é a mentira mais barata de cometer e a mais difícil de ver: o lab roda, ensina a topologia certa, e nomeia errado o que passa por ela.

Medido como as outras duas. Com call:pedido no mmd e o comportamento emitindo request: sem-perda.test.ts passa 8/8, a guarda de fio morto passa, e só a nova acusa — gateway.call: rótulo diz "pedido", viajou ["request"].

Fecha o conjunto, e as três são distintas:

O que aconteceQuem acusa
emissão sai por porta sem fioemissoesPerdidas (as três guardas de domínio)
fio sai de porta onde nada é emitidofios.test.ts — “carrega alguma coisa”
rótulo nomeia o kind erradofios.test.ts — “carrega o que o rótulo diz”

Nos três casos o mundo é válido, o run roda e nenhum erro aparece. É o que torna o conjunto necessário: o formato não falha alto, ele ensina outra coisa.

A tabela que ensinava a opção rejeitada

A §4.2 da spec do design dizia que linha pontilhada é canal de controle. A Nota 1 do exemplo põe as duas saídas lado a lado e recomenda a outra — traço é ênfase visual, data/control vem do prefixo do rótulo —, que é o que o parser implementa e o que parse.test.ts já fixava em teste. A §4.3 da própria spec já usava control:tick: a tabela contradizia a seção seguinte, e o documento que alguém lê primeiro ensinava a regra morta.

Quem depende disso é a anatomia. O span é dado — chega no inbox, não em ctx.signals — e ainda assim sai por linha pontilhada, para dizer “sai pelo lado, não pela cadeia”. Pela regra antiga ela teria de escolher entre o desenho certo e o fio certo.

A placa entra no catálogo nativo, e não precisou de sintaxe nova

O buraco óbvio do compilador é o catálogo: nativamente ele montava source/buffer/sink, e router/switch/store/sequencer/static só existiam por overrides — é assim que a anatomia tem router e store. A tentação é atacar router primeiro, porque é o que os labs usam. Está errado: nele o que falta é política de rota, e declarar política é decisão de linguagem que a §6 manda aprovar antes de implementar.

static é de outro tipo, e foi o que sobrou olhando o que o motor de fato exige. O ObjectSpec de uma placa é { id, kind, label, leaf } e nada mais: sem init, sem behavior, porque indexTree recusa placa que aja — placa que age deixou de ser placa. O .mmd já traz id (o nó), kind (o classDef) e label. Não faltava nada.

A recusa dizia “o formato ainda não declara o conteúdo dela”, e é verdade para conteúdo rico — um recurso com atributos, uma tabela de limites. Só que esse conteúdo nunca chegou ao motor nem pelo caminho do TS: o placa() do lab dos provedores recebe um PlacaDeRecurso, usa dados.titulo como label, e para aí. Para a estrutura, o rótulo é o conteúdo — então a recusa guardava uma porta que não tinha nada atrás.

Com isso o critério de morte do .modelet.yaml (§7 da spec: “quando o Mermaid cobrir source, buffer, sink”) foi atingido e passado — o compilador de Mermaid monta um kind que o de modelet recusa.

Duas recusas que ficaram, e a segunda é a interessante. Fio que CHEGA numa placa já era recusado por validateWorld; agora é recusado na compilação, mais cedo e com a frase que explica o que placa é, em vez de “não recebe por nenhuma porta”. E fio que SAI de uma placa é provadamente morto: placa nunca tem behavior, logo nunca emite, logo aquele fio não carregaria nada em nenhum tick. É o único caso de fio morto que se recusa sem rodar o mundo — os outros dependem do run, e é por isso que as guardas da rodada são testes e não checagem estática.

O erro de quem liga um fio numa placa não é ter errado a porta: é ter lido a placa como peça de fluxo. A mensagem responde isso, e não o sintoma.

As guardas saem da anatomia e viram peça do formato

As três guardas nasceram amarradas à anatomia, e isso tinha prazo: quando o lab nº2 em .mmd aparecer, alguém copia e cola — e copia a ressalva de composto sem ler. Duas delas (fio sem emissão, rótulo que mente) viraram conferir.ts no mermaid-skeleton, com a API em três peças: observarCanais(estados) acumula o que viajou, conferirDesenho(sk, spec, observado) devolve só o que divergiu, relatoDeDivergencias escreve a mensagem. A terceira (emissão sem fio) fica no depth-core, porque não é do formato: vale para mundo escrito à mão também.

A ressalva de composto virou pré-condição que levanta, em vez de comentário. Um chamador que ignora um aviso recebe uma lista de mentiras, e a próxima pessoa a ler o teste acredita nela.

E aí o parser respondeu algo melhor do que a pré-condição: ele não deixa um .mmd ligar fio num subgraph, e recusa nas duas ordens — "bloco" já é subgraph quando o subgraph vem antes, colide com outro id já usado quando vem depois. Valia medir a segunda, porque a checagem olha ctx.subgraphs, que é preenchido na leitura, e a ordem inversa poderia escapar. Não escapa. Então para o caminho do mmd a pré-condição é rede de segunda camada: um composto só nasce de subgraph, e nenhum fio toca um. Ela segue valendo para quem apontar a conferência para um WorldSpec escrito em TS — que é como alguém a usaria num lab de CPU, e onde a conta ingênua erraria 67 de 68 vezes. O teste da ordem inversa mora junto da conferência, e não só em parse.test.ts, porque é a premissa de que ela depende.

A anatomia virou a primeira consumidora, e o diagnóstico melhorou de graça: com cal:request no lugar de call:request, o relato agora mostra o raio de propagação — gateway.cal mudo mais checkout.call, payments.call e checkout.export, que emudecem porque a cadeia quebrou atrás. Antes a guarda local acusava um canal só.

E os cenários de defeito ganharam a distinção que faltava: canal mudo ali é esperado — desligar a amostragem silencia os export de propósito, e cobrar lista vazia seria cobrar que o lab não ensinasse. O que vale nos cenários é a outra metade, que não depende de parâmetro: o que carrega tem de carregar o kind prometido. Com um teste a mais provando que o cenário muda algo, senão o filtro passaria mesmo com parâmetro inerte.

O CI vermelho que não era meu, e não era para ignorar

65586c9 reprovou em [mobile] gates-lab: a porta acesa é desenhada acesa, e o commit seguinte passou no mesmo teste. Nada no diff toca portas lógicas nem o CSS do palco. A saída fácil era chamar de flake. Medido, com uma sonda que lê o mesmo estilo seis vezes:

altabaixadeltalimiar
em regime0.0680.0090.05940.02 ✅
primeira leitura0.0560.0090.04720.02 ✅
o que o CI pegou0.0220.0090.01310.02 ❌

Em regime a margem é três vezes o limiar. O teste compara a croma em oklab durante a transição do fill, que stage.css declara em 160ms; num runner mais lento o quadro pego é mais cedo. O limiar não é o problema — afrouxá-lo esconderia justamente o defeito que o teste existe para pegar, que é a porta acesa cair na tinta da apagada.

tintaEstavel espera o fill parar de mudar, com orçamento de 1,6s (dez vezes a transição), e levanta se estourar: tinta que não assenta é achado, não ruído, e devolver a última medida faria a asserção reprovar com a mensagem errada. Aplicado também no laço de temas do mesmo teste, que tem a corrida idêntica pela razão idêntica — ele mede logo depois de trocar o tema.

E a guarda que impede a guarda de envelhecer — que achou um furo na primeira volta

O defeito que esta rodada inteira perseguiu é guarda que passa a não medir nada sem avisar. As varreduras sem-perda eram exatamente isso: lista de mundos escrita à mão. O lab novo entra, ninguém lembra do arquivo, e o verde continua verde cobrindo os antigos.

A convenção que resolve já valia antes de ser usada: todo construtor de mundo deste repositório termina em World. Então a guarda é sobre a superfície do pacote — lê Object.keys do índice, filtra por /World$/, e compara com a lista varrida. Um construtor novo faz o teste cair nomeando-o e dizendo onde acrescentá-lo.

Não é hipótese: na primeira vez que ela rodou, achou portaCmosWorld de fora. Exportado do cpu-domain, varredura escrita à mão na rodada anterior, ficou para trás. Os doze variantes dele (seis portas × com e sem atalho) estavam limpos — então o que se perdeu não foi um defeito, foi a garantia. Entraram na varredura, e a CPU passou de 7 para 20 testes.

O limite fica declarado no próprio teste: construtor que não é exportado do índice é invisível para ela. É guarda sobre superfície pública, não sobre chamadas.

Houve uma escolha de desenho a registrar: a lista de nomes (CONSTRUTORES_VARRIDOS) é segunda fonte da mesma verdade, e o projeto normalmente recusa isso. Ela fica porque existe só para a comparação nome-a-nome, e o preço está dito no docblock — um nome listado ali sem estar em LABS não é pego por nada. A alternativa seria a guarda chamar cada construtor, e os construtores não têm assinatura uniforme: cpuWorld quer uma imagem montada, rpnWorld quer tokens.

O guia de autoria ensinava um caminho que não existe mais

A última coisa olhada foi a doc que um autor de lab lê primeiro, e ela estava três mudanças atrás:

  • mandava escrever em apps/site/src/labs/<slug>/scenario.ts, e esse diretório não existe;
  • descrevia a API Scenario<S> com initialState/step. Ela ainda é exportada do depth-core, e nenhum lab a usa: as únicas menções no repositório são o próprio motor e o teste dele. Os sete componentes de lab vão todos por new World(spec);
  • não mencionava Mermaid nem .mmd uma única vez — nem ela, nem model-format.md, nem kinds.md. O formato que a spec declara canônico desde 16/09 era invisível nos três documentos que um autor leria.

Reescrito o que um lab é hoje, com as duas formas lado a lado (esqueleto Mermaid para labs novos; WorldSpec à mão para os oito que antecedem o formato, e sem obrigação de reescrever), mais a tabela das três guardas, que é o que um lab novo tem de satisfazer.

Engine/Scenario<S> ficam no código, com uma nota dizendo que nenhum lab as usa e que se elas deviam continuar públicas é pergunta aberta. Remover superfície pública é decisão, não limpeza.

Uma afirmação minha foi corrigida pela leitura do motor. Eu havia escrito que “um comportamento que ignora a fase roda duas vezes”. Está pela metade: ele é chamado nas duas, sim, mas o escalonador recusa em voz alta emissão em porta cujo regime não casa com a fase, nomeando a porta — e o state devolvido na acomodação nunca é lido, que é o que separa lógica combinacional de elemento de memória. A mecânica real é mais útil que o aviso vago, e foi essa que ficou escrita.

E o fio solto que o conserto do guia deixou: a §0 do model-format.md é literalmente a tabela “o que já é código, e o que ainda é proposta”, e a decisão de 16/09 não estava nela. Ganhou uma §0.1 dizendo que o cenário saiu dali, que o modelet segue valendo para o agregado .model, e que nada implementado foi removido — o que não acontece mais é cenário novo nascer em YAML.

Com a divergência registrada com precisão, porque ela é nova: o compilador de Mermaid monta static e o de modelet não. A linha da tabela que recusa static descreve o compileModelet, e segue correta. A tabela de contratos de cada compilador é local de propósito, e o compile.ts do mermaid-skeleton dizia isso no docblock desde que nasceu — a divergência era prevista, e agora é fato.

O que não divergiu entrou junto, porque é a pergunta que alguém faria em seguida: router, switch, store e sequencer seguem recusados nos dois lados, e a razão é a mesma — falta política, não sintaxe — com o ponteiro para a spec que mediu isso.

kinds.md ficou sem ponteiro de propósito: é o catálogo dos primitivos do motor, agnóstico de formato de autoria. Acrescentar Mermaid ali seria confundir as duas coisas.

Estado: 1183 unitários (101 arquivos) — 68 novos —, typecheck, boundaries, catálogo e build verdes. O e2e local falha num teste [mobile] por artefato de container (o Playwright 1.62.1 procura chromium build 1234 e a imagem traz 1194, então o full chromium entra no lugar do headless shell e o site-nav fixo intercepta o clique); reproduz idêntico em main, onde o CI real está verde.

Entrega 17 — Dois labs, duas formas, e um diagnóstico meu que estava errado ✅

16/10/2026. O pedido era amplo — gráficos, conteúdo, exercícios, mais labs — com uma restrição que decide tudo antes de qualquer código: a ADR §9. Ela tem duas perguntas em ordem, e a rodada aplicou as duas em vez de escolher a forma por gosto.

O conteúdo que faltava: duas de três contrapartes sem exercício

O repositório tinha três contrapartes reais rodando em compose e só uma com exercício. A anatomia tinha Service.java desde a entrega 13 e nenhuma pergunta em cima dele.

Os dois da anatomia são sobre a MESMA peça vista das duas pontas — qual contexto o serviço passa adiante —, e isso é deliberado: o defeito que o lab ensina não é erro de API, é a árvore fechar parecendo completa com um salto a menos, e esse resultado sai de qualquer das duas pontas errada. Cada exercício tem um distrator que produz um cenário que o painel do lab já mostra: Context atual = recebido é o sem-instrumentacao, inject(Context.root(), …) é o derrubar-cabecalho-em.

Os dois dos pilares erram para o lado que parece mais cuidadoso — mais atributos, nome mais claro —, que é como eles acontecem: trace_id como atributo de métrica (a intenção está certa, e é para isso que exemplar existe), e setUnit("milliseconds") em vez do código UCUM.

O Java ganhou só linhas de comentário, e mvn compile foi rodado antes e depois nos dois labs.

Três consertos que o segundo conjunto obrigou: versaoDoBom() lia o pom.xml do providers fixo (a tela mostraria a versão de outro projeto, e os três usam 1.65.0 hoje — o defeito nasceria invisível); o teste varria uma lista escrita à mão, então as regras valiam para um conjunto e não para o outro; e o e2e cobrava comportamento do componente, que é o mesmo nas duas páginas, e seguiria verde se alguém apagasse a seção de uma delas.

A cobertura passou a ser derivada do domínio — e se estendeu sozinha quando o terceiro conjunto entrou: 5 testes viraram 6 sem eu tocar nela.

O lab do motor: collector-pipeline, o primeiro a nascer em .mmd

Régua: a pipeline do Collector é fluxo com camadas, e o protagonista é a fábrica — a pipeline é o assunto, não o span que passa. Logo motor, e em Mermaid.

A tese é a mais barata de errar do Collector: um receiver declarado em duas pipelines é um receiver só, e entrega uma cópia a cada. Alguém põe um filter para cortar /health, confere no backend certo, vê que parou — e o segundo exporter seguiu mandando tudo.

E ela é propriedade do desenho: duas arestas saem da mesma porta, o leque é nativo do motor, e o comportamento do receiver emite uma vez sem mencionar pipeline nenhuma — como no Collector, onde ele também não sabe quantas existem. Há teste cobrando isso no esqueleto, sem rodar nada. Medido: ligar o filtro deixa o backend em 44 spans, exatamente onde estava, e corta o stdout de 48 para 24.

Dois limites do formato achados ao construir, na §7.1 da spec do design:

  • o .mmd não expressa DROP. O id de nó do parser é [A-Za-z_][A-Za-z0-9_-]*, e @drop não casa; não há suporte a DROP em lugar nenhum do mermaid-skeleton. Contornado com um nó contador explícito, e o contorno ficou melhor que o original: o Collector expõe otelcol_processor_refused_spans — a perda dele é contador, não vazio;
  • o .mmd declara topologia, então parâmetro não reordena processador. A tese mais óbvia do Collector (processors: é lista ordenada) não cabe num lab de um mmd, e ficou na tabela de mal-entendidos sem ser simulada. A saída seria dois .mmd e um parâmetro escolhendo qual compila — novo, e não aprovado.

Contraparte real com o collector.yaml que é o desenho. A lacuna está declarada no README: sem daemon de Docker no container, otelcol-contrib validate não rodou, e a sintaxe evita de propósito as partes que mudaram entre versões.

O lab PhET-like: cardinality-and-the-bill

Régua, primeira pergunta: cardinalidade não é fluxo — é o tamanho de um produto cartesiano. Logo lab separado, sem palco, sem kinds, sem importar o motor.

A decisão de desenho que carrega a tese: o produto aparece como produto. 12 × 6 × 4 × 8 000 = 2 304 000 numa linha é o argumento inteiro; só o total seria um número grande sem mecanismo.

E o lab não afirma preço, com trava nos dois níveis: um teste de unidade cobra que a Conta não tenha campo de preço, e um e2e cobra que a saída não mostre moeda até o leitor digitar a dele. A ADR nomeia isso como o defeito mais caro do projeto, e preço de fornecedor é exatamente o caso — cada um cobra por unidade diferente. O modelo calcula volume, que é mecânico.

O limite de cardinalidade usa a mesma leitura do lab dos três pilares. Dois labs discordando da mesma spec seria pior que um lab faltando. E a contraparte real não é nova: three-pillars já roda um SDK de verdade, e PILLARS_USER_ATTRIBUTE é a jogada que este lab cobra.

Consequência estrutural da ADR na suíte: lab sem palco não tem desenho para medir, então o teto de cruzamentos não se aplica. SEM_PALCO não é isenção — há teste cobrando que a página desses labs de fato não tenha .dui-stage. No dia em que um ganhar palco, ele cai pedindo um teto.

O gráfico, e um diagnóstico meu que estava errado

A seção de exercícios quebrou seguir-a-carga no mobile. Medido em vez de contornado: a página foi de 412px de largura para 864 num viewport de 412.

A causa levou três medições: o cartão do exercício é item de grid, item de grid tem min-width: auto — que é min-content —, e com white-space: pre min-content é a linha mais longa do código. O cartão crescia até caber a linha e arrastava a página; o overflow-x: auto do <pre> nunca entrava em ação porque o contêiner dele é que crescia.

A primeira hipótese estava no elemento errado, e isso vale registrar: min-width: 0 em .lab-exercicios > * não mudou nada, porque astro-island é display: contents — a ilha não é caixa nenhuma, e o item de grid é a raiz do componente. Fixes de layout de ilha moram no CSS do componente, não no da página.

E a correção que mais importa: nas duas sessões anteriores eu afirmei, duas vezes, que a falha de [mobile] providers-lab era artefato de container. A reprodução em main era real, mas a causa não era o browser: a página rolava 812px num viewport de 412 desde a entrega 5, e era isso que jogava o clique sob o header fixo. Com o cartão consertado ela caiu para 528 e o teste passa 3/3 — e a suíte e2e inteira fechou com zero falhas pela primeira vez neste container. O headless shell do CI não disparava, e foi o que me fez atribuir ao ambiente um defeito da página.

Entrou a guarda que faltava: nenhum lab publicado rola na horizontal, com a lista derivada do roadmap. A regra existia só para cpu, gates e rpn, cada uma escrita no spec do seu lab — e por isso as páginas do OTel nunca foram medidas em quatro entregas. As duas que seguem rolando entram como dívida medida: o conjunto não isenta, ele afirma que ainda rola, e cai quando alguém consertar. E é escopada ao mobile, porque no desktop as duas não rolam.

Tentei overflow-wrap: break-word no código inline. Não mudou número nenhum, então saiu: manter uma regra com um comentário afirmando que ela conserta algo é a mentira que este repositório mais tenta evitar.

E a guarda da entrega 16 pegou os dois labs novos, sozinha

Três vezes, sem eu lembrar dela: nomeou collectorWorld fora da varredura de emissões perdidas, cobrou o teto de cruzamentos do lab novo, e o SEM_PALCO existe porque ela cobrou teto de um lab que não tem palco. Era o ponto de fazê-la derivada.

Estado: 1226 unitários (104 arquivos) — 72 novos —, e2e 323 com zero falhas, typecheck, boundaries, catálogo e build verdes. mvn compile verde nos três labs reais com Java.

Dois dos quinze labs que o roadmap prometia saíram de coming para publicados.

Entrega 18 — A amostragem, a dívida de rolagem fechada, e duas matérias ✅

Quatro coisas, nesta ordem, e a primeira é a que eu havia declarado como dívida minha.

A dívida de rolagem horizontal era o mesmo defeito em três camadas

providers (528px num viewport de 412) e three-pillars (464) eram a dívida medida que a entrega 17 deixou escrita. Eu havia dito ao Luigi que o que sobrava era prosa e não era do cartão. Estava errado, e a medição é curta: o li do distrator media 483 dentro de um <ul> corretamente preso em 322.

É o mesmo defeito do cartão, duas camadas abaixo: todo item de grid tem min-width: auto, que é min-content, e no cartão de exercício min-content é sempre algo que não quebra — a linha mais longa do <pre>, um identificador Java de 50 caracteres, um caminho de arquivo na linha de origem. Cada camada só apareceu depois de a de cima sair, porque a maior esconde a menor.

overflow-wrap: anywhere e não break-word porque só anywhere entra no cálculo do min-content. É exatamente por isso que a tentativa com break-word da entrega 17 não mudou nenhum número medido — e ela foi retirada em vez de mantida com um comentário afirmando um conserto que não acontecia.

As três páginas medem 412 em 412. AINDA_ROLA_NO_MOBILE ficou vazio e continua existindo: é o lugar onde uma dívida de rolagem se declara medida, e o teste dela afirma que a dívida existe em vez de isentar a página.

head-vs-tail-sampling: o terceiro lab vindo de .mmd

Cenário de fluxo pela régua da ADR §9 — carga atravessando partes, e o instante de cada decisão é a posição dela no desenho. Sete nós, nove arestas, topologia toda no esqueleto.

Três teses, cada uma cobrada comparando dois runs do mesmo mundo:

  1. head sampling decide antes de o erro existir. O erro mora no último span de propósito. O head está antes do leque no desenho, e há teste cobrando isso do esqueleto: mover o head para depois do balanceador deixaria o lab rodando e passaria a ensinar que as duas amostragens são intercambiáveis;
  2. tail sampling guarda o erro e cobra memória. A retenção é um número do run em cada gateway;
  3. sem roteamento por trace, o trace chega quebrado. Cada gateway decide certo sobre o pedaço que recebeu, e o pedaço não era um trace. Em produção nada reporta isso, porque nenhum backend sabe quantos spans o trace tinha — o modelo sabe, e é por isso que aqui o defeito é um número.

A diferença mecânica com o lab do Collector tem teste no esqueleto: lá duas arestas saem da mesma porta, e isso é cópia; aqui são duas portas, e isso é escolha. Um receiver copia, um balanceador decide.

Dois defeitos meus, corrigidos por medição e não por revisão:

  • a primeira versão decidia com trace % N. Com “um erro a cada 4” e “guarde 1 em 4” os restos coincidem e o head sampler aparecia guardando todos os erros — o lab provaria o contrário do que ensina. Agora a decisão passa por um hash do id, que é o que o TraceIdRatioBased de verdade faz, e há teste cobrando que o erro seja cortado justamente nesse alinhamento;
  • os dois painéis de gateway mostravam a soma dos dois. Mentira pequena, consequência grande num lab cuja tese é cada instância decidir com o pedaço dela. O estado virou por instância, e o e2e cobra que os números divirjam — o que só é possível com o tráfego em 1, e é por isso que esse controle existe na página.

A contraparte real, e as fontes conferidas em vez de lembradas

labs/head-vs-tail-sampling/ sobe: a aplicação, o collector que só roteia, e dois collectors que amostram na cauda rodando o mesmo tail.yaml.

A conferência das fontes rendeu três correções que a memória não daria: os caminhos da documentação mudaram de /docs/collector/deployment/ para /docs/collector/deploy/; o exportador foi renomeado de loadbalancing para load_balancing; e a página architecture/ afirma que o leque de um receiver é uma chamada síncrona — se o primeiro processor de uma pipeline travar, as outras não recebem e o receiver para de aceitar dado. Isso entrou como mal-entendido novo no lab do Collector, com fonte.

Três exercícios, em três arquivos diferentes, e isso é a lição: a decisão de amostragem é tomada em três lugares de três donos diferentes — o amostrador do SDK, o routing_key, as políticas. Nenhum dos três vê o arquivo dos outros dois, e cada um pode, sozinho, apagar o trace do erro.

Duas matérias, e a regra de pareamento honrada

  • “Agent or gateway is a blast-radius question” (fase 3): o collector-pipeline estava no ar sem teoria apontando para ele. A tese é que a forma do deployment é o que cada forma consegue saber — o agente sabe onde roda, o gateway sabe o que mais chegou;
  • “Sampling is a statement about what you will not know” (fase 5): a teoria do lab novo. Inclui o que a documentação diz e quase ninguém compõe: dois amostradores em série se multiplicam.

Um conserto que apareceu de lado

A linha de origem do exercício dizia “OpenTelemetry Java” fixo no componente. Com os exercícios de YAML da entrega 17, a página do Collector passou a dizer “OpenTelemetry Java collector 0.160.0” — número certo com o nome errado em volta. Quem sabe qual é a coisa agora é quem a lê do disco.

E o lab nasceu com página head-and-tail-sampling e id de mapa head-vs-tail-sampling. Todos os outros labs têm id igual ao slug, e a divergência quebraria a guarda derivada que visita labs/<id>/ para cobrar a contagem de exercícios — ela caiu, e o nome foi unificado.

Estado: 1276 unitários (106 arquivos), e2e 343 com zero falhas, typecheck, boundaries, catálogo e build (44 páginas) verdes. mvn compile verde nos quatro labs reais com Java.

Três dos quinze labs do roadmap do OTel seguem publicados, e a fase 5 ganhou lab, matéria e exercícios no mesmo dia.

Entrega 19 — O caminho dourado: handbook é submissão, com versão ✅

O pedido: qualquer pessoa ou agente cria, submete e mantém o handbook dela, usa em qualquer ambiente, e a casa sempre suporta a release anterior.

O que mudou de estrutura

Publicar um handbook exigia editar apps/site/src/data/handbooks.ts mais o mapa mais um arquivo de tema. Isso cobrava o monorepo inteiro de quem só domina um assunto, e deixava quem mantém a casa dono de conteúdo de terceiro. Agora é handbooks/<id>/handbook.yaml: um diretório, um dono, zero TypeScript.

O algorithms foi o primeiro a migrar, e a página dele hoje é gerada do manifesto — nome, fases, itens e tema vindos do arquivo.

A compatibilidade é mecanismo, e as duas versões são reais hoje

apiVersion nomeia o contrato, não a release. A lista de versões suportadas tem estado, data e um leitor por versão, e é ela que o teste varre:

  • versão na lista tem exemplar de conformidade, e o exemplar carrega em cada suíte. Apagar o leitor de uma versão listada derruba a suíte nomeando a versão;
  • a recusa separa as duas direções: quem escreveu contra uma versão mais nova é mandado atualizar a casa (nada no arquivo dele está errado); quem escreveu contra uma aposentada é mandado migrar. “Versão inválida” manda os dois procurarem no lugar errado;
  • migrate existe com a tabela vazia, e isso é declarado: uma versão só pode ser aposentada quando existe migração automática, e é ali que ela vai morar. Dizer “migrado” sem migrar seria a mentira que este repositório mais combate.

E as duas versões não são promessa sobre um v2 futuro: os três handbooks originais são objetos TypeScript escritos à mão, que é literalmente a versão anterior deste contrato, e ela está na lista com esse nome.

A ferramenta, e o molde que nasce válido

Cinco comandos em node puro — nada depende do Astro, de navegador, ou de estar dentro deste monorepo rodando:

comandoo que faz
newescreve um manifesto que passa no check
checko mesmo comando que o CI roda
exportpacote portátil em JSON: manifesto normalizado + artigos com corpo
migratesobe o manifesto de versão
codeownersderiva o CODEOWNERS dos manifestos

A ponte com o schema (que é TypeScript) é o compilado, garantido em dia pela própria ferramenta. A alternativa seria uma segunda cópia das regras em JS — e duas cópias da mesma regra divergem, como já divergiram aqui entre o mapa e o catálogo de labs.

Andei o caminho antes de dizer que ele funciona — e achei um defeito meu

Escrevi o primeiro artigo do handbook submetido (“A stack is a box”, com o sp do RISC-V conferido no próprio psABI: x2, pilha crescendo para baixo, 128 bits de alinhamento). Trocar o status do item de coming para available reprovou o meu teste de compatibilidade — e o teste estava mal projetado, não o artigo.

Ele comparava o manifesto vivo com a expectativa congelada, misturando duas coisas: “o leitor traduz fielmente” e “o conteúdo nunca muda”. Agora o par é congelado dos dois lados, no pacote de formato: a entrada é o manifesto como ele estava no dia da migração, a saída é o objeto que o catálogo em TypeScript tinha. Conteúdo novo entra no manifesto vivo e não encosta nele. Com um teste a mais, cobrando que a expectativa cubra o handbook inteiro — senão alguém “conserta” uma divergência apagando o campo.

Este é o argumento do caminho dourado em uma frase: ele só vale depois de alguém andá-lo, e quem o escreve é quem tem de andar primeiro.

O limite, escrito em vez de descoberto

O manifesto não descreve comportamento: lab que roda é modelo, e modelo é TypeScript. E o map do roadmap é desenho com coordenadas, que é a razão honesta de OTel e CPU seguirem em TypeScript. As duas coisas estão em docs/handbooks.md e na ADR §10, com o porquê.

Estado: 1354 unitários (110 arquivos), typecheck, fronteira, catálogo, handbooks, codeowners, build (46 páginas) e varredura de links verdes. CI ganhou os dois portões novos.

Entrega 20 — o quarto handbook, e ele veio de fora

19/10/2026. Splunk, submetido pelo caminho dourado. Dois labs de fluxo, três artigos, seis exercícios, duas contrapartes que sobem, e a confirmação do que a entrega 19 prometeu: handbooks/splunk/handbook.yaml é o manifesto inteiro, e o CODEOWNERS saiu dele.

Por que Splunk tem lugar num motor de fluxos

O produto é um pipeline antes de ser uma barra de busca. A documentação dele não descreve módulos, descreve quatro fases por onde o dado passa — input, parsing, indexing, search — e a regra que mais custa em produção é sobre qual componente executa cada fase. Isso não é um diagrama: é coisa se movendo entre peças, que é o que este motor roda.

Lab 1 (fase 2): onde o parsing acontece

A fase de parsing acontece uma vez, no primeiro componente capaz dela. Disso sai um defeito que não dá erro nenhum: um props.conf com LINE_BREAKER é correto ou é inerte dependendo de em qual máquina está. Três lugares × duas topologias = seis combinações, e duas funcionam.

O terceiro experimento é o que acontece com times que nunca tocaram na configuração: alguém põe um heavy forwarder no caminho por razão de rede, a fase se muda com ele, e o stack trace de doze linhas volta a entrar como doze eventos. Ninguém editou nada.

Lab 2 (fase 3): a escada dos baldes

Até onde dá para pesquisar não é uma configuração — é o que sobra da conta entre quatro limites e o volume diário. Com os defaults de verdade (500 000 MB de índice, 188 697 600 segundos de retenção) e 20 GB em disco por dia, a janela é de 26 dias. O número 26 não está escrito em lugar nenhum, e é por isso que esta pergunta é respondida errado de boa-fé: quem responde cita o número da configuração que tem o nome da coisa perguntada.

E congelar é apagar, a menos que alguém tenha escrito coldToFrozenDir. Ligar e desligar o arquivo no lab não muda nenhum outro número da tela — é a diferença invisível.

O teste que achou a região em que o meu modelo não vale

Pedi 60 GB de índice com os 300 baldes mornos do default, e o teste reprovou com uma janela de 11 dias onde a conta dá 3. A causa é real: o morno sozinho cabe 225 GB, o Splunk congela o balde mais velho do índice (que ali seria um morno), e um nó deste motor conhece apenas o próprio estado.

Em vez de modelar errado uma região inteira de configuração em silêncio, ela passou a ser medida e nomeada na tela: quando maxWarmDBCount × maxDataSize passa do limite do índice, o lab diz que o modelo para ali e manda baixar um dos dois. Limite declarado em vez de mentira pequena.

O que mudou na casa por causa do quarto handbook

  • o formato do exercício saiu do otel-domain para o handbook-format, junto das regras que valem para todo exercício do repositório. Ele é domínio-neutro, e deixá-lo lá obrigaria o segundo domínio a importar o primeiro ou a manter uma cópia;
  • duas varreduras de tela passaram a sair do catálogo, e não do roadmap. O map existe só nos dois handbooks mais antigos, então um lab de handbook submetido passava inteiro por fora da medição de cruzamentos e da de rolagem horizontal no telefone — a que já achou três defeitos. Está em docs/handbooks.md: é o que um lab submetido ganha de graça;
  • versaoDoArquivo virou tabela. O mesmo compose.yaml do lab novo sobe duas imagens de produtos diferentes, e qual delas executa um arquivo depende da pasta;
  • a fronteira motor↔domínio deriva a lista de pacotes de domínio do disco (a escrita à mão tinha dois nomes e já estava velha: algo-domain nunca entrou nela), ganhou a lista de termos do terceiro domínio, e passou a vigiar o handbook-format — que é o contrato em que um handbook de qualquer assunto é escrito. Ela acusou duas palavras de assunto num docblock meu no mesmo dia. De 97 para 107 arquivos varridos.

O que foi verificado nas contrapartes, e o que não foi

Toda configuração dos dois labs foi lida contra a documentação atual, com o link em cima de cada linha — e as páginas da Splunk estão bloqueadas por DNS neste contêiner, então a leitura foi pelo texto citado das páginas oficiais na busca, nunca de memória. Isso corrigiu coisas que eu teria escrito erradas: o grupo de captura do LINE_BREAKER não come a data da linha seguinte (o evento seguinte começa no fim do grupo), e EVENT_BREAKER existe para distribuição entre indexadores, não para impedir evento quebrado.

O que não foi verificado: um run. Este contêiner não tem daemon de Docker, e os dois README dizem isso em vez de prometer que sobem.

Estado: 1 483 unitários (116 arquivos), typecheck, fronteira, catálogo, handbooks, codeowners, build (52 páginas) e varredura de links verdes. E2E: 10 testes novos de tela, mais os dois labs entrando nas varreduras de desenho e de rolagem horizontal.