API de Regras
As regras do Archgate são arquivos TypeScript que exportam um objeto simples tipado com satisfies RuleSet. Cada regra recebe um RuleContext com utilitários para buscar arquivos, ler conteúdo e reportar violações.
RuleSet
Seção intitulada “RuleSet”/// <reference path="../rules.d.ts" />
export default { rules: { "my-rule-id": { description: "Human-readable description of what this rule checks", severity: "error", // optional, defaults to "error" async check(ctx) { // Rule logic here }, }, },} satisfies RuleSet;Um arquivo de regras exporta por padrão um objeto simples com um registro rules indexado por ID de regra. As chaves se tornam os IDs das regras que aparecem na saída de verificação e nos relatórios de violação. A anotação satisfies RuleSet fornece verificação de tipos sem envolver em uma chamada de função.
type RuleSet = { rules: Record<string, RuleConfig> };RuleConfig
Seção intitulada “RuleConfig”Cada regra no registro deve estar em conformidade com a interface RuleConfig.
interface RuleConfig { description: string; severity?: Severity; check: (ctx: RuleContext) => Promise<void>;}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
description | string | Sim | Descrição legível exibida na saída de verificação |
severity | Severity | Não | Severidade padrão para violações. Padrão: "error" |
check | (ctx: RuleContext) => Promise<void> | Sim | Função assíncrona contendo a lógica da regra |
RuleContext
Seção intitulada “RuleContext”A função check recebe um objeto RuleContext com o estado do projeto e métodos utilitários.
interface RuleContext { projectRoot: string; scopedFiles: string[]; changedFiles: string[]; glob(pattern: string): Promise<string[]>; grep(file: string, pattern: RegExp): Promise<GrepMatch[]>; grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>; readFile(path: string): Promise<string>; fileAtBase(path: string): Promise<string | null>; readJSON(path: string): Promise<unknown>; readYAML(path: string): Promise<ReadYamlResult>; ast(path: string, language: AstLanguage, opts?: AstOptions): Promise<AstNode>; findAstNodes(tree: AstNode, ...types: string[]): AstNode[]; checkCase(value: string, scheme: CaseScheme): boolean; report: RuleReport;}Propriedades
Seção intitulada “Propriedades”projectRoot
Seção intitulada “projectRoot”projectRoot: string;Caminho absoluto para o diretório raiz do projeto (onde .archgate/ está localizado).
scopedFiles
Seção intitulada “scopedFiles”scopedFiles: string[];Arquivos que correspondem aos padrões glob de files do frontmatter do ADR. Se o ADR não tiver o campo files, contém todos os arquivos do projeto. Use esta lista como a lista principal de arquivos para suas verificações de regra.
changedFiles
Seção intitulada “changedFiles”changedFiles: string[];Arquivos modificados conforme o Git. Por padrão, é autopreenchido com o diff da branch em relação à branch base detectada (ex.: origin/main) mais quaisquer alterações não commitadas da árvore de trabalho (arquivos staged, não staged e não rastreados que não sejam ignorados). Quando --staged é usado, contém apenas arquivos staged. Quando --base <ref> é usado, contém todos os arquivos alterados desde essa ref mais as alterações não commitadas da árvore de trabalho. Fica vazio quando a detecção falha ou nenhuma alteração é encontrada. Use para construir regras de dependência entre arquivos (ex.: “se o arquivo A mudou, o arquivo B também deve mudar”).
report: RuleReport;A interface de relatório para registrar violações, avisos e mensagens informativas. Veja RuleReport abaixo.
Métodos
Seção intitulada “Métodos”glob(pattern: string): Promise<string[]>;Encontra arquivos correspondentes a um padrão glob relativo à raiz do projeto. Retorna um array de caminhos de arquivo. Arquivos ignorados pelo .gitignore são excluídos por padrão. Defina respectGitignore: false no frontmatter do ADR para incluí-los.
const testFiles = await ctx.glob("tests/**/*.test.ts");grep(file: string, pattern: RegExp): Promise<GrepMatch[]>;Busca em um único arquivo por linhas correspondentes a uma expressão regular. Retorna um array de objetos GrepMatch com caminho do arquivo, número da linha, coluna e conteúdo correspondente.
const matches = await ctx.grep(file, /console\.error\(/);grepFiles
Seção intitulada “grepFiles”grepFiles(pattern: RegExp, fileGlob: string): Promise<GrepMatch[]>;Busca em múltiplos arquivos correspondentes a um padrão glob por linhas que correspondam a uma expressão regular. Combina glob e grep em uma única chamada. Arquivos ignorados pelo .gitignore são excluídos por padrão. Defina respectGitignore: false no frontmatter do ADR para incluí-los.
const matches = await ctx.grepFiles(/TODO:/i, "src/**/*.ts");readFile
Seção intitulada “readFile”readFile(path: string): Promise<string>;Lê o conteúdo de um arquivo como string. O caminho é relativo à raiz do projeto.
const content = await ctx.readFile("src/config.ts");fileAtBase
Seção intitulada “fileAtBase”fileAtBase(path: string): Promise<string | null>;Lê o código-fonte de um arquivo na revisão base de comparação — a base de merge entre a ref --base e HEAD, o mesmo commit contra o qual changedFiles é calculado. Use para comparar a árvore de trabalho com o ponto em que o conjunto de alterações divergiu da base.
Retorna null nos dois casos de “nada para comparar”, então uma única verificação de null cobre ambos:
- Nenhuma base é resolvida — a verificação rodou sem
--base(ou os históricos não têm relação). - O arquivo não existia na base — um arquivo adicionado.
const before = await ctx.fileAtBase("data/schema.py");if (before === null) { // Nenhuma versão base para comparar -- pular. return;}const after = await ctx.readFile("data/schema.py");Para uma comparação estrutural (ignorando comentários e formatação) prefira ast(path, language, { rev: "base" }) abaixo.
readJSON
Seção intitulada “readJSON”readJSON(path: string): Promise<unknown>;Lê e faz o parse de um arquivo JSON. O caminho é relativo à raiz do projeto. Retorna o valor parseado como unknown — faça o cast para o tipo esperado na sua regra.
const pkg = (await ctx.readJSON("package.json")) as { dependencies?: Record<string, string>;};readYAML
Seção intitulada “readYAML”readYAML(path: string): Promise<ReadYamlResult>;
interface ReadYamlResult { frontmatter: Record<string, YamlValue> | null; content: YamlValue;}
type YamlValue = | string | number | boolean | null | YamlValue[] | { [key: string]: YamlValue };Lê um arquivo YAML ou um arquivo Markdown com frontmatter YAML. O caminho é relativo à raiz do projeto e passa pelo mesmo sandbox de readFile. Um único objeto de resultado cobre as duas formas — um mapeamento frontmatter anulável e um content do tipo YamlValue, os dados JSON-like que o esquema central do YAML produz (após uma verificação de typeof você pode indexar mapeamentos e sequências sem cast) — e a escolha é baseada na extensão do arquivo:
- Arquivos
.yml/.yaml: o documento inteiro é parseado como YAML.frontmatteré semprenull;contenté o valor parseado, restringido por uma verificação detypeofem vez de um cast — diferente dereadJSON, que retornaunknown. Como a escolha é por extensão, os separadores---de um stream multi-documento (manifestos Kubernetes, configurações de CI) nunca são interpretados erroneamente como frontmatter. - Qualquer outro arquivo (tipicamente Markdown): o bloco inicial delimitado por
---é parseado comofrontmatter—nullquando ausente (“este arquivo tem frontmatter?” é um único teste de null, espelhandofileAtBase),{}quando existe mas está vazio.contenté o restante do corpo do texto, sem espaços nas extremidades — ele não é parseado como YAML.
readYAML() lança uma exceção (fail-closed, como ast()) quando um arquivo .yml/.yaml é YAML inválido, ou quando um bloco de frontmatter é YAML inválido ou o parse resulta em algo diferente de um mapeamento (um escalar ou uma sequência) — aparecendo como um erro de execução de regra com código de saída 2 em vez de uma aprovação falsa.
const { content } = await ctx.readYAML(".github/workflows/ci.yml");if ( typeof content === "object" && content !== null && !Array.isArray(content)) { const jobs = content.jobs; // YamlValue -- no cast needed}for (const file of await ctx.glob("docs/**/*.md")) { const { frontmatter } = await ctx.readYAML(file); if (frontmatter === null) { ctx.report.violation({ message: `${file} is missing frontmatter`, file }); continue; } if (typeof frontmatter.title !== "string") { ctx.report.violation({ message: `${file} frontmatter must declare a title`, file, }); }}ast( path: string, language: "typescript" | "javascript" | "python" | "ruby", opts?: AstOptions): Promise<AstNode>;
interface AstOptions { rev?: "base"; comments?: boolean;}Faz o parse de um arquivo-fonte para sua AST nativa da linguagem. O caminho é relativo à raiz do projeto e passa pelo mesmo sandbox de readFile. TypeScript e JavaScript são parseados in-process; Python e Ruby são parseados invocando como subprocesso o recurso de AST da biblioteca padrão do próprio interpretador do sistema. O formato da árvore retornada difere por linguagem — veja AstNode.
Os resultados de parse são cacheados durante uma única execução de archgate check, com chave (path, language, rev, comments): chamadas idênticas repetidas — até mesmo concorrentes — entre regras custam um único parse (uma única inicialização do interpretador para Python/Ruby), e um parse que falhou relança o mesmo erro para todos os chamadores. Trate a árvore retornada como somente leitura — ela pode ser compartilhada com outras regras.
const program = await ctx.ast("src/cli.ts", "typescript");for (const node of program.body) { console.log(node.type);}Parseando a revisão base ({ rev: "base" })
Seção intitulada “Parseando a revisão base ({ rev: "base" })”Com { rev: "base" }, ast() faz o parse do arquivo na revisão base de comparação em vez da árvore de trabalho — tudo o mais (formato de retorno, contrato de exceção) é idêntico. Faça o parse das duas revisões para perguntar “a estrutura executável mudou?” Comentários estão ausentes dos formatos ESTree e ast do Python, mas as posições dos nós não estão: os campos loc / lineno se deslocam quando um comentário ou linha em branco move as linhas abaixo dele. Compare uma projeção sem posições — remova loc / range (ESTree) e lineno / col_offset (Python) antes de comparar — para que uma edição apenas de comentários seja lida como inalterada. (Docstrings do Python são nós de string na árvore, então uma docstring editada é uma mudança real; remova-as também se edições de documentação forem neutras.)
// Sinaliza uma mudança apenas quando a estrutura executável de fato mudou.// `structurallyEqual` compara as árvores com os metadados de posição (loc/range,// lineno/col_offset) removidos -- veja o guia writing-rules para uma// implementação concreta.for (const file of ctx.changedFiles.filter((f) => f.endsWith(".py"))) { // Pula arquivos sem contraparte na base -- ast({ rev: "base" }) lançaria. if ((await ctx.fileAtBase(file)) === null) continue; const before = await ctx.ast(file, "python", { rev: "base" }); const after = await ctx.ast(file, "python"); if (!structurallyEqual(before, after)) { ctx.report.violation({ message: `${file} changed behavior`, file }); }}Recorra a fileAtBase() primeiro quando você precisar detectar os casos de sem-base ou arquivo-adicionado como fluxo de controle comum — ast({ rev: "base" }) lança uma exceção para eles (veja abaixo).
Coletando comentários ({ comments: true })
Seção intitulada “Coletando comentários ({ comments: true })”Com { comments: true }, a árvore retornada carrega um array comments — dados estruturados de comentário para regras de governança de comentários, no lugar de regex linha a linha. Suportado para as quatro linguagens. Para ruby, a árvore retornada é um array (saída de Ripper.sexp), então o array comments fica anexado a ela como uma propriedade não indexada.
interface CommentToken { type: "line" | "block"; value: string; // delimitadores (`//`, `/* */`, `#`) removidos loc: { start: { line: number; column: number }; end: { line: number; column: number }; };}const tree = await ctx.ast("src/api.ts", "typescript", { comments: true });for (const comment of tree.comments ?? []) { const lines = comment.value.split("\n").length; if (lines > 10) { ctx.report.warning({ message: "Comment block is too long -- link to an ADR instead", file: "src/api.ts", line: comment.loc.start.line, }); }}As posições dos comentários são precisas em relação ao código-fonte original, mesmo para TypeScript. Essa é uma vantagem deliberada em relação ao loc da própria árvore, que é relativo ao transpilado para TypeScript (veja AstNode): os comentários são varridos a partir do código-fonte antes da transpilação, então seu loc nunca diverge. Comentários em Python são sempre type: "line" (#) — Python não tem comentários de bloco, e docstrings """ são expressões de string na árvore, não comentários. Comentários # em Ruby são type: "line"; cada bloco de documentação =begin/=end é um único token type: "block" cujo value é o conteúdo interno (as linhas marcadoras =begin/=end são removidas, de forma análoga à remoção dos delimitadores /* */) e cujo loc vai da linha do =begin até a linha do =end. As colunas de loc dos comentários Ruby são deslocamentos de caracteres, consistentes com as demais linguagens (as posições de nó do próprio sexp do Ripper são deslocamentos de bytes), e as quebras de linha do value de blocos são normalizadas para LF independentemente das quebras de linha do arquivo-fonte. Para Python e Ruby, a extração de comentários é uma segunda passada sobre o mesmo código-fonte (tokenize / Ripper.lex); uma falha do tokenizador em código que parseia normalmente degrada para uma lista de comentários vazia em vez de falhar o parse. O scanner de TypeScript/JavaScript reconhece literais de string e de template, mas não rastreia literais de expressão regular, então um delimitador de comentário dentro de um literal regex é um ponto cego conhecido.
Comportamento de falha
Seção intitulada “Comportamento de falha”ast() lança uma exceção em caso de falha — nunca retorna null:
- Falha de parse: o arquivo não parseia como a linguagem solicitada. A mensagem de erro inclui o diagnóstico do parser.
- Interpretador ausente (apenas
python/ruby): nenhum interpretador adequado foi encontrado noPATH. - Entrada implausível: a extensão do arquivo não corresponde à linguagem solicitada (ex.:
ctx.ast("config.json", "python")lança a exceção antes de qualquer interpretador ser invocado). - Sem revisão base (apenas
{ rev: "base" }): a verificação rodou sem--base, então não há base para parsear. UsefileAtBase()para detectar isso comonullem vez disso. - Arquivo ausente na base (apenas
{ rev: "base" }): o caminho foi adicionado desde a base e não existia ali.
Um erro lançado é isolado à regra que falhou: as demais regras e ADRs da mesma execução de verificação continuam normalmente, e a falha aparece como um erro de execução de regra com código de saída 2 (distinto do código de saída 1 para violações). Os casos de exceção são distinguíveis pelo texto da mensagem, então a saída da verificação diferencia “este ambiente não consegue executar esta regra” de “este arquivo tem um erro de sintaxe” ou “não há revisão base”.
findAstNodes
Seção intitulada “findAstNodes”findAstNodes(tree: AstNode, ...types: string[]): AstNode[];Coleta recursivamente cada nó de uma AST parseada cujo campo discriminante de tipo corresponde a um dos types. É a substituição embutida para o caminhador recursivo que as regras de AST precisavam escrever à mão: ctx.ast() retorna deliberadamente formatos nativos da linguagem, mas encontrar nós por nome de tipo depende apenas do campo discriminante, então um único helper cobre todas as linguagens. Síncrono — não precisa de await.
- Agnóstico de linguagem: cada nó-objeto é verificado contra o campo discriminante que ele carrega —
_type(Python) outype(ESTree TypeScript/JavaScript). - Travessia completa: valores de objeto enumeráveis próprios e arrays são percorridos recursivamente, e o próprio argumento
treeé um candidato a correspondência. - Correspondência de múltiplos tipos: passe vários nomes quando uma construção abrange múltiplos tipos de nó — o caso comum (
"FunctionDef"/"AsyncFunctionDef", variantes síncrona/assíncrona). - Ruby: nós de
Ripper.sexpsão arrays puros sem campo discriminante de objeto, então uma árvore Ruby é percorrida, mas seus nós em formato de array nunca correspondem — percorra a saída do Ripper contra a gramática dele.
Antes — o coletor que cada arquivo de regras precisava repetir (arquivos de regras não podem importar módulos helper compartilhados):
function collectFunctionDefs( node: unknown, out: PythonAstNode[] = []): PythonAstNode[] { if (Array.isArray(node)) { for (const item of node) collectFunctionDefs(item, out); return out; } if (!node || typeof node !== "object") return out; const n = node as PythonAstNode; if (n._type === "FunctionDef" || n._type === "AsyncFunctionDef") out.push(n); for (const value of Object.values(n)) { if (value && typeof value === "object") collectFunctionDefs(value, out); } return out;}
const tree = await ctx.ast("app/models.py", "python");const funcDefs = collectFunctionDefs(tree);Depois:
const tree = await ctx.ast("app/models.py", "python");const funcDefs = ctx.findAstNodes(tree, "FunctionDef", "AsyncFunctionDef");checkCase
Seção intitulada “checkCase”checkCase(value: string, scheme: CaseScheme): boolean;
type CaseScheme = | "kebab-case" | "camelCase" | "PascalCase" | "snake_case" | "SCREAMING_SNAKE_CASE";Verifica se uma string segue um esquema de capitalização — a substituição integrada para os regexes por esquema que regras de nomenclatura precisavam escrever à mão. Síncrono e puro — não precisa de await. A correspondência é tudo-ou-nada (a string inteira deve estar em conformidade; a string vazia não corresponde a nenhum esquema) e o vocabulário é apenas letras e dígitos ASCII.
| Esquema | Aceita | Rejeita |
|---|---|---|
kebab-case | writing-rules, 2fa-setup | Writing-Rules, writing_rules, writing--rules |
camelCase | checkCase, parseURL | CheckCase, check_case, 2fast |
PascalCase | CheckCase, HTTPServer | checkCase, Check_Case, 1Value |
snake_case | check_case, 2fa_setup | Check_Case, check-case, check__case |
SCREAMING_SNAKE_CASE | CHECK_CASE, V2 | check_case, CHECK-CASE, CHECK__CASE |
camelCase e PascalCase seguem a convenção do ecossistema (a naming-convention do typescript-eslint): uma letra inicial minúscula/maiúscula seguida de quaisquer caracteres alfanuméricos ASCII, então sequências de acrônimos (parseURL, HTTPServer) correspondem. Valores degenerados podem satisfazer vários esquemas ao mesmo tempo (value é kebab-case, snake_case e camelCase válidos). Passar um nome de esquema não reconhecido lança uma exceção em vez de retornar false silenciosamente, para que um erro de digitação apareça como um erro de regra em vez de um resultado falso.
for (const file of await ctx.glob("docs/**/*.md")) { const stem = file.split("/").pop()?.replace(/\.md$/, "") ?? ""; if (!ctx.checkCase(stem, "kebab-case")) { ctx.report.violation({ message: `${file} must have a kebab-case filename`, file, }); }}RuleReport
Seção intitulada “RuleReport”A interface de relatório para registrar resultados de verificação. Cada método aceita um objeto de detalhe descrevendo o problema.
interface RuleReport { violation(detail: ReportDetail): void; warning(detail: ReportDetail): void; info(detail: ReportDetail): void;}violation
Seção intitulada “violation”report.violation(detail: ReportDetail): void;Reporta uma violação de regra. Violações fazem a verificação falhar com código de saída 1. Use para restrições rígidas que não devem ser mergeadas.
warning
Seção intitulada “warning”report.warning(detail: ReportDetail): void;Reporta um aviso. Avisos aparecem na saída de verificação mas não fazem a verificação falhar. Use para orientações não bloqueantes.
report.info(detail: ReportDetail): void;Reporta uma mensagem informativa. Não afeta o código de saída da verificação. Use para sugestões ou notas.
ReportDetail
Seção intitulada “ReportDetail”O objeto de detalhe passado para violation, warning e info.
interface ReportDetail { message: string; file?: string; line?: number; endLine?: number; endColumn?: number; fix?: string;}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
message | string | Sim | Descrição legível do problema |
file | string | Não | Caminho do arquivo onde o problema foi encontrado |
line | number | Não | Número da linha inicial (base 1) |
endLine | number | Não | Número da linha final (base 1), para destaque preciso no editor |
endColumn | number | Não | Número da coluna final (base 0), para destaque preciso no editor |
fix | string | Não | Sugestão de correção ou ação de remediação |
Quando endLine e endColumn são fornecidos, editores (VS Code, Cursor) podem destacar a expressão exata que viola a regra, em vez da linha inteira. Se omitidos, a linha completa em line é destacada.
GrepMatch
Seção intitulada “GrepMatch”Retornado por ctx.grep() e ctx.grepFiles().
interface GrepMatch { file: string; line: number; column: number; content: string;}| Campo | Tipo | Descrição |
|---|---|---|
file | string | Caminho relativo ao projeto do arquivo correspondente |
line | number | Número da linha da correspondência (base 1) |
column | number | Número da coluna da correspondência (base 1) |
content | string | Conteúdo completo da linha correspondente |
AstNode
Seção intitulada “AstNode”Retornado por ctx.ast(). O formato é nativo da linguagem e deliberadamente não é unificado entre linguagens — cada linguagem retorna seu próprio vocabulário padrão de AST, então uma regra que inspeciona código Python trabalha com uma gramática diferente de uma que inspeciona TypeScript.
type AstLanguage = "typescript" | "javascript" | "python" | "ruby";type AstNode = Record<string, unknown> | unknown[];Quando parseado com { comments: true }, o nó raiz também carrega um array comments: CommentToken[] (as quatro linguagens; para ruby ele é uma propriedade não indexada no array sexp raiz). Ausente nos demais casos.
| Linguagem | Parser utilizado | Formato retornado |
|---|---|---|
typescript | meriyah, in-process, após remover o TypeScript por transpilação com Bun.Transpiler | Program ESTree com informações de posição em loc. Sintaxe exclusivamente de tipos (interface, type aliases, export type { ... } from) é apagada antes do parse — um arquivo contendo apenas declarações de nível de tipo parseia para um Program com body vazio. As posições em loc referem-se à saída transpilada, não ao arquivo .ts original — declarações exclusivamente de tipos removidas, comentários e linhas em branco fazem os números de linha divergirem, então relocalize a construção no código-fonte original (p. ex. ctx.readFile() mais indexOf) antes de reportar um line, ou omita line por completo; loc é fiel ao código-fonte apenas para javascript |
javascript | meriyah, in-process | Program ESTree com informações de posição em loc |
python | O módulo ast da biblioteca padrão do Python, via interpretador do sistema | Nós do ast serializados em JSON: { "_type": "Module", "body": [...] } — cada nó carrega _type, os campos do próprio nó e as posições lineno / col_offset |
ruby | O Ripper da biblioteca padrão do Ruby, via interpretador do sistema | Arrays aninhados de Ripper.sexp: ["program", [["command", ...]]] com pares de posição [linha, coluna] embutidos nas entradas de token |
Veja Verificações estruturais com ctx.ast() para um exemplo completo de regra por linguagem.
Severity
Seção intitulada “Severity”type Severity = "error" | "warning" | "info";| Valor | Impacto no código de saída | Descrição |
|---|---|---|
"error" | Causa saída 1 | Restrição rígida, bloqueia merges |
"warning" | Sem impacto | Orientação não bloqueante |
"info" | Sem impacto | Informativo, apenas sugestões |
ViolationDetail
Seção intitulada “ViolationDetail”A representação interna de um problema reportado, usada na saída de verificação e nos resultados JSON.
interface ViolationDetail { ruleId: string; adrId: string; message: string; file?: string; line?: number; endLine?: number; endColumn?: number; fix?: string; severity: Severity;}| Campo | Tipo | Descrição |
|---|---|---|
ruleId | string | ID da regra da chave do objeto rules |
adrId | string | ID do ADR do frontmatter |
message | string | Descrição legível |
file | string? | Caminho do arquivo onde o problema foi encontrado |
line | number? | Número da linha inicial (base 1) |
endLine | number? | Linha final (base 1), para destaque preciso no editor |
endColumn | number? | Coluna final (base 0), para destaque preciso no editor |
fix | string? | Sugestão de correção |
severity | Severity | Severidade efetiva desta violação |
Supressão inline
Seção intitulada “Supressão inline”Violações podem ser suprimidas no código-fonte usando comentários archgate-ignore. O engine lida com isso automaticamente. As regras não precisam de nenhuma lógica especial.
// archgate-ignore ARCH-006/no-unapproved-deps dep legada, migração planejadaimport chalk from "chalk";Um motivo é obrigatório. A supressão no nível do arquivo usa archgate-ignore-file. Empilhe múltiplos comentários para suprimir mais de uma regra na mesma linha. Veja Diretivas de opt-out para detalhes completos e padrões de diretivas customizadas.