O Crystal Ball roda como app declarado, e o manifesto nomeia o motor sem carregá-lo - #22
Closed
davidbenal wants to merge 1 commit into
Closed
Conversation
…sem carregá-lo
A Etapa 1 fixou a fronteira: o manifesto NOMEIA `motor` e `funcao` de uma lista
fechada, e o adaptador resolve. Quem resolvia era nada: `_run_reason_step`
recusava duro qualquer `motor != "template"`, e os `outputs` de um passo `reason`
eram template do próprio manifesto, sem caminho por onde o retorno de uma função
entrasse. Este commit abre esse seam e o fecha com guardas medidas.
O motor é VENDORADO, cópia byte-idêntica de `sultowskigus/mk-crystalball` em
`13a55d5` (blob `22fbdd01`, ver `lib/motores/PROVENIENCIA.md`). Contra um checkout
apontado por variável de ambiente, que é escopo de máquina: não viaja pelo git, a
outra estação precisaria do próprio clone no ref certo, e o CI deste repo não
alcança repositório fora da org. A cópia paga um arquivo com dois donos para que o
CI consiga testar o adaptador.
Carregar o motor sem arrastar a casca custa duas peças, e as duas foram provadas
por execução, não por leitura:
- `webapp/store.py` inteiro arrastaria psycopg, a taxonomia do blueprint e um
`init_store()` que escreve `crystalball.db` ao lado do fonte, tudo por três
tuplas de 17 strings que o motor lê em `:182`. As tuplas entram inlinadas em
`carga.py`, e o módulo sintético é registrado direto em `sys.modules`, sem
tocar `sys.path`. Medido: nem flask, nem cv2, nem numpy, nem psycopg, nem
`crystalball.db`.
- `API_KEY` e `MODEL` da linha 37 do motor são CÓDIGO MORTO no caminho de
chamada: quem lê a chave é `_load_config()`, chamado fresco dentro de `_call`
(`:881`). Definir a variável antes do import não faz nada, e reatribuir
`mod.API_KEY` também não. A chave entra por `chave_ligada()`, um contexto, que
a põe no ambiente pelo tempo da chamada e a tira depois, inclusive em exceção.
O adaptador existe porque quatro comportamentos do motor falham em SILÊNCIO:
1. `pesquisar_referencias` devolve `None` em falha e em dossiê vazio, e
`caminhos_criativos` refaz a pesquisa quando recebe `dossie is None` (`:1181`).
A pesquisa é a chamada mais lenta do sistema. `None` vira `{}`, que é falsy
sem ser None, e a run deixa de pagar duas vezes.
2. Os cinco caminhos saem com `numero`, sem `id`, e a retomada casa por `id`. Sem
injetar `id`, toda escolha do humano é recusada, longe da causa. E não dava
para consertar no manifesto: `_interpolate` não itera lista.
3. `diagnose` tem `if dict` / `elif str não vazia` sem `else` (`:1207-1212`).
Para None, "", lista ou int, o diagnóstico sai bonito, sem o caminho que a
pessoa escolheu, que é justo o que o interpolador devolve quando um caminho
não resolve. Os dois defeitos se somavam.
4. `recalcular_nota` renormaliza pelo peso usado (`:57-68`): sem `gancho_3s`, que
é 25%, a nota sai igual e nada acusa. O adaptador não pode consertar isso sem
declarar régua, o que o contrato proíbe, então ele CONTA os critérios que
voltaram e devolve o que faltou.
`aceita: [item_da_lista, texto_livre]` era a peça que o contrato da Etapa 1
deixou explicitamente para cá, e ela não podia ficar em `ui`: muda o TIPO do
valor que o motor recebe. Vive no corpo do passo, com `campo_livre` nomeando a
chave, e é persistida junto das opções, para que editar o YAML depois não mude o
que uma pausa já feita aceita. Texto em branco é recusado; run pausada antes
desta versão continua retomável.
Três decisões do David nesta rodada, todas registradas no cabeçalho do manifesto:
o passo 3 segue o código, com UMA direção afiada mais o campo livre; a run termina
`done`, porque pausa terminal com o termo de `runs.status='paused'` no
deriveColumn poria toda sessão concluída em "precisa de você" para sempre (o que
o contrato manda não fechar é a SESSÃO, e o card e a conversa seguem abertos); e
o teto de duração é 45s, que é a única guarda contra o truncamento duro de
`max_tokens=16384` em `_gerar_conceito`, onde um pacote de 15 cenas já ocupa o
teto de saída do modelo.
274 testes passam, 194 de base e 80 novos, e o gate `E9,F` do CI segue limpo
inclusive sobre o vendorado. O relatório informativo do ruff volta aos 23 achados
herdados por `per-file-ignores` com os 30 códigos medidos no arquivo copiado:
reformatá-lo quebraria a conferência de proveniência por blob sem mudar
comportamento nenhum.
O que este commit NÃO faz, e está registrado: nenhuma rodada com chave real, que
custa dinheiro e é decisão do operador; o custo segue `nao-apurado` porque o motor
não lê `usageMetadata` e `models.yaml` não tem preço por milhão de token; e o lado
da tela, o disparo assíncrono e o termo de `runs` no deriveColumn são as frentes
seguintes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Primeira frente da Etapa 2 (
PROD-2128). Empilhada emdavid/PROD-2127-passo-reason-e-retomada, não namain, porque a PR #16 da Etapa 1 ainda não mergeou (ADR-024).O que muda
_run_reason_steprecusava duro qualquermotor != "template", e osoutputsde um passoreasoneram template do próprio manifesto, sem caminho por onde o retorno de uma função entrasse. Este PR abre esse seam, e o fecha com guardas medidas.O motor é vendorado: cópia byte-idêntica de
sultowskigus/mk-crystalballem13a55d5, blob22fbdd01, verlib/motores/PROVENIENCIA.md. A alternativa era um checkout apontado por variável de ambiente, que é escopo de máquina: não viaja pelo git, a outra estação precisaria do próprio clone no ref certo, e o CI deste repo não alcança repositório fora da org.Duas correções que só apareceram por execução
API_KEYeMODELda linha 37 do motor são código morto no caminho de chamada. Quem lê a chave é_load_config(), chamado fresco dentro de_call(:881). Definir a variável antes do import não faz nada, e reatribuirmod.API_KEYtambém não. A chave entra porchave_ligada(), um contexto que a põe no ambiente pelo tempo da chamada e a tira depois.webapp/store.pyinteiro arrastaria psycopg e uminit_store()que escrevecrystalball.dbao lado do fonte, por três tuplas de 17 strings. As tuplas entram inlinadas, o módulo sintético vai direto parasys.modules, esys.pathnão é tocado. Medido: nem flask, nem cv2, nem numpy, nem psycopg, nem banco criado.As quatro guardas, e por que cada uma existe
Cada uma corresponde a um comportamento do motor que falha em silêncio, medido em
13a55d5:pesquisar_referenciasdevolveNoneem falha, ecaminhos_criativosrefaz a pesquisa quandodossie is None:1153-1163,:1181Nonevira{}, que é falsy sem ser None. A run deixa de pagar a chamada mais lenta duas vezesnumero, semid, e a retomada casa porid:558+,workflow_runner.py:212idinjetado no adaptador. Não dava para consertar no manifesto:_interpolatenão itera listadiagnosetemif dict/elif str não vaziasemelse:1207-1212None,"", lista e int são recusados antes de gastar chamadarecalcular_notarenormaliza pelo peso usado: semgancho_3s, que é 25%, a nota sai igual:57-68criterios_faltandoepeso_faltando. Não declara régua, o que o contrato proíbeaceita: [item_da_lista, texto_livre]Era a peça que o contrato da Etapa 1 deixou explicitamente para esta etapa, e não podia ficar em
ui: muda o tipo do valor que o motor recebe. Vive no corpo do passo, comcampo_livrenomeando a chave, e é persistida junto das opções, para que editar o YAML depois não mude o que uma pausa já feita aceita. Texto em branco é recusado. Run pausada antes desta versão continua retomável, e há teste para isso.Três decisões do David, 2026-08-23
direcao_afiada, mais o campo livre. As "3 direções" da tabela da jornada eram intenção de produto, não descrição do que existe, e ficam fora.done. O último passo não é uma pausa: com o termo deruns.status='paused'noderiveColumn, pausa terminal poria toda sessão concluída em "precisa de você" para sempre. O que o contrato manda não fechar é a sessão, e isso continua verdade:session closesegue inalcançável de um passo, o card fica no kanban, a conversa fica aberta. Isto corrige uma frase docontrato-do-app.md.max_tokens=16384em_gerar_conceito(:1236), onde um pacote de 15 cenas já ocupa o teto de saída do modelo. A 1,5-3s por corte, 15 cenas caem entre 22 e 45 segundos.Prova
O
per-file-ignoresdo vendorado lista os 30 códigos medidos no arquivo, um a um, em vez de silenciar por atacado, e mantémE9eF, que é o gate de verdade. Reformatá-lo quebraria a conferência de proveniência por blob sem mudar comportamento nenhum.tests/test_manifesto_crystal_ball.pyé o teste que segura o contrato: otemplates/apps/crystal-ball.yamlreal, pelo runner real, com o motor substituído por um duplo que registra cadaargsrecebido. Prova a cadeia de estado inteira, as duas pausas nos dois ramos, e queduracaochega como número em vez de string.O que este PR não faz
nao-apurado. O motor não lêusageMetadata(é regressão do motor JS antigo, que tinha a contagem) emodels.yamlnão tem uma entrada de texto nem campo de preço por milhão de token. Fica issue.runsnoderiveColumnsão as frentes seguintes, nomk-ai-studio-app.