CommunityEscrita e Ediçãogithub.com

jessegall/commandments-writing-detectors

Como escrever seu próprio mandamento — uma regra local do projeto que julga todo arquivo a partir de então. Leia isto ANTES de escrever ou alterar uma regra em `.commandments/custom/`, quando o usuário pedir uma nova regra/verificação/detector, quando `commandments make` apontar para cá, ou quando você estiver prestes a recorrer a uma regex para inspecionar código.

O que é commandments-writing-detectors?

Esta skill ensina qualquer agente AI (Claude Code, Cursor, Codex, etc.) a criar e modificar regras de detecção personalizadas para o sistema Commandments, que julga automaticamente cada arquivo do projeto com base em regras declarativas em JSON. Ela explica que uma regra é um arquivo `.json` em `.commandments/custom/`, composta por um motor (ex.: "php"), um pecado (sin) com nome, descrição e regra, e uma consulta (find) que usa um seletor neutro (ex.: `call`, `function`) ou específico da linguagem (`kind:Expr_MethodCall`), combinado com etapas `where` (que mantêm o nó) e `reject` (que descartam). A documentação detalha todos os tipos de verificações disponíveis, desde `is`, `name`, `resolves`, `type`, `file`, `commentIs` até `descendant`, `child`, `body`, `inside`, e como combinar regras com `rule`, `from`, `any`, `all`, `not`. A skill suporta PHP, Python, C#, JavaScript, TypeScript e outras linguagens através de bridges, e é útil para qualquer ferramenta que integre o Commandments.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/jessegall/code-commandments/tree/HEAD/skills/writing-detectors

Installed? Explore more Escrita e Edição skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Escrevendo seus próprios mandamentos

As regras fornecidas não são o teto. Uma convenção que você repete em revisões é uma regra que você ainda não escreveu. Escreva-a uma vez, e ela julgará cada arquivo a partir de então.

Uma regra é uma pergunta que o motor responde

Uma regra própria são dados que o binário executa — um arquivo .json em .commandments/custom/, nessa pasta ou em qualquer pasta abaixo dela, para que um projeto com regras para várias partes do código as agrupe pelo que protegem (custom/core/, custom/viewer/) — e ela faz a mesma pergunta que toda regra fornecida faz. Uma regra é nomeada pelo seu ARQUIVO onde quer que esteja, então mover uma para uma pasta mantém todas as menções a ela na configuração intactas, e dois arquivos com o mesmo nome são ambos recusados em vez de um vencer silenciosamente. Ela nomeia o motor que julga, o pecado que encontra, e uma consulta: um seletor que a abre, então etapas where que mantêm um nó e etapas reject que descartam um. Cada etapa faz exatamente uma verificação; uma regra é tão boa quanto a pergunta que cada etapa faz.

{
    "engine": "php",
    "sin": {
        "name": "raw-sql",
        "description": "SQL escrito inline no local da chamada.",
        "rule": "Consultas passam pelo repositório que possui a tabela.",
        "skill": "no-raw-sql"
    },
    "find": {
        "select": "call",
        "where": [
            {"resolves": "Illuminate\\Support\\Facades\\DB", "of": "child:class"},
            {"nameIn": ["select", "statement", "unprepared"]}
        ],
        "reject": [
            {"file": "*Repository.php"}
        ]
    }
}

O seletor é um tipo neutro, que se lê igual em toda linguagem — call, construction, function, type-declaration, assignment, loop, branch, return, throw, catch, comparison, literal, member-access, null-safe, identifier, import, parameter, block, bail-out, expression-statement, self-reference — ou kind:<Kind>, um tipo de nó próprio da linguagem (kind:Expr_MethodCall) quando nenhum tipo neutro o descreve.

Uma regra é construída a partir de outras regras. Uma verificação que você já escreveu uma vez é nomeada, nunca escrita novamente: {"rule": "ActionClass"} passa quando a regra com esse nome também encontra o nó, e um find que escreve "from": "ActionClass" no lugar de "select" restringe o que aquela regra encontrou, sendo uma regra lida dentro de outra. Ambas alcançam qualquer regra do mesmo motor em .commandments/custom/, e cada parte continua sendo uma regra própria, listada e executada em seus próprios termos. Um nome que nada declara, uma regra de outro motor e uma cadeia que volta a si mesma são recusados quando as regras são lidas, antes que qualquer julgamento aconteça.

Diga either e not sem uma segunda regra. {"any": [{"is": "loop"}, {"is": "branch"}]} passa quando uma de suas etapas passa, {"all": [...]} quando todas passam, e {"not": {...}} quando sua etapa não passa — que é como um where diz o que apenas um reject poderia.

A verificação única de uma etapa — todas elas, geradas a partir da própria ferramenta:

O que é e como é chamado

VerificaçãoMantém o nó quando
{"is": "loop"}ele responde ao tipo neutro
{"kind": "Expr_StaticCall"}é o tipo próprio da linguagem, escrito como a linguagem escreve ou como o vocabulário soletra: Expr_StaticCall e static-call são uma mesma construção. As grafias são derivadas dos tipos, então a própria lista de autocompletar do editor mostra o que uma linguagem realmente emite, e uma grafia que dois tipos compartilham é a grafia de nenhum deles
{"name": "select"}seu nome — o nome de uma chamada ou acesso é o filho que o nomeia — é igual a ele
{"nameIn": ["select", "statement"]}seu nome é um deles
{"nameLike": "get*"}seu nome corresponde ao glob, * qualquer sequência de caracteres e ? um caractere
`{"nameMatches": "^(getset)[A-Z]"}`
{"nameMood": "question"}seu nome está nesse modo linguístico: bare-verb é o verbo e nada mais (holds, ships), third-person narra um verbo (entersTestMode), e question abre com uma palavra interrogativa (is, has, can, should). Ele lê o modo, nunca uma lista de palavras que uma regra escreve. É a única verificação aqui que julga um NOME, e responde apenas se o nome LÊ de uma certa maneira: o que torna isso um pecado é o modo contra o que a coisa FAZ, então combine-o com o que o membro responde ou se ele altera algo, como as regras fornecidas fazem
{"nameCase": "snake"}seu nome, sem o $ ou _ inicial, está escrito em camel, pascal, snake, upper ou kebab case
{"hasModifier": "static"}ele carrega o modificador
{"hasFlag": "byRef"}ele carrega a flag que a árvore da sua linguagem define
{"operator": "*"}seu operador é exatamente ele: * e ?? distinguem duas expressões binárias, += uma atribuição, ! uma unária, extends uma cláusula de herança
{"operatorIn": ["*", "/"]}seu operador é um deles

O que ele diz

VerificaçãoMantém o nó quando
{"text": "select * from users"}o texto de um literal é exatamente ele
{"textLike": "*where id*"}o texto de um literal corresponde ao glob
{"textMatches": "(?i)^select"}o texto de um literal corresponde à expressão regular
{"commentIs": "history"}um comentário é uma das leituras calibradas de prosa: history narra por que o código foi ALTERADO em vez do que ele faz, strawman defende contra um erro que ninguém cometeria, e restates não diz nada que o código que documenta não diga. As leituras são do próprio pacote de prosa, medidas por seus testes em cada linguagem, e o conjunto é fechado: uma regra pede um julgamento que foi calibrado, nunca uma prosa que corresponda a palavras que ela mesma escreve. Perguntado de um comentário ("select": "comment") ele julga aquele comentário, e perguntado de código ele julga os comentários escritos para ele
{"docReference": {"resolves": false}}um comentário de documentação nomeia um código que FOI REMOVIDO: uma referência que faz (<see cref>, {@see}, @see, Sphinx :class:) não resolve para nada, e o projeto teria que ser o dono dela. Duas referências são deixadas de lado, que é toda a diferença entre isso e um scanner de links quebrados: uma que o compilador nunca poderia ter resolvido de qualquer maneira, e outra cujo dono é declarado fora da varredura, já que nada aqui pode dizer se aquele dono ainda possui o membro. A resolução é da própria ponte
{"commentLike": "*TODO*"}um comentário sobre ele, ou na sequência de comentários diretamente acima dele, corresponde ao glob — e, perguntado de um comentário em si ("select": "comment"), o que AQUELE comentário diz, para que uma chave leia um comentário independentemente de como uma regra o alcançou
`{"commentMatches": "(?i)\b(TODOFIXME)\b"}`
{"docTag": "deprecated"}sua documentação carrega a tag: @tag em PHPDoc e JSDoc, um elemento XML em C#, um campo ou diretiva Sphinx, ou uma seção Google/NumPy em uma docstring Python
{"documented": false}ele carrega um comentário de documentação, ou não com false

Ao que ele se refere

VerificaçãoMantém o nó quando
{"resolves": "App\\Models\\User"}ao que seu nome se refere, resolvido, é aquilo — ou, para um valor que não nomeia nada (uma variável, uma propriedade), a classe que ele contém: exatamente essa classe, não uma que a estende
{"resolvesLike": "App\\Models\\*"}ao que seu nome se refere, resolvido, corresponde ao glob, ou a classe que um valor contém: {"resolvesLike": "*Service", "of": "child:var"} é um receptor de qualquer *Service
{"declaredBy": "App\\Models\\*"}o membro que ele lê é declarado por uma classe que o glob nomeia: a classe que seu receptor contém, seguida por seus pais até que um declare aquele membro. É de quem este campo é, o que nenhum nome pode dizer, e está vazio — então a etapa não mantém nada — onde o receptor não tem tipo na varredura ou nenhuma declaração dele declara o membro
{"answeredBy": false}o tipo que declara o que ele lê já nomeia a pergunta que faz — um método desse tipo cujo corpo inteiro retorna a mesma coisa, lido de qualquer maneira — ou NÃO com false. É o que distingue use o nome que existe de dê um nome a ele, e uma pergunta feita exatamente pelo método que a nomeia nunca é um dos lugares que a fazem
{"sameAs": {"of": "closest:loop", "target": "child:iter"}}ele nomeia a mesma coisa que outro nó do mesmo arquivo, alcançado seguindo of a partir dele e depois target a partir dali — a única pergunta que uma regra não poderia fazer: um remove na mesma coleção que seu loop percorre, um argumento que é o campo do qual acabou de ser lido. Os dois devem ser escritos da mesma forma, senão lidos como a mesma árvore; nenhum nó no final do caminho não é aprovação
{"declaredElsewhere": true}o que ele nomeia é declarado em OUTRO ARQUIVO diferente daquele em que está, ou não é com false. Um campo definido de fora do arquivo que o declara contornou os métodos que sua classe escreveu para se modificar, que é a pergunta que uma regra sobre o próprio negócio de um tipo faz; não mantém nada onde nenhuma declaração na varredura declara o que ele nomeia, então uma regra também diz o dono
{"type": "number"}o tipo que a expressão tem, como sua linguagem o lê: o que um verificador lhe deu, o que sua forma diz, ou o tipo que sua declaração escreve — um primitivo entre eles, onde resolves responde apenas a uma classe
{"typeLike": "Money*"}o tipo que a expressão tem corresponde ao glob
{"calls": {"name": "dd"}}uma chamada própria — não uma dentro de uma função aninhada nela — passa na etapa
{"argument": {"at": 0, "is": "literal"}}o argumento naquela posição passa na etapa; um at negativo conta a partir do final, -1 o último, e nenhum lá não passa nada. Sem at nenhum, QUALQUER argumento que passe na etapa é suficiente, que é como uma regra pergunta sobre um passado por nome, cujo lugar não o identifica
{"constructs": "Date*"}uma construção, ou em qualquer outra coisa uma própria, cria uma instância de um tipo que o glob nomeia — em Python, uma chamada de uma classe
{"unused": true}nada se refere a ele: uma função ou tipo de fora de si mesmo, através dos nomes e chamadas que a varredura resolve e o grafo de chamadas de sua linguagem; um parâmetro, pelo seu nome lido em sua função; um campo ou propriedade, pelo que alcança sua declaração através do tipo que seu receptor contém, já que um campo não se refere a nenhum símbolo na maioria das linguagens. Um membro que a própria linguagem alcança pelo nome, e um que sobrescreve, não são isso: nada que o projeto escreve precisa nomeá-los
{"referredFrom": {"is": "parameter"}}algo que se refere a ele — uma chamada, um nome, um tipo escrito — passa na etapa. É calledFrom perguntando sobre a REFERÊNCIA em vez do arquivo em que está, que é o que distingue um lado de uma coisa do outro: uma classe de evento nomeada por um despacho e por nenhum parâmetro de ouvinte não é ouvida por ninguém, e {"not": {"referredFrom": ...}} diz exatamente isso
{"calledFrom": "app/Http/**"}algo que se refere a ele — uma chamada, na maioria das vezes — está em um arquivo que o glob corresponde, lido como file lê um

Onde está

VerificaçãoMantém o nó quando
{"file": "*Repository.php"}seu caminho a partir do projeto (o repositório em que está, caso contrário a pasta julgada), ou qualquer parte final dele, corresponde ao glob: * permanece dentro de uma pasta e ** as cruza, como no .gitignore
`{"fileMatches": "(^/)pages/.*(Create
{"namespaceLike": "App\\Http\\*"}o namespace, pacote ou módulo em que é declarado corresponde ao glob
{"layer": "App\\Domain"}está naquela camada da pilha que o projeto declara (backend, Python, C#)
{"topLevel": true}está fora de toda função, closures também, e de todo tipo — código que executa quando seu arquivo é carregado
{"withinLoop": true}está dentro de um loop, ou não com false
{"position": "first"}é o first, last ou only entre seus irmãos (um único também é primeiro e último)
{"field": "cond"}preenche aquele slot de seu pai na árvore — o nome próprio da linguagem para o slot, como cond, body, else, args — que é o único eixo de lugar que as outras verificações nunca perguntam: uma chamada servindo COMO condição em vez de uma passada como argumento. Os nomes são os de sua linguagem, então uma regra escrita com um julga aquela linguagem
{"role": "member"}é aquele tipo de coisa, nas palavras que toda ponte escreve: statement, expression, member (qualquer declaração de um tipo, uma função, uma propriedade ou um caso de enum), type (um tipo como escrito), pattern, markup ou other. É como uma regra diz uma declaração sem nomear um tipo por linguagem, que é o que uma regra sobre a ordem ou a companhia de membros precisa
{"descendant": {"is": "return"}}algum nó dentro dele passa na etapa; com "within": "ownBody" apenas os nós que ele mesmo CONTÉM são percorridos, parando em uma função aninhada dentro dele, que é a diferença entre um método que salva e um que entrega um closure contendo a ação para uma fila
{"child": {"is": "return"}}algum nó um nível dentro dele passa na etapa, o que descendant em qualquer profundidade não pode dizer: uma função cujo próprio corpo retorna, e nada para uma que retorna apenas dentro de um if. Com "field" apenas os filhos que preenchem aquele campo da árvore são percorridos, assim como o campo de uma contagem restringe o que ela conta
{"body": {"is": "branch"}}alguma instrução do seu CORPO passa na etapa, lida através do que sua linguagem envolve em um corpo: C# escreve uma instrução de bloco e Python escreve instruções simples, então child as responde de forma diferente e isso as responde de forma igual. É como uma regra sobre um loop ou o corpo de uma função é escrita uma vez para cada linguagem
{"inside": {"is": "catch"}}algum nó acima dele, até a raiz do arquivo, passa na etapa
`{"all": [{"name": "store"}, {"hasModifier": "pu

…

[truncado para tradução]

Habilidades Relacionadas