Projeto 03 · lifecycle-lint

O defeito está
na interseção.

Cada régua passa em revisão isolada. Somadas, duas delas alcançam a mesma audiência e cobram pressão semanal de 9,5 do mesmo contato, contra um teto de 6,0. Essa conta não consta de nenhum dos dois arquivos.

Seções desta página

12 regras de operação de CRM, não de engenharia de software
2 regras de portfólio, que leem o conjunto de jornadas
21 testes, cada regra com caso positivo e negativo
0 chamadas de modelo no diagnóstico

Como esses números são contados: doze regras é o que lifecycle-lint rules imprime, cada uma com identificador, severidade e escopo. Vinte e uma é a contagem de testes em tests/test_rules.py, que o CI roda em Python 3.10 e 3.12. Zero chamada de modelo é verificável no código, porque nenhuma das doze regras importa cliente de API e a explicação em prosa fica atrás de uma flag opcional. A dependência única é PyYAML, declarada no pyproject.toml.

01

Narrativa

01
Problema

Uma jornada com defeito de construção continua executando. O fluxo dispara, as mensagens saem, o relatório fecha com número positivo, e quem já converteu segue recebendo a sequência de quem não converteu. O efeito aparece semanas depois, no descadastro e na entregabilidade do domínio.

02
Decisão

Declarar a jornada em YAML e auditar o arquivo antes da publicação. Critério de saída, lista de supressão, teto de frequência e grupo de controle são quatro campos que a interface de automação não solicita, e que por isso ficam em branco. No formato declarado, a ausência deles vira falha com identificador e exit code.

03
Execução

Doze regras. Dez leem uma jornada por vez, e duas leem o portfólio de jornadas ativas. As duas de portfólio são a razão de a ferramenta existir. L011 mede a sobreposição entre as definições de audiência, L012 soma a pressão semanal ponderada que essas jornadas cobram do mesmo contato e compara com o teto configurado.

04
Aprendizado

A primeira versão de L011 usava índice de Jaccard e não acusava nada nos exemplos. Ela devolvia 0,5 justamente no caso que motivou a regra, porque um dos conjuntos tem um filtro a mais. Uma regra que erra o caso que a originou é pior que a ausência de regra, porque ela devolve resultado verde.

02

A auditoria em ação

O mesmo comando que roda na máquina roda no CI. A saída fica em português, que é o idioma da operação para a qual a ferramenta foi escrita, nas duas versões desta página.

Execuções da CLI

$ lifecycle-lint audit examples/journeys
winback_inativos · Reengajamento de inativos
ERRO L001 Jornada ativa sem critério de saída
ERRO L003 [repete_ate_reagir] Loop sem limite de iterações
ERRO L004 Audiência sem lista de supressão
AVISO L005 Segmento sem decaimento temporal
AVISO L008 Jornada ativa sem grupo de controle
checkout_abandonado · Recuperação de checkout abandonado
ERRO L002 [decide_desconto] Branch sem caminho padrão
ERRO L012 (com winback_inativos) Pressão agregada acima do teto por contato
As jornadas ['checkout_abandonado', 'winback_inativos'] alcançam a mesma audiência e somam pressão semanal de 9.5 (teto: 6.0).
AVISO L011 (com winback_inativos) Jornadas ativas concorrendo pela mesma audiência
5 erro(s) · 12 aviso(s) · 0 info
exit code 1CI barraria o merge

O exit code é 1 quando há erro, o que permite rodar em CI e barrar merge. A flag --explain não muda o diagnóstico: ela opera sobre o resultado já fechado e escreve a prosa em stderr, separada do stdout que a automação lê.

Conhecimento tácito de operação não escala em revisão manual. Escala quando vira regra executável, com identificador, severidade e exit code.

03

O defeito que nenhum arquivo contém

As duas regras de portfólio leem o que as revisões individuais não conseguem ver. Percorra as quatro etapas para acompanhar de onde sai o exit code 1.

etapa 1/4
L011 e L012 Duas jornadas passam em revisão isolada, alcançam a mesma audiência, somam pressão semanal de 9.5 contra um teto de 6.0 e o linter devolve exit code 1. winback_inativos revisão isolada: passa checkout_abandonado revisão isolada: passa L011 · escopo portfólio mesma audiência · 1,0 L012 · escopo portfólio pressão semanal 9,5 · teto 6,0 exit code 1

Cada arquivo passa na revisão individual. Nada aqui está errado quando se lê uma régua por vez.

04

Pressão semanal

Cada jornada isolada cabe no teto de jornada. Somadas sobre o mesmo contato, elas estouram o teto de portfólio. São dois limites diferentes, e só o segundo depende de ler o conjunto.

teto de portfólio · 6,0
teto de jornada · 4,0
5,5
4,0
9,5
checkout_
abandonado
winback_
inativos
agregada
(L012)

escala 0 a 10 · pesos por canal: e-mail 1,0 · push 1,5 · SMS e WhatsApp 2,0
weekly_pressure = carga × 7 / max(duração_dias, 7)

Contagem simples de mensagens não serve, porque push, SMS e e-mail têm custo de atenção diferente. Cada canal recebeu um peso que reflete intrusividade percebida, e a pressão da jornada é a carga ponderada em janela de sete dias, com piso de sete dias no denominador. Sem o piso, um fluxo de checkout com três toques em 24 horas seria lido como 21 toques por semana, número que não ocorre porque a jornada termina antes. Falso positivo é a causa mais comum de abandono de linter, e calibrar essa fórmula foi o que mais consumiu tempo no projeto.

.lifecycle-lint.yaml
# limites são decisão de operação, versionados no repositório
max_weekly_pressure: 6.0
max_journey_pressure: 4.0
audience_overlap_threshold: 0.6
holdout_required_above: 5000
disabled_rules: []
downgrade_to_warning: [L007]

Por que os limites ficam em arquivo: teto de pressão e limiar de sobreposição são decisão de operação, não constante de código. No repositório, mudá-los é um commit revisável, e desligar uma regra deixa rastro em vez de virar acordo verbal.

05

As decisões de cálculo

"Inativos há 60 dias" e "inativos há 60 dias que abandonaram o checkout" alcançam o mesmo grupo de pessoas: o segundo conjunto está contido no primeiro. Mesmo numerador, dois denominadores, dois veredictos.

Índice de Jaccard · a escolha padrão
|A ∩ B||A ∪ B|
0,5 abaixo do limiar 0,6

Divide pela união. Como o conjunto contido é menor, a união cresce e o resultado cai abaixo do limiar. Verde no caso que originou a regra.

Coeficiente de sobreposição · o que ficou
|A ∩ B|min(|A|, |B|)
1,0 L011 dispara

Divide pela cardinalidade do menor conjunto. Sob contenção, a interseção é o próprio conjunto menor, então o coeficiente é 1,0 e a regra passa a enxergar o conflito.

06

As doze regras

Codificam prática de operação de CRM, não convenção de engenharia de software. O escopo é a coluna que importa: dez leem uma jornada por vez, e duas leem o portfólio inteiro.

ID Escopo Regra
L001jornadaJornada ativa sem critério de saída
L002jornadaBranch sem caminho padrão
L003jornadaLoop sem limite de iterações
L004jornadaAudiência sem lista de supressão
L005jornadaSegmento sem decaimento temporal
L006jornadaPressão de mensagem alta dentro da jornada
L007jornadaCanal intrusivo sem janela de silêncio
L008jornadaJornada ativa sem grupo de controle
L009jornadaMétrica de sucesso ausente ou de vaidade
L010jornadaReentrada sem período de carência
L011portfólioJornadas ativas concorrendo pela mesma audiência
L012portfólioPressão agregada acima do teto por contato

Por que só as de portfólio justificam a ferramenta: as dez primeiras poderiam virar checklist de revisão. As duas últimas não, porque exigem ler todas as jornadas ativas ao mesmo tempo e fazer uma conta que nenhum revisor faz de cabeça. Cada regra tem caso positivo e caso negativo em teste, e o caso negativo existe para conter falso positivo.

07

Onde o modelo entra

A flag --explain envia os achados já fechados a um assistente de IA e imprime um resumo em prosa no stderr, para levar à reunião de revisão. Ela é opcional. O diagnóstico é integralmente determinístico: nenhuma das doze regras chama modelo, e sem chave de API o linter roda igual, com a flag avisando que a explicação está indisponível.

Um LLM no caminho crítico de uma auditoria troca um resultado reproduzível por um que varia entre execuções, e auditoria com essa propriedade não serve como critério de publicação. O critério que apliquei: onde existe regra determinável, escrevo regra. O modelo entra na síntese e na tradução do achado para a linguagem de quem decide.

08

A suíte

Três repositórios sobre a mesma tese: jornada de cliente é artefato versionável. Se ela pode ser declarada em arquivo, ela pode ser lida, auditada e revisada em pull request, com histórico e revisão por pares. Hoje ela vive dentro da interface da ferramenta de automação, onde não existe diff entre versões nem registro de por que aquele delay é de 48 horas.

A saída de um é a entrada do outro, o que torna objetivo o critério de aceite do agente: a jornada proposta precisa passar no linter.

01 · Auditar
lifecycle-lint

Lê as jornadas declaradas em YAML e acusa o defeito antes da publicação, inclusive o que só existe entre duas réguas.

Esta página
02 · Propor

Lê os eventos de produto e devolve o briefing de segmento com a jornada proposta, já validada pelo linter.

03 · Ler

Guarda os prompts de leitura versionados, com asserções verificáveis e placar que mede regressão em CI.

09

Stack

Ver o repositório