Kanon expressions v0.4
kanon specs/kanon/expressions.kmd
Kanon é a linguagem de expressões de regra do governance plane da Koder Stack (stack-RFC-015): nomeada, versionada, NÃO-Turing-completa, com EBNF e corpus de conformidade próprios. v0.1 cobre `applies_when` (predicados de aplicabilidade); v0.2 quantificadores estruturais (forall/exists + `has`); v0.3 composição (`refines:` + reticulado RFC-2119); v0.4 binding cláusula→oráculo (`kanon_bindings:` — verify_by/gate/fp_budget/autofix). Transporte oficial: frontmatter de `.kmd`. O avaliador canônico é a lib compartilhada `engines/lang/kanon` (extraída de koder-tools/internal 2026-06-27 — RFC-020 §5 / reuse-first); o koder-spec-audit a consome (RFC-015 R4).
When this spec applies
All triggers
- Escrever ou alterar uma expressão Kanon (applies_when) numa spec/policy
- Estender a gramática do Kanon (exige bump de versão + revisão da RFC-015)
- Implementar/alterar o avaliador do Kanon no koder-spec-audit
Specification body
Spec — Kanon expressions v0.5
Document format reference: meta/docs/stack/policies/document-format.kmd
Nome ratificado pelo owner em 2026-06-11 (
/k-gosobre a deliberação/k-arch; registrystack-manifest.kmd). Kanon (gr. kanōn, régua/regra) é carga, não portador: vive dentro do.kmd(frontmatter hoje; fenced blocks ```kanon em versões futuras) e é avaliada exclusivamente pelo motor dokoder-spec-audit. Extração para arquivo próprio é movimento de transporte, nunca de semântica (seam D9 — RFC-015 §2).
1. Princípios normativos (herdados da RFC-015)
- R1 — Total e decidível. Sem recursão de usuário, sem iteração ilimitada.
Toda expressão termina; profundidade de aninhamento de parênteses ≤ 32
(erro
kanon-depth-exceededacima disso). - R2 — A válvula de escape é Go. O que não couber nesta gramática vira checker Go nomeado. Estender a gramática = bump de versão + revisão da RFC-015. Nunca "só mais um operador" sem processo.
- R3 — Versionada. Todo documento portando Kanon declara
kanon: "<maj.min>"no frontmatter. Avaliador rejeita major desconhecido ou minor maior que o suportado com erro nomeadokanon-unsupported-version— nunca silêncio. - R5 — O corpus é o contrato. §5 é executável: o avaliador DEVE passar
todos os casos (espelhados em
internal/kanon/kanon_test.go). Feature nova entra com casos positivos E negativos aqui antes do motor.
2. Construtos da v0.1
v0.1 — applies_when: predicado de aplicabilidade de uma spec/policy a
uma ação/alvo. Chave de frontmatter:
kanon: "0.1"
applies_when: path ~ "**/landing/**" and kind == "product"
v0.2 — regra estrutural (quantificador): obrigação sobre o grafo de
módulos, avaliada por koder-spec-audit structural:
forall module where domain == "products": has "README.md" or has "README.kmd"
exists module: path ~ "**/kodix"
v0.3 — composição (refines:): documento declara no frontmatter o pai
que refina; o avaliador (koder-spec-audit refines) PROVA o invariante
policies-RFC-003 sobre as cláusulas Requirement (RFC-013): só TIGHTEN/ADD,
nunca LOOSEN. Reticulado de obrigação (semântica da linguagem): MAY(1) <
SHOULD(2) < SHALL(3); SHALL NOT tem força de SHALL. Ausência de cláusula
no filho = herança (nunca violação); cláusula sem keyword RFC-2119 não compara
(malformação é território do kmd lint). Grafo validado: pai inexistente
(kanon-refines-missing), ciclo (kanon-refines-cycle, cadeia ≤32), e id de
cláusula duplicado entre docs NÃO-relacionados (kanon-duplicate-clause,
warning de contradição).
kanon: "0.3"
refines: meta/docs/stack/policies/observability-first.kmd
v0.4 — binding cláusula→oráculo (kanon_bindings:): o vínculo
cláusula↔verificação vira DADO do documento — koder-spec-audit bindings
valida e (com --exec) executa:
kanon: "0.4"
kanon_bindings:
req-auth:
verify_by: "koder-spec-audit landing-responsive --module {path}"
gate: block # advisory | warn | block
fp_budget: "2/30d" # OBRIGATÓRIO quando gate: block (RFC-014 R3)
autofix: ""
Reticulado de gates (semântica da linguagem): advisory(1) < warn(2) <
block(3); através de refines: o gate só APERTA (kanon-gate-loosen).
Findings de validação: kanon-binding-orphan (id inexistente no doc),
kanon-bad-gate, kanon-block-without-budget (R3), kanon-binding-no-oracle,
kanon-unbound-shall (SHALL sem binding — warning de cobertura). No --exec,
{path} é substituído pelo path repo-relativo do documento; oráculo não-zero
em binding block reprova a execução.
3. Gramática (EBNF, normativa)
toplevel = quantexpr | expr ;
quantexpr = ( "forall" | "exists" ) , "module" ,
[ "where" , expr ] , ":" , expr ;
expr = or_expr ;
or_expr = and_expr , { "or" , and_expr } ;
and_expr = unary , { "and" , unary } ;
unary = "not" , unary
| primary ;
primary = "(" , expr , ")"
| has_pred
| comparison ;
has_pred = "has" , literal ;
comparison = fact , operator , literal ;
fact = "path" | "surface" | "kind" | "domain" | "area" ;
operator = "==" | "!=" | "~" ;
literal = '"' , { any character except '"' } , '"' ;
- Palavras-chave (
and,or,not,forall,exists,module,where,has) e fatos são case-sensitive, minúsculos. - Literais são sempre entre aspas duplas. Sem escapes na v0.1 (um literal
não contém
"). - Precedência:
not>and>or. Parênteses agrupam.
4. Semântica
Fatos. O chamador fornece um mapa fato→valor (strings):
| Fato | Significado | Exemplo |
|---|---|---|
path | path repo-relativo (separador /) do alvo da ação | products/dev/kvs/landing/index.html |
surface | superfície da ação | web, mobile, desktop, cli, tv, tui |
kind | natureza do alvo | product, area, sector, institutional, catalog, spec, package |
domain | domínio L1 (derivável do path pelo chamador) | products |
area | área L2 (derivável do path) | dev |
component | (v0.5) slug do componente em foco na sessão (domínio notices) | koda, kvs, kdb |
repo | (v0.5) repo em foco na sessão (domínio notices) | koder |
Domínio notices (v0.5, koder-tools#053). Os fatos
component/reposervem okoder-spec-audit notices— o análogo por-turno doapplicable: um broadcast-alert declaraapplies_when(Kanon) sobre o foco da sessão, e o motor computa a relevância (universais semapplies_when+ os que casam), substituindo "a IA lê os ~70 notices todo turno". Ausência de foco ⇒ sem base pra filtrar ⇒ todos relevantes (fail-open; nunca esconde um lock).
Domínio de fatos parametrizável (API de embedding). A tabela acima é o vocabulário do governance plane — o domínio default de
kanon.Eval. Um consumidor que embarca o avaliador noutro domínio fornece o seu vocabulário viakanon.EvalWith(expr, facts, *FactDomain)(Namesexatos +Prefixesabertos, ex.:arg_). A garantia anti-typo (kanon-unknown-fact) vale então por domínio, não global: um fato fora do domínio do chamador é erro alto, e os fatos de um domínio não vazam pra outro. Primeiro consumidor: a política de autorização §5 doservices/ai/agents(stack-RFC-020), domínio{tool, tier, provenance, arg_*}(provenance ∈ {owner, world}= taint instrução×dados do §5.1). Isto é transporte/embedding, não gramática (seam D9): o EBNF (§3) é idêntico, logo não há bump de versão —EvalWithé aditivo eEvalsegue usando o domínio de governança (backward-compatible).
Operadores. == igualdade exata · != desigualdade · ~ glob match
sobre o valor do fato: * casa dentro de um segmento (não atravessa /),
** atravessa segmentos, ? um caractere. Glob ancora a string inteira.
Fato ausente (chamador não forneceu, ex.: surface desconhecida):
== e ~ avaliam false; != avalia true. (Racional: ausência não
pode satisfazer uma igualdade; e "não é X" é verdade do desconhecido.)
Quantificadores (v0.2). Domínio module = diretório que contém
koder.toml (mesma definição do projectlayout). Pra cada módulo, os fatos
são path (repo-relativo do diretório), domain, area; has "<rel>" testa
a existência do path relativo dentro do módulo. where filtra o domínio; o
corpo é a obrigação. forall reprova com a lista de violadores (path de
cada módulo filtrado que falha o corpo); exists passa se ≥1 módulo satisfaz.
has fora de quantificador e quantificador dentro de applies_when são
erros nomeados (contextos não se misturam).
Erros nomeados (avaliação nunca falha em silêncio):
kanon-parse-error · kanon-unknown-fact · kanon-unsupported-version ·
kanon-depth-exceeded.
5. Corpus de conformidade (normativo, executável)
Fatos-base do corpus: path=products/dev/kvs/landing/index.html,
surface=web, kind=product, domain=products, area=dev.
(sem surface) = mesmo mapa sem a chave surface.
| # | Expressão | Fatos | Resultado |
|---|---|---|---|
| C01 | path ~ "**/landing/**" | base | true |
| C02 | path ~ "**/backend/**" | base | false |
| C03 | surface == "web" | base | true |
| C04 | surface == "web" | (sem surface) | false |
| C05 | surface != "tv" | (sem surface) | true |
| C06 | path ~ "**/landing/**" and kind == "product" | base | true |
| C07 | path ~ "**/landing/**" and kind == "area" | base | false |
| C08 | kind == "area" or kind == "product" | base | true |
| C09 | not kind == "area" | base | true |
| C10 | not (kind == "product" or surface == "tv") | base | false |
| C11 | domain == "products" and area == "dev" | base | true |
| C12 | path ~ "products/*/kvs/**" | base | true |
| C13 | path ~ "products/*" | base | false (um * não atravessa /) |
| C14 | path ~ "**/index.htm?" | base | true |
| C15 | banana == "x" | base | erro kanon-unknown-fact |
| C16 | kind == product | base | erro kanon-parse-error (literal sem aspas) |
| C17 | kind == | base | erro kanon-parse-error |
| C18 | (33 parênteses aninhados) | base | erro kanon-depth-exceeded |
Corpus estrutural (v0.2). Domínio do corpus: módulos m1 = products/dev/a {README.md}, m2 = products/dev/b {}, m3 = services/ai/c {README.kmd}.
| # | Expressão | Resultado |
|---|---|---|
| C19 | forall module: has "README.md" or has "README.kmd" | fail; violators = [products/dev/b] |
| C20 | forall module where domain == "products": has "README.md" | fail; violators = [products/dev/b] |
| C21 | exists module: has "README.kmd" | pass |
| C22 | exists module: has "LICENSE" | fail |
| C23 | has "README.md" (em applies_when/Eval) | erro kanon-parse-error (has exige quantificador) |
| C24 | forall module has "x" (sem :) | erro kanon-parse-error |
Corpus de refinamento (v0.3). Pai declara req-a SHALL, req-b SHOULD,
req-c MAY.
| # | Filho declara | Resultado |
|---|---|---|
| C25 | req-b SHALL, req-c SHOULD (tighten) | sem violação |
| C26 | req-z SHALL (add) | sem violação |
| C27 | req-a SHOULD | kanon-loosen em req-a |
| C28 | req-b MAY | kanon-loosen em req-b |
| C29 | (nada) ou req-a sem keyword | sem violação (herança / kmd lint) |
Corpus de bindings (v0.4). Doc declara req-a SHALL, req-b SHOULD.
| # | Bindings | Resultado |
|---|---|---|
| C30 | req-a verify_by+block+fp_budget | sem findings |
| C31 | req-a block SEM fp_budget | kanon-block-without-budget |
| C32 | req-z (órfão) + gate "hard" | kanon-binding-orphan + kanon-bad-gate |
| C33 | nenhum binding | kanon-unbound-shall em req-a (warning); req-b não acusa |
| C34 | refines: pai block → filho warn no mesmo id | kanon-gate-loosen |
6. Versionamento e transporte
kanon: "0.1"no frontmatter do documento portador é OBRIGATÓRIO quando qualquer chave Kanon (applies_when) está presente; ausência = doc inválido (kanon-unsupported-versionna avaliação).- Avaliador v0.4 aceita
0.0–0.4; rejeita0.5+e1.x(forward-compat explícita, nunca tácita). - Documentos sem chaves Kanon não declaram
kanon:— a linguagem é opt-in.
References
meta/docs/stack/rfcs/stack-RFC-015-kanon-declarative-rule-engine.kmdmeta/docs/stack/rfcs/stack-RFC-014-spec-adherence-harness.kmdmeta/docs/stack/registries/stack-manifest.kmd