Contexto
Spoke Cohort Student Edition V5, sinkra-os 3.7.0, skill sinkra-update v1.9.0 (a versão do bundle). Rodei /sinkra-update (full-sweep default) num workspace já mapeado e migrado.
Achei 4 defeitos. Os dois primeiros são de dado; os dois últimos, de sinal (o motor mente sobre o que fez). Todos reproduzidos e verificados. Não achei issue relacionada (busquei corpo + título por sinkra-update, process_id, workflow_kind, prepass, dry-run, sweep-all).
Diagnóstico transversal: 3 dos 4 não são bugs de transform — são falhas de composição da cadeia. Cada um dos 7 transforms foi entregue por uma story própria e é correto isoladamente; ninguém é dono da ordem entre eles. A ordem vive em comentário em prosa e na ordem das linhas de sweep-all.js. Não existe nada que verifique a invariante "se a saída de um transform pode ser rejeitada por um portão posterior, ele tem que rodar antes desse portão" — que é exatamente o que os defeitos 2 e 4 violam. O pre-pass do executor (232.W1.6) já foi essa mesma armadilha, resolvida localmente como caso específico em vez de como regra.
Defeito 1 — process_id migrado como se fosse vocabulário → divergência R18 a cada sweep
Onde: scripts/lib/transform-v5.js#migrateProgress (escrita ~L593 e detecção ~L583)
O modo --state renomeia o sufixo _stem dentro de process_id (sp_acme_stem → sp_acme_workspace). Mas process_id é identidade do processo, não vocabulário — e o modo artifact do mesmo transform preserva process.id de propósito (o comentário da L356 diz "já-válidos (sp_x_stem) intactos"), migrando só process.type. Os dois modos do mesmo transform discordam entre si.
Efeito: a identidade é declarada em 3 lugares — progress.json (process_id), sinkra-output.json (process.id) e entity-execution-registry.json (process_id). Só o progress.json é reescrito, então ele passa a divergir dos outros dois.
Agravante: _stem no process_id também entra em looksLegacy, o que derrota o early-return already-unified. Um state já migrado é reescrito a cada invocação — a idempotência não protege, porque o defeito é a própria escrita.
Impacto real medido: o full-sweep reverteu uma reconciliação R18 que tinha sido feita à mão numa sessão limpa horas antes.
Correção: process_id/process.id na denylist, mesma classe de exceção permanente de branch_id (D10) — e fora de looksLegacy.
Defeito 2 — v5-legacy com workflow_kind fora do enum é IMPOSSÍVEL de migrar
Onde: ordem da cadeia em scripts/sweep-all.js (o #7 roda depois do #4)
O transform #1 valida AJV strict contra o schema final antes de gravar, e o schema rejeita as 3 formas que o transform #7 converte (workflow_kind fora do enum, activation_condition escalar, honey_artifact). Como o #7 roda depois do #4, um artefato v5-legacy que carregue qualquer delas HALTa permanentemente no #1 — o #7 nunca chega a agir.
Repro: a fixture do próprio bundle, scripts/test/fixtures/legacy-with-managed-agent.json (derivada do corpus Cohort T1), tem event_processes[2].workflow_kind: "bee". Rodar sweep-all.js sobre ela: transform1 → halt-invalid. É a causa das 5 asserções T60b falhando no test-kit distribuído (node scripts/test/run-tests.js → 556 PASS / 5 FAIL no bundle como entregue).
Atenção ao diagnosticar: o relatório mostra erros AJV espalhados (enum @ /process/type, required @ /entity_types, additionalProperties @ (root)). Isso é ruído a jusante — o #1 aborta, o arquivo fica em disco ainda legacy, e as asserções seguintes validam o arquivo não-migrado. O erro real é um só: enum @ /event_processes/2/workflow_kind.
Impacto: aluno com output legado contendo um event_process bee não tem caminho de migração nenhum.
Correção: pre-pass estrutural antes do #1, irmão de prepass-executor-rename.js. Não dá para reusar o CLI do #7: ele é AJV-strict-or-halt e valida o doc inteiro, que nessa posição ainda é v5-legacy por definição — ele converte certo e então recusa gravar. O pre-pass tem de reusar a lib pura (transform-structural-5x.js#migrateStructural5x) sem o portão AJV, exatamente como o pre-pass do executor reusa renameManagedAgentExecutors.
Defeito 3 — exit code 1 permanente: o alarme que se aprende a ignorar
Onde: scripts/lib/open-decisions.js (MARCAS_DE_COBERTURA) + discovery de sweep-all.js
O sweep sai com exit 1 quando há open_decisions. Num spoke saudável todas as entradas eram benignas, então o exit 1 ficava permanentemente ligado e parava de carregar informação. Duas famílias:
- Os pre-passes num doc já na versão final reportam
noop com razão declarada ("o prepass cobre versões < X"), que o classificador não cobre → 1 open_decision por pre-pass, por documento.
- O discovery adota como candidato a output qualquer arquivo
sinkra-output*.json, o que inclui sidecars que o próprio motor escreve (*.migration-report.json) e templates (.sinkra/templates/). O motor reportava a própria saída como "classificar manualmente".
O item 2 tem um risco latente junto: a varredura de outputs alcança .sinkra/templates/, então um template que por acaso satisfizesse o reconhecimento por conteúdo seria migrado de fato — migrar o molde em vez do objeto.
O código já reconhece esse modo de falha: o comentário da marca G3.1 diz textualmente "o alarme que se aprende a ignorar". São as próximas instâncias do mesmo problema.
Correção: marca de cobertura para o noop dos pre-passes — ancorada em schema_version final, porque o veredito de classify() é final OU unknown, e unknown (sem versão, major diferente, semver inválido, versão maior que o alvo) é justamente o caso que precisa de olho humano. Cobrir a frase toda silenciaria os dois. Mais uma política de "nunca foi alvo" (distinta da frozen, que é "foi alvo, hoje é histórico") para sidecar e template, com skip governado — log nomeado + contador.
Defeito 4 — --dry-run reporta HALT falso e assusta sobre artefato migrável
Onde: estrutura da cadeia em sweep-all.js
Cada estágio é um processo separado que lê o arquivo do disco. Com --dry-run nenhum grava, então o estágio N+1 lê o arquivo original, não o resultado do estágio N. Em cadeia que depende de pre-pass isso produz HALT que o run real não produz.
Repro: mesma fixture do defeito 2. --dry-run → transform1: halt-invalid. Run real → transform1: migrated, 0 halts.
Impacto: um aluno que rode o dry-run antes de migrar lê "impossível migrar" sobre um artefato perfeitamente migrável, e para. Um relatório que assusta sem motivo é tão ruim quanto um que cala — os dois ensinam a não confiar no relatório.
Correção: simular estágio a estágio é estruturalmente impossível num pipeline encadeado de processos. O caminho é rodar a cadeia de verdade sobre uma cópia em temp, remapear os paths de volta ao arquivo real (pelo diretório, não pelo arquivo — os estágios derivam irmãos do basename: .legacy.bak, .migration-report.json) e limpar o temp no finally. O contrato read-only (VC-3) se mantém: o temp fica fora da árvore varrida.
Sugestão estrutural
Consertar os 4 individualmente resolve estes 4. O que retira a classe é tornar a cadeia um objeto declarado — ordem e restrições entre transforms em dado, com um teste que falhe quando um transform novo entra num ponto onde um portão posterior rejeita a saída dele. Assim o transform #8 não nasce com o defeito 2 de novo.
Nota sobre distribuição
Apliquei as 4 correções localmente para desbloquear e validar (test-kit 556→599 PASS, 0 FAIL; sweep do spoke em 0 halts / 0 open_decisions / exit 0 nos 3 modos). Elas não sobrevivem: .claude/skills/sinkra-*/ é verbatim-install no MANIFEST, então a próxima atualização sobrescreve tudo. Estou reportando, não propondo patch — se o time quiser os diffs, é só pedir.
Relacionado: o G5 já registrado no próprio SKILL.md (drift entre ~/.claude/skills/sinkra-update e a cópia do repo) fica pior com defeito de motor, porque a máquina pode rodar uma versão diferente da que o aluno acha que tem.
Contexto
Spoke Cohort Student Edition V5,
sinkra-os3.7.0, skillsinkra-updatev1.9.0 (a versão do bundle). Rodei/sinkra-update(full-sweep default) num workspace já mapeado e migrado.Achei 4 defeitos. Os dois primeiros são de dado; os dois últimos, de sinal (o motor mente sobre o que fez). Todos reproduzidos e verificados. Não achei issue relacionada (busquei corpo + título por
sinkra-update,process_id,workflow_kind,prepass,dry-run,sweep-all).Diagnóstico transversal: 3 dos 4 não são bugs de transform — são falhas de composição da cadeia. Cada um dos 7 transforms foi entregue por uma story própria e é correto isoladamente; ninguém é dono da ordem entre eles. A ordem vive em comentário em prosa e na ordem das linhas de
sweep-all.js. Não existe nada que verifique a invariante "se a saída de um transform pode ser rejeitada por um portão posterior, ele tem que rodar antes desse portão" — que é exatamente o que os defeitos 2 e 4 violam. O pre-pass do executor (232.W1.6) já foi essa mesma armadilha, resolvida localmente como caso específico em vez de como regra.Defeito 1 —
process_idmigrado como se fosse vocabulário → divergência R18 a cada sweepOnde:
scripts/lib/transform-v5.js#migrateProgress(escrita ~L593 e detecção ~L583)O modo
--staterenomeia o sufixo_stemdentro deprocess_id(sp_acme_stem→sp_acme_workspace). Masprocess_idé identidade do processo, não vocabulário — e o modo artifact do mesmo transform preservaprocess.idde propósito (o comentário da L356 diz "já-válidos (sp_x_stem) intactos"), migrando sóprocess.type. Os dois modos do mesmo transform discordam entre si.Efeito: a identidade é declarada em 3 lugares —
progress.json(process_id),sinkra-output.json(process.id) eentity-execution-registry.json(process_id). Só oprogress.jsoné reescrito, então ele passa a divergir dos outros dois.Agravante:
_stemnoprocess_idtambém entra emlooksLegacy, o que derrota o early-returnalready-unified. Um state já migrado é reescrito a cada invocação — a idempotência não protege, porque o defeito é a própria escrita.Impacto real medido: o full-sweep reverteu uma reconciliação R18 que tinha sido feita à mão numa sessão limpa horas antes.
Correção:
process_id/process.idna denylist, mesma classe de exceção permanente debranch_id(D10) — e fora delooksLegacy.Defeito 2 — v5-legacy com
workflow_kindfora do enum é IMPOSSÍVEL de migrarOnde: ordem da cadeia em
scripts/sweep-all.js(o #7 roda depois do #4)O transform #1 valida AJV strict contra o schema final antes de gravar, e o schema rejeita as 3 formas que o transform #7 converte (
workflow_kindfora do enum,activation_conditionescalar,honey_artifact). Como o #7 roda depois do #4, um artefato v5-legacy que carregue qualquer delas HALTa permanentemente no #1 — o #7 nunca chega a agir.Repro: a fixture do próprio bundle,
scripts/test/fixtures/legacy-with-managed-agent.json(derivada do corpus Cohort T1), temevent_processes[2].workflow_kind: "bee". Rodarsweep-all.jssobre ela:transform1 → halt-invalid. É a causa das 5 asserções T60b falhando no test-kit distribuído (node scripts/test/run-tests.js→ 556 PASS / 5 FAIL no bundle como entregue).Atenção ao diagnosticar: o relatório mostra erros AJV espalhados (
enum @ /process/type,required @ /entity_types,additionalProperties @ (root)). Isso é ruído a jusante — o #1 aborta, o arquivo fica em disco ainda legacy, e as asserções seguintes validam o arquivo não-migrado. O erro real é um só:enum @ /event_processes/2/workflow_kind.Impacto: aluno com output legado contendo um event_process
beenão tem caminho de migração nenhum.Correção: pre-pass estrutural antes do #1, irmão de
prepass-executor-rename.js. Não dá para reusar o CLI do #7: ele é AJV-strict-or-halt e valida o doc inteiro, que nessa posição ainda é v5-legacy por definição — ele converte certo e então recusa gravar. O pre-pass tem de reusar a lib pura (transform-structural-5x.js#migrateStructural5x) sem o portão AJV, exatamente como o pre-pass do executor reusarenameManagedAgentExecutors.Defeito 3 — exit code 1 permanente: o alarme que se aprende a ignorar
Onde:
scripts/lib/open-decisions.js(MARCAS_DE_COBERTURA) + discovery desweep-all.jsO sweep sai com exit 1 quando há
open_decisions. Num spoke saudável todas as entradas eram benignas, então o exit 1 ficava permanentemente ligado e parava de carregar informação. Duas famílias:noopcom razão declarada ("o prepass cobre versões < X"), que o classificador não cobre → 1 open_decision por pre-pass, por documento.sinkra-output*.json, o que inclui sidecars que o próprio motor escreve (*.migration-report.json) e templates (.sinkra/templates/). O motor reportava a própria saída como "classificar manualmente".O item 2 tem um risco latente junto: a varredura de outputs alcança
.sinkra/templates/, então um template que por acaso satisfizesse o reconhecimento por conteúdo seria migrado de fato — migrar o molde em vez do objeto.O código já reconhece esse modo de falha: o comentário da marca G3.1 diz textualmente "o alarme que se aprende a ignorar". São as próximas instâncias do mesmo problema.
Correção: marca de cobertura para o
noopdos pre-passes — ancorada emschema_version final, porque o veredito declassify()éfinalOUunknown, eunknown(sem versão, major diferente, semver inválido, versão maior que o alvo) é justamente o caso que precisa de olho humano. Cobrir a frase toda silenciaria os dois. Mais uma política de "nunca foi alvo" (distinta da frozen, que é "foi alvo, hoje é histórico") para sidecar e template, com skip governado — log nomeado + contador.Defeito 4 —
--dry-runreporta HALT falso e assusta sobre artefato migrávelOnde: estrutura da cadeia em
sweep-all.jsCada estágio é um processo separado que lê o arquivo do disco. Com
--dry-runnenhum grava, então o estágio N+1 lê o arquivo original, não o resultado do estágio N. Em cadeia que depende de pre-pass isso produz HALT que o run real não produz.Repro: mesma fixture do defeito 2.
--dry-run→transform1: halt-invalid. Run real →transform1: migrated, 0 halts.Impacto: um aluno que rode o dry-run antes de migrar lê "impossível migrar" sobre um artefato perfeitamente migrável, e para. Um relatório que assusta sem motivo é tão ruim quanto um que cala — os dois ensinam a não confiar no relatório.
Correção: simular estágio a estágio é estruturalmente impossível num pipeline encadeado de processos. O caminho é rodar a cadeia de verdade sobre uma cópia em temp, remapear os paths de volta ao arquivo real (pelo diretório, não pelo arquivo — os estágios derivam irmãos do basename:
.legacy.bak,.migration-report.json) e limpar o temp nofinally. O contrato read-only (VC-3) se mantém: o temp fica fora da árvore varrida.Sugestão estrutural
Consertar os 4 individualmente resolve estes 4. O que retira a classe é tornar a cadeia um objeto declarado — ordem e restrições entre transforms em dado, com um teste que falhe quando um transform novo entra num ponto onde um portão posterior rejeita a saída dele. Assim o transform #8 não nasce com o defeito 2 de novo.
Nota sobre distribuição
Apliquei as 4 correções localmente para desbloquear e validar (test-kit 556→599 PASS, 0 FAIL; sweep do spoke em 0 halts / 0 open_decisions / exit 0 nos 3 modos). Elas não sobrevivem:
.claude/skills/sinkra-*/éverbatim-installno MANIFEST, então a próxima atualização sobrescreve tudo. Estou reportando, não propondo patch — se o time quiser os diffs, é só pedir.Relacionado: o G5 já registrado no próprio SKILL.md (drift entre
~/.claude/skills/sinkra-updatee a cópia do repo) fica pior com defeito de motor, porque a máquina pode rodar uma versão diferente da que o aluno acha que tem.