5. Ferramentas¶
English | Português (Brasil)
Tradução do original em inglês; quando os dois divergirem, vale o inglês.
← O contrato · Índice · Adoção e CI →
As ferramentas que viajam na release ficam em tools/. Elas dependem só de Python 3.12 ou mais e das versões
fixadas, e com hash, em tools/requirements.txt (PyYAML e jsonschema, mais as
dependências do jsonschema).
Todo rc segue a mesma convenção: 0 passou, 1 algo reprovou, 2 a entrada está quebrada (arquivo
ilegível, base ausente, tag inexistente). Um rc 2 nunca passa por verde.
Os exemplos abaixo rodam num repositório de demonstração com o kit em vendor/kg-ssot. Num clone do produto,
troque vendor/kg-ssot/tools/ por tools/.
Língua da saída¶
Desde a 4.18.0, as ferramentas imprimem a saída humana em inglês por padrão, e em português do Brasil quando você pede. Cada ferramenta escolhe a língua do jeito do gettext e para na primeira variável definida:
KG_SSOT_LANG, a escolha só para o KG-SSOT (pt-BR,pt_BRouen);LANGUAGE, uma lista separada por dois-pontos (pt_BR:en), em que vence a primeira língua com catálogo;LC_ALL, depoisLC_MESSAGES, depoisLANG(pt_BR.UTF-8).
Uma variável vazia conta como não definida. C e POSIX são o inglês. Uma língua sem catálogo também: a variável
definida decide do mesmo jeito, e as ferramentas não passam para a próxima. Um pt sozinho casa com pt_BR. A frase que ainda não tem tradução sai
em inglês. O catálogo é locale/pt_BR/LC_MESSAGES/kg-ssot.po, e ele viaja na release e no vendor (também no
--profile gate).
KG_SSOT_LANG=pt-BR python3 -I -B vendor/kg-ssot/tools/kg_validate.py docs/broken.kg.yaml
KG_SSOT_LANG=en python3 -I -B vendor/kg-ssot/tools/kg_validate.py docs/broken.kg.yaml
Dado não muda de língua. A estrutura do JSON e as chaves dele, os códigos (form.required.node.label), os códigos de
saída, o lugar de uma ocorrência (nó C_RETRY_SAFE, aresta #3, arquivo) e a planilha TSV do kg_migrate.py ficam
iguais em toda língua. Os campos message e hint de um veredito são texto para pessoas: seguem a língua na linha
de comando. O servidor MCP fala inglês em qualquer locale, porque as descrições, os erros e os resultados dele são
protocolo e prompt para o modelo. Como no gettext, o LANGUAGE só vale quando o locale efetivo não é o C, então
LC_ALL=C força o inglês. Para um passo de CI não depender da língua, decida pelo rc, como mostra o
capítulo 6.
kg_validate.py: o leitor de referência¶
Valida um ou mais arquivos contra o contrato vigente.
PASSA docs/example.kg.yaml
REPROVA docs/broken.kg.yaml
MUST form.required.node.provenance ×1 · form.unknown-key.node.priority ×1
SHOULD form.required.node.verified_against ×1 · integrity.untraced-decision ×1 · yaml.unquoted-date ×1
· 2 arquivos; 1 passam no MUST; 1 com aviso SHOULD (kg-contract-v4.schema.json + kg-contract-v4.should.schema.json)
rc 1. Para ver de qual nó sai cada código, use --where:
REPROVA docs/broken.kg.yaml
MUST form.required.node.provenance ×1 · form.unknown-key.node.priority ×1
SHOULD form.required.node.verified_against ×1 · integrity.untraced-decision ×1 · yaml.unquoted-date ×1
MUST form.required.node.provenance nó C_RETRY_SAFE
→ Acrescente provenance com source, locator e method; sem fonte recuperável, mude o status para unverifiable.
MUST form.unknown-key.node.priority nó C_RETRY_SAFE
→ Renomeie a chave com o prefixo x_ (extensão que os leitores ignoram) ou remova-a.
SHOULD form.required.node.verified_against nó C_RETRY_SAFE
SHOULD integrity.untraced-decision nó D_RETRY_POLICY
SHOULD yaml.unquoted-date arquivo
· 1 arquivos; 0 passam no MUST; 1 com aviso SHOULD (kg-contract-v4.schema.json + kg-contract-v4.should.schema.json)
Opções úteis:
| Opção | Efeito |
|---|---|
--where |
Uma linha por ocorrência, com o nó ou a aresta; numa ocorrência MUST, a correção vem logo abaixo |
--corpus DIR |
Confere a ponta externa de cada entrada de external_edges contra os .kg.yaml e .kg.json que o git rastreia sob DIR (fora de um repositório git, todos) |
--schema F |
Valida contra outro schema MUST (o schema SHOULD vizinho vem junto, se existir) |
--format json |
O veredito estruturado em stdout (ver abaixo), para ferramentas e servidores MCP |
--limit N |
Com --format json, quantas ocorrências por arquivo (default 50); o resumo conta todas |
- no lugar do arquivo |
Lê o grafo do stdin, sem arquivo temporário; --stdin-format json lê um grafo .kg.json (default yaml) |
--should-schema F |
Com --schema, o schema SHOULD que acompanha (no lugar do vizinho) |
--due AAAA-MM-DD |
No lugar do veredito, o relatório de vencimento na data (desde a 4.15.0): cada nó com reverify_every cujo verified_at somado à cadência cai na data ou antes, cada nó que tem cadência e não tem carimbo, e cada nó que só tem reverify_on. Desde a 4.19.0 lista também os perfis canônicos com cadência: cada sinônimo de um x_term que não está aposentado, o x_mirror (synced_at mais resync_every) e o x_doc_cycle (checked_at mais recheck_every), com subject e detail no JSON. O rc é 0 (ou 2 com entrada quebrada): o vencimento nunca reprova, e o relatório não julga o grafo (o gate segue sendo o kg_gate.py). Um arquivo que não parseia sai como NÃO LIDO; num job agendado, olhe essa lista, porque o rc não a acusa. A cadência fora da forma ou zerada fica fora do relatório (o aviso SHOULD a acusa), e o vencimento depois do ano 9999 também. Com --format json, a saída usa o formato kg-ssot.due/1 (spec/due.schema.json) |
As opções --json OUT, --spike, --git, --rev e --list são do instrumento de medição que deu origem ao
contrato (o "spike"). Elas gravam e reproduzem um formato legado, congelado, e não fazem parte da interface estável.
Para ferramentas, use --format json.
Com external_edges e sem --corpus, o leitor avisa que não conferiu a ponta externa:
PASSA docs/decision.kg.yaml
· 1 arquivos; 1 passam no MUST; 0 com aviso SHOULD (kg-contract-v4.schema.json + kg-contract-v4.should.schema.json)
· 1 arquivo(s) com external_edges: a ponta externa NÃO foi conferida (rode com --corpus <raiz do repo>)
O veredito estruturado (--format json)¶
Se você chama o leitor por programa (um job de CI, um servidor MCP, um chat), o --format json devolve o veredito
num formato versionado, kg-ssot.verdict/1, com JSON Schema em spec/verdict.schema.json. Saída real de um caso da
suíte:
python3 -I -B vendor/kg-ssot/tools/kg_validate.py --format json \
vendor/kg-ssot/spec/conformance/fixtures/latest/form/minimal/bad-node-missing-label.kg.yaml
{
"schema_version": "kg-ssot.verdict/1",
"contract": {
"must": "spec/kg-contract-v4.schema.json",
"should": "spec/kg-contract-v4.should.schema.json",
"release": "kg-ssot-v4.39.0",
"sha256": { "must": "6a05cb1e…", "should": "04327d29…" }
},
"results": [
{
"source": "…/bad-node-missing-label.kg.yaml",
"verdict": "FAIL",
"summary": { "MUST": { "form.required.node.label": 1 }, "SHOULD": {} },
"occurrences": [
{
"level": "MUST",
"code": "form.required.node.label",
"place": "nó Q_A",
"path": "nodes[0]",
"line": 5,
"message": "Falta o campo obrigatório «label» no nó.",
"hint": "Acrescente label: a afirmação curta do nó, até 280 caracteres."
}
],
"total": 1,
"truncated": false,
"external_checked": true
}
]
}
(Os dois sha256 e o caminho estão abreviados aqui, e alguns objetos ficam numa linha só.)
- O que é estável:
codeé o vocabulário da suíte e do gate, e é por ele que uma ferramenta decide.messageehintsão texto para pessoas: seguem a língua da saída e podem mudar. - O lugar da ocorrência:
pathaponta no documento (nodes[7].provenance.method) elineé a linha no arquivo, no.kg.yamle no.kg.json, para quem edita corrigir o ponto certo sem reescrever o grafo. - O corte:
summaryconta todas as ocorrências.occurrencestraz até--limit, etruncatedetotaldizem se a lista foi cortada. Assim um grafo grande não enche o contexto de um modelo. - O contrato que julgou:
contracttraz o caminho, o sha256 de cada schema e a release, para quem precisa provar qual contrato julgou. - O rc: 0 quando todos os arquivos passam, 1 quando algum reprova (um arquivo que não parseia reprova com
parse.*, como na suíte) e 2 quando a entrada está quebrada (arquivo ilegível, opção inválida). - O desempenho: com a libyaml instalada (o caso comum do PyYAML), o parse usa o parser em C, com o mesmo perfil
YAML 1.2. Ele só cai no parser puro, mais lento, nos raros textos em que os dois podem divergir (tab,
%YAML, escape de surrogate ou\U, BOM fora do início).
kg_conformance.py: a suíte¶
Roda a suíte de conformidade contra o leitor de referência. Primeiro o metaschema se auto-testa, depois cada manifesto é validado, depois cada caso roda.
As últimas linhas:
rc 0. latest/ é o contrato vigente, e um caso que falha ali reprova a rodada (rc 1). optional/ é comportamento
que um leitor pode implementar (hoje, a camada semântica de frescor e domínio, que o leitor de referência não
implementa) e nunca muda o rc. rc 2 quer dizer que a própria suíte está quebrada: manifesto inválido, arquivo fora
de manifesto, pasta desconhecida.
kg_gate.py: a catraca por grafo¶
O gate é para o CI de um repositório que guarda grafos. Ele mede todos os .kg.yaml que o git rastreia e
compara o resultado com uma linha de base (a pasta .kg-ssot/gate/). A regra: nada piora. Um grafo que já
reprovava fica registrado como dívida herdada; um grafo novo tem de passar.
Sem base, o gate se recusa a rodar:
rc 2. A primeira base:
MUST docs/broken.kg.yaml (herdado): form.required.node.provenance · form.unknown-key.node.priority
SHOULD form.required.node.verified_against: 1 grafo
SHOULD integrity.untraced-decision: 1 grafo
SHOULD yaml.unquoted-date: 1 grafo
linha de base gravada: .kg-ssot/gate (2 grafos, 1 na dívida herdada)
Um grafo novo que reprova, depois de git add:
✗ PIOROU docs/new.kg.yaml falha no MUST: form.required.node.provenance, form.unknown-key.node.priority
✗ PIOROU debt.form.required.node.verified_against: 1 → 2
✗ PIOROU debt.integrity.untraced-decision: 1 → 2
✗ PIOROU debt.yaml.unquoted-date: 1 → 2
· 3 grafos; 1 passam no MUST; 1 na dívida herdada
MUST docs/broken.kg.yaml (herdado): form.required.node.provenance · form.unknown-key.node.priority
MUST docs/new.kg.yaml (novo): form.required.node.provenance · form.unknown-key.node.priority
SHOULD form.required.node.verified_against: 2 grafos
SHOULD integrity.untraced-decision: 2 grafos
SHOULD yaml.unquoted-date: 2 grafos
rc 1. E quando a dívida é paga (o grafo herdado consertado):
↑ melhorou docs/broken.kg.yaml: passa no MUST (trave o ganho: kg_gate.py --update)
↑ melhorou debt.form.required.node.verified_against: 1 → 0 (trave o ganho: kg_gate.py --update)
↑ melhorou debt.integrity.untraced-decision: 1 → 0 (trave o ganho: kg_gate.py --update)
↑ melhorou debt.yaml.unquoted-date: 1 → 0 (trave o ganho: kg_gate.py --update)
· 2 grafos; 2 passam no MUST; 0 na dívida herdada
rc 0, com o pedido de travar o ganho. Rode --update no mesmo PR para a dívida não voltar.
| Opção | Efeito |
|---|---|
--update |
Grava a base. Se algo piorou, recusa, a menos que você passe --accept-regression |
--accept-regression "<motivo>" |
Aceita uma piora; o motivo fica gravado na base |
--exclude GLOB |
Tira caminhos do corpus (repetível). */fixtures/*, docs/materials/* e o vendor do KG-SSOT (qualquer pasta com o carimbo .kg-ssot-version, desde a 4.19.1) já saem por padrão; o --no-default-excludes os traz de volta |
--granularity node |
Com --update: conta a dívida SHOULD por ocorrência, não por grafo |
--no-default-excludes |
Desliga os excluídos padrão (e aí as fixtures do vendor entram no corpus) |
--repo DIR · --baseline ARQ |
Outro repositório, outra base |
kg_vendor.py: trazer e conferir a release¶
O adotante nunca copia o contrato à mão. A ferramenta de vendor lê uma tag com git archive e grava um carimbo
com o sha256 de cada arquivo.
Rode da raiz do adotante, com um clone do repositório público em ../kg-ssot. As saídas abaixo são de execuções
reais da ferramenta da 4.18.0 (o update e o check) com a tag kg-ssot-v4.17.2. As ferramentas das releases
anteriores à 4.18.0 imprimem em português. Numa release mais nova, a tag, o commit e a contagem de arquivos mudam:
git clone https://github.com/marciocar/kg-ssot ../kg-ssot
python3 -I -B ../kg-ssot/tools/kg_vendor.py update --tag kg-ssot-v4.17.2 --dest vendor/kg-ssot
O update grava também vendor/kg-ssot/.gitattributes com * -text. Um atributo aninhado vence o do repositório,
e assim a regra de fim de linha do adotante (*.json text eol=lf, core.autocrlf) não reescreve no índice os bytes
da release. A suíte tem fixtures com CRLF de propósito, que sem isso chegariam alteradas ao CI.
O check recalcula os sha256 e compara com o carimbo. Um arquivo editado, apagado ou sobrando (inclusive bytecode
num __pycache__) dá rc 1, e o check também confere o .gitattributes do vendor. Com --index, o check confere
os blobs do índice do git, que é o que o commit vai levar, e não a árvore de trabalho. Rode-o depois do
git add vendor/kg-ssot: ele pega, antes do commit, uma normalização que a árvore de trabalho esconde.
| Opção | Efeito |
|---|---|
--source REPO |
O repositório (caminho ou URL, como https://github.com/marciocar/kg-ssot) que tem a tag. Obrigatório quando você roda de dentro de um vendor |
--dest DIR |
Onde fica o vendor (default vendor/kg-ssot) |
--profile gate |
Traz só o mínimo para rodar o gate e o check (17 arquivos desde a 4.26.0, sem a suíte); --profile full volta à release inteira |
--force |
Substitui um vendor editado localmente |
--index (no check) |
Confere o índice do git em vez da árvore de trabalho |
--ref COMMIT (no update) |
A bancada antes da tag (4.24): traz um commit que ainda não é release, de um repositório a que você chega. O carimbo diz unreleased |
--allow-unreleased (no check e no kg_gate.py) |
Aceita um vendor de bancada. Sem ele, o check e o gate reprovam esse vendor (e o kg_gate.py nunca grava a base a partir de um) |
Por que -B? Os scripts do kit não gravam bytecode, mas uma ferramenta sua que importa um módulo do kit
grava o .pyc dentro do vendor, e o próximo check reprova. Rode tudo com python3 -I -B, ou com
PYTHONDONTWRITEBYTECODE=1.
kg_store.py: o armazém que só grava o que passa¶
Se você grava grafo por programa (um servidor de escrita, um chat, um pipeline que gera o grafo de um caso), o
kg_store.py é uma biblioteca, com um CLI por cima, que só escreve depois do veredito PASS do contrato. Há três
armazéns com a mesma interface (read, list, write, delete, history). O formato vem do nome: .kg.yaml é
julgado no perfil YAML e .kg.json no perfil JSON do contrato
(capítulo 3):
DiskStore: só a cabeça em disco, sem histórico;GitStore: um commit por gravação ou remoção. O git roda isolado na raiz do armazém e nunca sobe para um repositório de fora. O autor vem de quem grava (author="Name <email>"), e o committer é uma identidade fixa do serviço;VersionedStore: a cabeça em disco mais um registro de eventos e as versões no diretório oculto.kg-store/, sem git.
Pelo pacote PyPI, é from kg_ssot import kg_store. Para importar do vendor, ponha vendor/kg-ssot/tools no
sys.path e rode sem gravar bytecode, como no capítulo 6:
import kg_store
store = kg_store.GitStore("areas/ana", init=True)
r = store.write("graphs/case.kg.yaml", text, message="first graph", author="Ana <ana@example.org>",
expected_sha256=kg_store.NEW)
if not r["written"]:
print(r["reason"], r["verdict"]) # "gate", "conflict", "missing", or "not_judged"
O gate julga mais que o candidato. A ponta externa de cada entrada de external_edges é conferida contra o corpus
do armazém (no GitStore, também os grafos rastreados em pasta com ponto, desde a 4.27.0), já com o candidato no lugar. Cada grafo que aponta para o nome gravado é julgado de novo, porque trocar ou
apagar um grafo pode deixar a aresta externa de outro sem ponta. O armazém só grava com PASS em todos.
Desde a 4.20.0, um objeto de armazém guarda, na memória, o veredito de cada grafo que já julgou, pela chave de tudo de
que o veredito depende: o texto e o formato dele, os dois schemas, o limit, a língua das mensagens, se há corpus e,
para cada ponta externa, se o id citado existe. Um grafo que aponta para o nome gravado e não mudou não é julgado de
novo enquanto as pontas dele seguem resolvidas, e um processo longo (o servidor MCP, um app que mantém o armazém
aberto) paga o custo uma vez. Gravar o mesmo texto de novo também não o rejulga pelo contrato; o gate externo
(--gate-cmd), se houver, roda em toda gravação. Um processo novo começa com o cache vazio, e a primeira gravação paga
o custo inteiro.
O corpus é o mesmo que o gate do repositório veria. No GitStore, isso quer dizer só o rastreado: um .kg.yaml que
está no disco sem commit não conta, nem na lista, nem no julgamento, nem na trava (para a trava, o nome está livre, e
a gravação substitui o arquivo). Desde a 4.27.0 (contrato v4.15), o julgamento do GitStore lê também os grafos
rastreados em pasta com ponto (.github/), como o gate: a ponta externa ali resolve, e uma mudança ali pode barrar
uma gravação que a deixe sem ponta. O armazém continua sem escrever ali, e o list(), o read() e as ferramentas
kg_query e kg_terms do servidor MCP veem só os nomes do próprio armazém. O DiskStore e o VersionedStore
julgam só os nomes do armazém (a pasta com ponto fora do git não é lida), e recusam uma raiz dentro de um repositório
git, onde o gate leria o rastreado e o armazém leria o disco. E a cabeça só fica trocada com o registro feito: se o commit
ou o evento falha, ela volta ao que era, e a exceção chega a quem chamou.
Para regenerar grafos que se citam, use write_many (desde a 4.14.0). Um a um, a troca do primeiro grafo deixaria a
aresta externa do segundo sem ponta, e o rejulgamento barraria. O conjunto inteiro, inclusive com remoções (texto
None), é julgado de uma vez, com todas as mudanças no lugar, e entra num registro só (um commit, ou um bloco de
eventos), ou nada entra:
r = store.write_many([("case.kg.yaml", case), ("crossing.kg.json", crossing), ("old.kg.yaml", None)],
message="regenerate the case", expected={"case.kg.yaml": sha_read})
if not r["written"]:
print(r["reason"], [x["reason"] for x in r["results"]])
Em conflict e missing, o nome que barrou o conjunto leva o motivo, e os outros voltam como skipped. Em gate e
not_judged, o julgamento é do conjunto inteiro, e todos os nomes voltam com esse motivo. O gate externo
(--gate-cmd), se houver, julga cada mudança isolada, como no write. Nomes do conjunto que só diferem pela caixa
são recusados, como no armazém.
O CLI tem um subcomando por operação, com a saída em JSON no stdout:
kg_store.py --backend disk|git|versioned --root DIR [--init] [--gate-cmd CMD [--gate-timeout S]] list
kg_store.py ... read NAME [--ref REF]
kg_store.py ... write NAME [FILE|-] [--message M] [--author "Name <email>"] [--expected-sha256 S|new]
kg_store.py ... delete NAME [--message M] [--author A] [--expected-sha256 S]
kg_store.py ... write-many SET.json [--message M] [--author A]
kg_store.py ... history NAME
Pelo CLI, um grafo que passa e um que reprova:
python3 -I -B vendor/kg-ssot/tools/kg_store.py --backend git --root areas/ana --init \
write graphs/valid-minimal.kg.yaml vendor/kg-ssot/spec/conformance/fixtures/latest/form/minimal/valid-minimal.kg.yaml \
--author "Ana <ana@example.org>" --message "first graph"
{
"written": true,
"reason": null,
"name": "graphs/valid-minimal.kg.yaml",
"sha256": "b725f37669f2e5979ed8c02eaae3c59ba80a490177e2f28c0cb8a580cf51cfd7",
"ref": "2c6e97cc668be073020246d673228ec11dddb9aa",
"verdict": { … },
"error": null,
"unchanged": false
}
python3 -I -B vendor/kg-ssot/tools/kg_store.py --backend git --root areas/ana \
write graphs/x.kg.yaml vendor/kg-ssot/spec/conformance/fixtures/latest/form/minimal/bad-node-missing-label.kg.yaml
{
"written": false,
"reason": "gate",
"name": "graphs/x.kg.yaml",
...
"verdict": { "results": [ { "source": "graphs/x.kg.yaml", "verdict": "FAIL",
"summary": { "MUST": { "form.required.node.label": 1 }, "SHOULD": {} }, ... } ] }
}
(O verdict está abreviado aqui; ele é o documento kg-ssot.verdict/1 inteiro.)
reason: énullquando gravou."gate"quer dizer que o contrato reprovou o grafo, e overdictdiz onde."conflict"quer dizer que oexpected_sha256não bate com a cabeça."missing"aparece quando você remove o que não existe."not_judged"aparece quando o julgamento falhou ou saiu fora despec/verdict.schema.json: nunca vira PASS nem"gate", para quem chama distinguir "reprovou" de "não deu para julgar". Com um gate externo,"gate"comerror: "reprovado pelo gate externo"quer dizer que o contrato aprovou e o gate de fora reprovou o grafo; overdicté o do gate externo.- A trava otimista:
expected_sha256é o sha256 da cabeça que quem grava leu, ouNEW(no CLI,new) para exigir que o nome ainda não exista. Gravar o mesmo texto da cabeça devolvewrittencomunchangede sem commit. - O caminho: o nome é relativo, termina em
.kg.yamlou.kg.jsone não tem..nem segmento oculto. Para o armazém ser copiável entre sistemas, ele recusa criar um nome com segmento reservado do Windows (CON,NUL,COM1...) ou que termina em ponto (o que já existe segue funcionando), e nenhuma operação aceita um nome que só difere pela caixa de um que já está no armazém. A raiz, as pastas e o arquivo não podem ser link: - no macOS e no Linux (POSIX), tudo se abre por
dir_fdcomO_NOFOLLOW, e trocar uma pasta no meio da operação não desvia nada; - no Windows, que não tem
dir_fd, cada passo do caminho é conferido porlstat(link simbólico, junção e ponto de montagem são recusados; o reparse point que não redireciona o nome, como o arquivo do OneDrive sob demanda, passa), o arquivo aberto tem de ser o mesmo que olstatviu, e o caminho é conferido de novo depois da leitura e, na gravação, antes da troca final. É mais fraco: quem pode escrever dentro da raiz consegue trocar uma pasta entre a conferência e o uso. Num sistema de arquivos sem id de arquivo (o FAT de um pendrive), a comparação do arquivo aberto não prova nada, e só a recusa de link sobra. Num armazém de uma pessoa só, na pasta dela, isso não muda nada; para um armazém compartilhado, prefira POSIX. - O histórico:
history(name)lista as operações, da mais nova para a mais antiga, comref,op(writeoudelete), autor, data e mensagem.read(name, ref=...)lê uma versão anterior: o sha do commit no git, oseqdo evento no versionado. - O git isolado: o
GitStorenão lê a config do sistema nem a do usuário (sem hooks, sem gpg, semautocrlf, sem arquivo de exclusão), então o que ele guarda é byte a byte o texto aprovado, com o sha256 do resultado. - O gate externo (
--gate-cmd): é o gancho por onde um adotante liga um segundo gate, o dele, sem o produto depender desse gate. O comando roda depois de o contrato aprovar, em cada gravação e cada remoção, e o armazém só grava com PASS dos dois. Um grafo que o contrato reprova nunca chega ao comando. O gate que okg_mcp.pyroda recebeKG_SSOT_LANG=en, porque o veredito dele vai ao modelo; pelokg_store.py, ele herda a sua língua. - A entrada: o texto candidato vai pelo stdin em UTF-8 (vazio na remoção); leia-o como bytes e decodifique, para
não depender da página de código do sistema. O nome e a operação vão pelo ambiente, como
KG_GATE_NAMEeKG_GATE_OP(writeoudelete). O comando herda o resto do ambiente do processo, menos a variável do segredo dokg_mcp(--secret-env). - A saída: um documento
kg-ssot.verdict/1válido porspec/verdict.schema.json, com ao menos um resultado, rc 0 quando tudo passa e rc 1 quando algo reprova, e no máximo 16 MiB (o teto vale enquanto ele roda). O stderr do gate é descartado: para diagnosticar, rode o comando à mão com o mesmo stdin e o mesmo ambiente. O código de uma ocorrência tem de começar por uma camada do contrato (parse.,form.,integrity.ouyaml.); um gate de fora usa, por exemplo,integrity.external.<nome>. - Fail-closed: comando que não roda, que passa de
--gate-timeout(padrão 60 s, no máximo 86.400), que sai sem JSON, com saída grande demais, com JSON fora do schema ou sem resultado, com rc fora de 0 e 1 ou com rc que não bate com o veredito dá"not_judged", nunca PASS, e nada é gravado. No tempo esgotado, numa interrupção (Ctrl+C), num SIGTERM ao processo ou com a saída grande demais, morrem o comando e os processos que ele abriu. No Windows, matar o processo à força (o "Finalizar tarefa") não deixa tratador nenhum agir, e o gate que estiver rodando pode ficar. No macOS e no Linux, um processo que o gate deixa rodando depois de terminar bem também é morto (o grupo de processos dele); no Windows, não, então o gate de um adotante não deve deixar nada para trás.--gate-cmd ""dá rc 2, para uma variável vazia nunca subir um servidor sem o gate dele;--gate-timeoutsem--gate-cmde, nokg_mcp.py,--gate-cmdcom--read-only(o gate nunca rodaria) também dão rc 2. - A trava: o gate roda sob a trava da gravação. Um gate lento segura as outras gravações do mesmo armazém até o
--gate-timeout; no stdio, segura o servidor. - O mesmo texto: gravar de novo o texto da última versão também passa pelo gate. Se ele reprovar agora, a
resposta é
"gate", mesmo sem nada a mudar. - Sem shell: o comando é uma lista de argumentos, nunca uma linha de shell. No macOS e no Linux, a linha segue
as regras do
shlex(a barra invertida escapa; ponha o caminho entre aspas). No Windows, as aspas duplas agrupam e a barra invertida fica como está, como no comando"C:\Program Files\Python\python.exe" C:\gates\x.py. As aspas valem em volta do argumento inteiro:--opt="a b"vira dois argumentos, e aspas simples não agrupam. O texto do grafo só chega pelo stdin, então$(...)ou;nele não executam nada. - Pela biblioteca:
open_store(..., gate=CommandGate(argv, timeout)), ou qualquer chamável(name, text, op) -> (verdict, error). Um chamável que levanta exceção também dá"not_judged".
Um gate de exemplo, só com a biblioteca padrão, que reprova todo texto com a palavra FORBIDDEN:
import json, os, sys
bad = "FORBIDDEN" in sys.stdin.buffer.read().decode("utf-8")
occ = [{"level": "MUST", "code": "integrity.external.forbidden-word", "place": "arquivo", "path": "",
"line": None, "message": "the text has a forbidden word"}] if bad else []
print(json.dumps({"schema_version": "kg-ssot.verdict/1",
"contract": {"must": None, "should": None, "release": None, "sha256": {"must": None, "should": None}},
"results": [{"source": os.environ["KG_GATE_NAME"], "verdict": "FAIL" if bad else "PASS",
"summary": {"MUST": {occ[0]["code"]: 1} if bad else {}, "SHOULD": {}},
"occurrences": occ, "total": len(occ), "truncated": False,
"external_checked": False}]}))
sys.exit(1 if bad else 0)
python3 -I tools/kg_store.py --backend versioned --root areas/ana --init \
--gate-cmd "python3 -I gates/forbidden.py" write graphs/a.kg.yaml graph.kg.yaml
flock nem msvcrt.
- O rc do CLI: 0 gravou ou leu, 1 recusado (gate, conflict, missing, nome inexistente) e 2 entrada
quebrada ou not_judged.
kg_mcp.py: o servidor MCP de referência¶
Para dar a um modelo o contrato e o armazém sem escrever um servidor, o kg_mcp.py fala o
Model Context Protocol por stdio (uma mensagem JSON-RPC por linha) ou por HTTP
(POST /mcp, resposta JSON, sem stream). Ele usa só a biblioteca padrão, o leitor e o armazém, e é um adaptador: o
produto não depende dele, e ele não depende de nenhum cliente específico.
| Ferramenta | O que faz |
|---|---|
kg_validate |
Julga um texto .kg.yaml sem gravar e devolve o kg-ssot.verdict/1 (com o armazém como corpus, se há armazém) |
kg_write |
Grava pelo armazém, só com PASS; devolve o resultado do kg_store com o veredito |
kg_delete |
Remove pelo armazém, se nenhum grafo que aponta para o nome reprovar sem ele |
kg_get |
Lê um grafo, ou uma versão anterior com ref |
kg_list |
Lista os grafos do armazém |
kg_history |
As operações de um grafo, da mais nova para a mais antiga |
kg_query |
A consulta e a projeção do kg_query.py sobre o armazém: filtros, a fila dos abertos, a vizinhança e fields, paginada por cursor; desde a 4.43.0, também due, as_of, has, missing e chain |
kg_terms |
Desde a 4.19.0: resolve um termo de domínio pelo nome preferido ou por um sinônimo (o perfil x_term), com a fonte, a versão e a data de conferência de cada sinônimo e o termo que substitui um termo substituído; sem text, lista os termos |
kg_schema_help |
O contrato vigente, para um modelo: os schemas MUST e SHOULD inteiros, um exemplo mínimo que passa, as regras de integridade e a correção dos códigos mais comuns |
Desde a 4.17.2, cada ferramenta anuncia no tools/list as anotações do MCP: as de leitura (kg_validate, kg_get,
kg_list, kg_query, kg_terms, kg_history, kg_schema_help) com readOnlyHint e idempotentHint, e o kg_write e o
kg_delete com destructiveHint; todas com openWorldHint: false, porque o armazém é local. São dicas para o cliente
pedir ou dispensar a confirmação; o gate do armazém segue decidindo o que é gravado.
Sem --root, só kg_validate e kg_schema_help aparecem. Com --read-only, kg_write e kg_delete somem da
lista.
Com --store-optional, uma raiz que não abre (um arquivo no lugar da pasta, uma pasta sem permissão) não derruba o
servidor. Ele sobe só com kg_validate e kg_schema_help e diz o motivo ao modelo, nas instruções, na descrição das
duas ferramentas e num campo store do kg_schema_help. Assim o modelo conta ao usuário por que não há kg_write, em
vez de supor um modo só leitura. Sem a opção, a raiz que não abre é rc 2, com a mensagem no stderr.
Uma configuração de cliente por stdio, com um armazém versionado fora do repositório:
{
"mcpServers": {
"kg-ssot": {
"command": "python3",
"args": ["-I", "-B", "vendor/kg-ssot/tools/kg_mcp.py",
"--root", "/path/to/store", "--backend", "versioned", "--init",
"--author", "Assistant <assistant@example.org>"]
}
}
}
| Opção | Efeito |
|---|---|
--root DIR |
O armazém. Sem ele, só kg_validate e kg_schema_help |
--backend disk\|git\|versioned |
O tipo de armazém (default disk) |
--init |
Cria a raiz se não existir |
--author "Name <email>" |
Quem assina as gravações |
--read-only |
Tira kg_write e kg_delete |
--http PORT · --host H · --allow-remote |
HTTP no lugar do stdio; fora do loopback, só com --allow-remote, que exige --secret-env |
--secret-env VAR · --secret-header H |
O segredo compartilhado, lido da variável VAR, que cada requisição HTTP traz no header H (padrão X-Api-Key), comparado por hmac |
--identity-header H |
O perfil "atrás de proxy": a identidade de cada requisição vem do header H (um e-mail), com área e autor próprios; exige --secret-env |
--max-bytes N |
O tamanho máximo de um grafo (default 2.000.000) |
--max-graphs N |
Grafos por armazém, ou por área no perfil; além disso, um nome novo é recusado |
--max-concurrent N |
Requisições HTTP ao mesmo tempo; além disso, 503 |
--store-optional |
Uma raiz que não abre não derruba o servidor: ele sobe sem armazém e diz o motivo ao modelo |
--gate-cmd CMD · --gate-timeout S |
Um gate externo depois do contrato, em toda gravação e remoção, também por área no perfil (veja o kg_store.py) |
Uma gravação recusada, como o modelo a recebe (o structuredContent, abreviado):
{
"written": false,
"reason": "gate",
"name": "graphs/x.kg.yaml",
"verdict": { "results": [ { "verdict": "FAIL",
"occurrences": [ { "code": "form.required.node.label", "place": "nó Q_A", "path": "nodes[0]", "line": 5,
"message": "Falta o campo obrigatório «label» no nó.",
"hint": "Acrescente label: a afirmação curta do nó, até 280 caracteres." } ] } ] },
...
}
A resposta de tools/call traz o mesmo objeto em structuredContent e, como texto JSON, em content. Uma gravação
recusada ou um nome inexistente vem com isError: true, para o modelo corrigir e tentar de novo.
- Quem assina: o autor das gravações é o
--authordo servidor ou, no perfil "atrás de proxy", a identidade autenticada da requisição. Nunca é um argumento da ferramenta: o modelo não escolhe quem assina. - Os limites:
--max-bytes(padrão 2.000.000) limita o texto de um grafo. Os argumentos são conferidos contra oinputSchemaque otools/listanuncia: tipo errado, campo obrigatório ausente e campo desconhecido voltam comoisError. Uma falha interna vira o erro JSON-RPC -32603, e o processo segue atendendo. - O leitor por trás: um documento que abre mais de 1.000 coleções de uma vez (em flow, em bloco ou por
indentação), ou cujos aliases expandem para mais que o dobro do tamanho do texto, reprova com
parse.yaml-errorantes de ser construído. Sem esse teto, uma mensagem de 60 KB derrubava o processo. Colchete dentro de texto não conta. - O protocolo: uma mensagem por vez; lote JSON-RPC é recusado (saiu do MCP em 2025-06-18), e o id tem de ser texto ou inteiro. O stdio lê e escreve UTF-8 como bytes, independente do locale, e a resposta sai em JSON ASCII.
- O HTTP: escuta em
127.0.0.1por padrão. RecusaOrigincujo host não seja de loopback (contra DNS rebinding),MCP-Protocol-Versiondesconhecida (400) e corpo semContent-Length. Não há TLS: o servidor só sai do loopback com--allow-remote, que exige--secret-env, para rodar numa rede privada atrás de um proxy com TLS. - O perfil "atrás de proxy" (
--identity-header): é o modo para um cliente com muitos usuários, como o LibreChat, que manda o usuário logado num header. - Recusa por padrão: toda requisição traz a identidade (um e-mail) e o segredo compartilhado. Sem identidade, com identidade vazia (o LibreChat troca um placeholder ausente por texto vazio) ou malformada, ou com o segredo errado, a resposta é 401, antes de qualquer ferramenta rodar.
- Área e autor: cada identidade grava na própria área sob
--root, num diretóriou-<nome>-<hash>, e não enxerga a dos outros. As gravações dela são assinadas na formaparte-local <e-mail>. O e-mail não distingue caixa:Ana@X.orgeana@x.orgsão a mesma identidade. - Por requisição: identidade, área e autor viajam num contexto de cada requisição, e nunca num atributo do servidor, que é um só entre as threads. Guardá-los no servidor deixaria uma requisição concorrente gravar na área de outra, a classe do CVE-2026-25536 do SDK TypeScript. Um teste determinístico confere que nenhuma requisição deixa ou troca estado no servidor, e outro teste, com duas identidades em paralelo, exercita o isolamento de ponta a ponta. Um header de identidade ou de segredo repetido é recusado, para um proxy que acrescente o header em vez de substituí-lo não deixar o cliente escolher a identidade.
- O modelo de confiança: quem tem o segredo afirma qualquer identidade. O segredo autentica o proxy (o LibreChat), e é o proxy que autentica o usuário. Por isso o segredo nunca vai a um cliente final, e o servidor fica numa rede que só o proxy alcança.
- Os limites:
--max-graphsvale por área e é contado sob a trava da gravação, então gravações paralelas não passam juntas dele.initialize,pinge as notificações não abrem nem criam área, e com--read-onlya área que ainda não existe não é criada. O número de áreas não tem limite: quem tem o segredo cria uma por identidade nova (o modelo de confiança acima), e o servidor guarda as áreas abertas em memória enquanto roda. - A raiz das áreas: é conferida ao subir. Valor vazio, raiz inexistente sem
--init, arquivo ou link dá rc 2, a menos que você passe--store-optional, como na raiz fora do perfil. - O envelope
untrusted_data:kg_get,kg_list,kg_history,kg_queryekg_termsdevolvem{"untrusted_data": ..., "notice": ...}, para o modelo tratar o texto gravado por outros como dado, nunca como instrução. - O que o perfil não é: é "atrás de proxy", numa rede privada. Não é o perfil OAuth da especificação MCP, que espera até haver uma matriz medida de clientes.
- A imagem OCI:
ghcr.io/marciocar/kg-ssot:<version>traz okg-mcppronto, para rodar como serviço ao lado de um cliente sem Python, como o LibreChat. Ela é construída do mesmo wheel do PyPI, com as dependências fixadas por hash e a base fixada por digest (packaging/oci/Dockerfile), roda com um usuário sem privilégio (uid 10001) e guarda o armazém no volume/data. Leva o labelio.modelcontextprotocol.server.name, que o MCP Registry usa para conferir a posse. O workflow do espelho a publica atrás de aprovação, com a atestação de proveniência (gh attestation verify oci://ghcr.io/marciocar/kg-ssot:<version> --repo marciocar/kg-ssot). - Por stdio:
docker run -i --rm ghcr.io/marciocar/kg-ssot:<version>só julga. Com-v kg-data:/datae--root /data/graphs --init, grava. -
Ao lado do LibreChat: o perfil "atrás de proxy", na rede do compose e sem porta publicada no host:
# docker-compose.override.yml services: kg-ssot: image: ghcr.io/marciocar/kg-ssot:<version> command: ["--http", "8080", "--host", "0.0.0.0", "--allow-remote", "--identity-header", "X-User-Email", "--secret-env", "KG_MCP_SECRET", "--root", "/data/areas", "--init", "--backend", "versioned"] environment: KG_MCP_SECRET: ${KG_MCP_SECRET} volumes: - kg-data:/data volumes: kg-data:# librechat.yaml mcpSettings: allowedDomains: # a CLOSED list: only the listed domains pass, for every MCP server reached by URL - kg-ssot # the service name in compose # - the domains of the other MCP servers this librechat.yaml already uses mcpServers: kg-ssot: type: streamable-http url: http://kg-ssot:8080/mcp headers: X-User-Email: "{{LIBRECHAT_USER_EMAIL}}" X-Api-Key: "${KG_MCP_SECRET}"Uma lista
allowedDomainsque não está vazia é fechada: só os domínios dela passam, para todos os servidores MCP alcançados por URL. Se o seulibrechat.yamljá tem outros servidores, ponha os domínios deles na mesma lista, senão eles param de funcionar.O
--host 0.0.0.0é o de dentro do container: sem porta publicada, só os containers da rede do compose alcançam o serviço (a rede padrão do compose não éinternal, então o container ainda sai para a internet). SemKG_MCP_SECRET, o container sai com rc 2 e não serve nada. O segredo fica no.envdo compose, nunca nolibrechat.yamlversionado. Como toda variável de ambiente de container, ele aparece nodocker inspect: quem tem acesso ao docker do host o lê, como já lê o.env. - O volume: com um volume nomeado, como no compose acima, o/datajá nasce do usuário da imagem. Com uma pasta do host montada em/data, ela tem de ser do uid 10001 (chown 10001:10001 <pasta>); senão o container sai com rc 2 ePermission denied. - A plataforma: a imagem sai só paralinux/amd64. - O que a imagem não faz: TLS e exposição para fora da rede do compose ficam com o proxy do adotante, como no perfil. - Num repositório: o armazém de disco e o versionado recusam raiz dentro de um repositório git (veja okg_store.py). Dentro do repositório, use--backend git --root ., que faz um commit por gravação no branch atual, fora de qualquer PR (rode-o num branch de trabalho), ou rode sem--rootpara só julgar. - O que fica com o adotante, de propósito: o TLS, o perfil OAuth, cota por usuário e timeout por ferramenta. - Cliente que guarda otools/list: alguns clientes, como o LibreChat, validam os argumentos contra otools/listque guardaram ao subir. Depois de uma atualização que muda oinputSchemade uma ferramenta (um argumento novo, um valor novo num enum), reinicie o cliente, ou ele recusa o argumento novo antes de chamar o servidor. Um adotante já tropeçou nisso e passou a reiniciar o cliente no deploy quando otools/listmuda. - As duas eras do protocolo: o servidor fala as duas, e cada requisição escolhe a sua era. - A era nova,2026-07-28, sem estado: toda requisição traz noparams._metaa versão (io.modelcontextprotocol/protocolVersion) e as capacidades do cliente (io.modelcontextprotocol/clientCapabilities). - O que o servidor implementa:server/discover. - O que todo resultado traz:resultTypee oserverInfono_meta. - As dicas de cache:server/discoveretools/listtrazemttlMsecacheScope, que éprivateno perfil "atrás de proxy", porque a lista depende da área. - Os erros, com o status HTTP da especificação: - falta um campo obrigatório do_meta: -32602, com 400; - versão que o servidor não suporta: -32022, com 400 e, emdata.supported, a mesma lista doserver/discover(as duas eras), por qualquer caminho; uma versão da era antiga pedida no_metatambém dá -32022, com a mensagem de que ela só vale peloinitialize; - método desconhecido: -32601, com 404; - header que falta ou diverge do corpo (MCP-Protocol-Version,Mcp-Method,Mcp-Name, com o sentinela base64): -32020, com 400. - A era antiga (2025-11-25,2025-06-18,2025-03-26,2024-11-05): segue cominitialize, que responde com a versão pedida quando a conhece. O servidor nunca emiteMcp-Session-Id, e GET e DELETE dão 405. - Medido com clientes oficiais: o SDK Pythonmcp2.3.0, que é o v2, na era nova, por stdio e por HTTP; e os SDKs TypeScript 1.30.0, o que o LibreChat usa, e 1.32.1 na era antiga. O CI roda os três.
skills/kg-ssot/: a skill de agente (4.30.0)¶
A release traz uma skill no formato Agent Skills: um SKILL.md que diz a um agente de
código como escrever um grafo que passa, como corrigir pelo código, como consultar e quando ler uma das referências
(o contrato, as correções, o vocabulário de domínio, a aresta para outro grafo, o critério pré-registrado e o gate no
CI). Ela não tem código próprio: aponta para as ferramentas desta página.
- Instale com
npx skills add marciocar/kg-ssot -s kg-ssot, que copia a pasta para o diretório de skills de cada cliente que você escolher (Claude Code, Codex e outros). - No Claude Code, ela é também um plugin:
/plugin marketplace add marciocar/kg-ssote depois/plugin install kg-ssot@kg-ssot. - O que ela rende: num teste pré-registrado de 24 execuções, os agentes com e sem a skill acertaram 12 de 12; com ela, gastaram 45% dos tokens de saída e 54% das chamadas de ferramenta. Sem ela, leram os schemas dentro do pacote e escreveram por tentativa.
kg_query.py: a consulta e a projeção do corpus¶
Lê muitos grafos de uma vez e seleciona nós, sem julgá-los (o veredito é do kg_validate.py). Cada nó sai
projetado: as arestas de saída e de entrada, com a outra ponta resolvida para o label, local ou externa, e se essa
ponta existe no corpus lido. Com --root, o corpus são os .kg.yaml e .kg.json que o git rastreia sob a pasta
(fora do git, todos sob ela), e os caminhos saem relativos a ela, porque é a raiz contra a qual a ponta externa
<caminho>#<id> se resolve.
python3 -I -B vendor/kg-ssot/tools/kg_query.py --root . --open --sort impact # the open queue
python3 -I -B vendor/kg-ssot/tools/kg_query.py --root . --around alvo.kg.yaml#EV_A # the neighborhood of a node
python3 -I -B vendor/kg-ssot/tools/kg_query.py --root . --text "gate base" --type decision --format json
Saída real, na pasta external-corpus da suíte (cortada):
alvo.kg.yaml EV_A evidence/confirmed/DEV imp=3 A evidência sintética que sustenta A.
→ SUPPORTS Q_A A pergunta sintética A.
→ REFUTES valid-corpus-external-refutes-refuted.kg.yaml#EV_A A evidência sintética que sustenta A.
→ REFUTES warn-corpus-external-refutes-live.kg.yaml#EV_A A evidência sintética que sustenta A.
← REFUTES valid-corpus-external-refutes-outgoing.kg.yaml#EV_A A evidência sintética que sustenta A.
← SUPERSEDES valid-corpus-target-exists.kg.yaml (o grafo inteiro)
| Opção | O que faz |
|---|---|
--root DIR ou arquivos |
O corpus. Com --root, o vendor e as fixtures ficam de fora por padrão (vendor/*, fixtures/*, */fixtures/*, docs/materials/* e, desde a 4.20.0, toda pasta com o carimbo .kg-ssot-version); --exclude GLOB (repetível) tira mais caminhos, e --no-default-excludes lê tudo. Um arquivo que você nomeia um a um nunca é excluído |
--id, --id-prefix, --type, --status, --plane, --layer, --file |
Filtros, todos combinados em E |
--text PALAVRAS |
Palavras que o label contém, todas, sem maiúscula nem acento (verificacao casa Verificação, desde a 4.23); --in-narrative procura também na narrative. Quando o texto inteiro é um termo do vocabulário (o pref ou um sinônimo de domínio atual, dobrados, ou a notation, exata), casam também o termo e todo nó que tenha qualquer forma dele como palavra inteira, e o expanded diz qual termo; o sinônimo de caso ou aposentado não expande. Nenhum nó dá um hint, que diz quando o zero vem dos outros filtros, para o zero não passar por "não há" |
--semantic PERGUNTA |
Desde a 4.26, com o extra vector: ordena os nós que passam nos outros filtros pela semelhança com a pergunta (--model, --model-dir; ver o kg_vector.py abaixo). Sozinho, a ordem é a do vetor; com --text, a lista do texto (na ordem do corpus) e a do vetor se fundem por RRF (k=60), com o empate pela posição no vetor. Dê o termo curto ao --text e a pergunta inteira ao --semantic: é o que a bancada mediu (14 de 20 acertos, contra 12 do vetor e 9 do texto). Os filtros valem para os dois, as duas listas são completas, e o --limit só pagina a lista fundida; por isso, no híbrido, o total conta todos os candidatos, e o semantic.text_matches diz quantos casam o --text. O JSON ganha semantic (modo, modelo com revisão e sha256, e de onde veio cada vetor: o índice, embutido na hora, a memória do processo, ou sem texto) e um rank por nó; sem --semantic a saída não muda |
--open |
A fila dos abertos (status open) |
--group-by CAMPO |
Desde a 4.41.0: conta os nós por campo em vez de listá-los (kg-ssot.groups/2 desde a 4.42.0). Veja "Quantos, por campo" e, para label, narrative e o grau, "Vencidos, grau, presença e texto" abaixo |
--edges, --count-distinct CAMPO, --min-count N |
Desde a 4.42.0: as arestas como sujeito, a contagem de distintos e o mínimo. Veja "As arestas como sujeito" abaixo |
--where CHAVE=VALOR |
Desde a 4.39.0: só os nós em que owner, um campo x_* ou (desde a 4.42.0) provenance.<chave> (com ponto para chave aninhada) tem o valor; repetível, e todas têm de valer. Veja "A fila de um dono" abaixo; desde a 4.43.0, também o grau e os operadores ^= e *=, em "Vencidos, grau, presença e texto" abaixo |
--due [--as-of AAAA-MM-DD], --has CAMPO, --missing CAMPO |
Desde a 4.43.0: o que venceu, e se um campo está lá. Veja "Vencidos, grau, presença e texto" abaixo |
--verified-before AAAA-MM-DD |
Os nós sem verified_at, ou conferidos antes da data |
--around ID |
A vizinhança: <caminho>#<id>, ou um id único no corpus; --depth, --edge-type (repetível), --direction out\|in\|both |
--sort corpus\|impact\|attention |
A ordem do corpus, o impacto declarado ou a atenção (impacto × confiança × fator do status × (1 + grau), o valor do radar) |
--limit N e --cursor C |
A página (até 1000) e o cursor da próxima, que vale para a mesma consulta sobre o mesmo corpus |
--fields GLOB |
Chaves do nó que saem em extra, como estão no grafo: --fields 'x_*' |
--narrative |
Inclui a narrative de cada nó |
--format json |
O formato estável kg-ssot.query/1; o kg-ssot.query/2 com --due, que acrescenta due_on e days_late (spec/query.schema.json) |
O grafo que não parseia sai em unreadable, com o código parse.* dele, e não derruba a consulta. O label da outra
ponta de cada aresta sai cortado em 200 caracteres: um nó de label longo citado por milhares de arestas não
multiplica a resposta. rc 0 quando consultou, mesmo sem nó; 2 na entrada quebrada; 3 quando o --semantic não roda
aqui (falta o extra vector ou o modelo), para quem chama cair no --text e dizer isso, nunca em silêncio. No servidor MCP, a mesma consulta
é a ferramenta kg_query, sobre o armazém: a página para no teto de bytes do servidor (com ao menos um nó), e o
next_cursor continua de onde ela parou. Cada chamada relê e reparseia o armazém da área, e num armazém de dezenas
de MB a primeira chamada leva dezenas de segundos; o cache do julgamento vem na 4.19.
A cadeia de transmissão (--chain, 4.33.0)¶
--chain <id> segue uma afirmação até a origem: de cada nó para aquele de onde ele veio, pelo fragmento #<id> do
provenance.source, ou, num nó sem provenance.source, pelas arestas TRACES_TO que saem dele (outro tipo com
--edge-type). Ela
responde quem passou a afirmação para quem, onde a corrente é mais fraca e se ela quebra.
chain: complete
E_RELAY_B confidence=0.7 method=measurement session B checked it against the repo
E_RELAY_A confidence=0.7 method=testimony session A relayed it
D_ORIGIN confidence=0.9 method=testimony the owner decided
weakest link: 0.7 at E_RELAY_B, E_RELAY_A
origin: D_ORIGIN, method testimony, confidence 0.9 (a separate axis)
grade: strict weak, lenient acceptable
- Dois eixos, que nunca se somam: o elo mais fraco é o menor
confidenceentre os repasses; a força da origem é a classe do método dela e oconfidencedela. - O grau:
strong(≥ 0,85),acceptable(≥ 0,60),weak(abaixo). O grau leniente é a faixa do elo mais fraco. O estrito leva também a origem, e uma origem que se apoia emtestimony(ou que não temmethod) conta comoweak, porque ninguém a mediu. - A corrente quebrada (um passo para um nó fora do corpus) termina ali, marcada
found: false, e saiweaknos dois modos. Uma corrente que bifurca sai com um caminho por ramo; um ciclo é cortado e marcado. --format jsondá a mesma projeção, comokg-ssot.chain/1.- O passo pelo
provenance.source: um fragmento vira passo quando o que vem antes do#é vazio, é o caminho do próprio arquivo, é uma URL que termina nometa.iddo grafo, é um arquivo do corpus ou tem a extensão de um grafo. Se o nó que ele nomeia não existe, a corrente sai quebrada. Qualquer outro fragmento (a seção de um README, uma URL externa) é a origem, e nunca cai num nó local com o mesmo id. - O caminho antes do
#é relativo à raiz do repositório, como na referência externa, e a query string sai. Um nó cujosourcenomeia a si mesmo é um ciclo. - O
provenanceprimeiro (4.34.0): um nó comprovenance.sourcesegue só o fragmento dele, e umsourceque não nomeia nó de grafo (uma pessoa, uma URL, um README) faz do nó a origem, quaisquer que sejam as arestas que saem dele. A aresta diz o que o nó responde ou em que se apoia; osourcediz de onde ele veio. Uma decisão selada pelo dono queTRACES_TOa pergunta que responde é a origem, não o primeiro elo da história da pergunta. - Uma origem sem
provenance.sourcesai comdeclared: false, e a saída diz que a corrente pode seguir fora do que o grafo registra. Uma corrente que termina nela saiweaknos dois modos, de qualquer comprimento, e umsourcesó com#não declara nada. As arestas que o provenance de um nó deixa de fora saem nomeadas (edges_not_followed), para que um tipo pedido com--edge-typenunca seja pulado em silêncio. - Uma afirmação repassada de outro repositório grava a ponte no
provenance.sourcecomo a URL daquele grafo, fixada no commit que foi lido (…/blob/<sha>/<caminho>.kg.yaml#<id>), com methodtestimony. A corrente mostra esse elo fora do corpus, visível eweak, nunca como uma corrente de um nó só em silêncio. --with PREFIXO=PASTA(4.35.0) lê esse outro grafo de uma pasta local, e a corrente segue por ele. O prefixo é a URL antes do commit (https://github.com/acme/core/blob). Quando a pasta é um repositório git que tem o commit da URL, o grafo é lido naquele commit (o que foi citado, não o que está lá hoje); senão, é lido da árvore de trabalho, e o elo sai compinned: falsee um aviso. Sem--with, ou com um prefixo que não casa, o elo fica fora do corpus. Um caminho relativo dentro daquele grafo resolve naquele repositório, nunca no seu. O ref é o primeiro segmento depois do prefixo, então um nome de branch com barra (feature/x) não resolve;pinned: truequer dizer "lido no ref da URL", que só é um commit fixo quando a URL traz um sha. Quando o ref existe e o arquivo não está nele, o elo sai quebrado: a árvore de trabalho nunca é lida no lugar. A pasta é só lida: um ref que começa com-é recusado.- Um elo sem
confidencenumérico, ou com um fora de [0, 1], é o mais fraco, mostrado com valor nulo, e o grau leniente saiweak. Os elos contam por arquivo e id, eweakest_link.atdá o arquivo de cada um. - Limites: até 200 passos (além disso,
too_long) e 1000 caminhos (além disso,truncated). Uma corrente quebrada, em ciclo ou longa demais saiweaknos dois modos. - O grau leniente deixa a origem de fora: uma corrente de repasses fortes a partir de uma origem fraca sai
strongno leniente eweakno estrito. Um repasse que se apoia emtestimonynão é rebaixado pela classe, porque oconfidencedele já dá o preço do repasse; a origem é, porque é onde a afirmação nasce.
A corrente é declarada por quem escreve o grafo: a projeção a mede, não prova quem de fato disse o quê.
Desde a 4.43.0, o --chain é repetível, até 50 ids (acima disso, rc 2), e o id repetido conta uma vez. Um id só imprime o
kg-ssot.chain/1, e o id que não se resolve dá rc 2. Mais de um imprime o kg-ssot.chains/1
(spec/chains.schema.json), na ordem pedida: cada entrada traz o kg-ssot.chain/1 inteiro em chain e, em origins,
cada origem com o status do nó. O id que não existe, ou que existe em mais de um grafo, fica na entrada dele com
outcome: not_resolved e o motivo em error, e o lote segue com rc 0. O lote para de abrir cadeias depois de 1000
caminhos, o teto de uma cadeia: as que sobram saem com outcome: not_run, e o truncated fica true. Peça esses ids num
lote menor. O --edge-type vale para o lote inteiro. No servidor MCP, o chain recebe uma lista de ids e, ao lado, só
edge_types, e a resposta acima do teto de bytes do servidor é recusada, com o pedido de pedir menos ids.
A atenção e as decisões a revisitar (4.36.0)¶
Uma decisão é um nó como os outros, e o grafo já guarda a história dela: a decisão nova SUPERSEDES a antiga, a
refutação guarda o nó refutado, e nada é apagado. Três visões tornam essa história uma pergunta que se faz.
--sort attentionordena os nós pelo peso deles no radar:impact × confidence × fator do status × (1 + grau), em que o grau conta as arestas que tocam o nó e o fator do status é 1,0 paraopen,confirmedeunverifiable, 1,3 paradrifted, 0,2 parasuperseded, 0,1 paradonee 0 pararefuted. O empate vai para o id em ordem decrescente. É a mesma fórmula e o mesmo desempate do radar do framework onde o formato nasceu, então os dois dão a mesma fila num mesmo grafo que passa no MUST. O grau fica no escopo do arquivo do nó, como no radar: a aresta conta para as pontas locais do arquivo que a declara, o evento de umtriggertambém conta, e a aresta que chega de outro arquivo não, então a atenção não muda com o--root. Cada nó da página traz a suaattention; a ordem usa o valor sem arredondar.--decision <id>dá a história de uma decisão: o que ela substituiu e o que a substituiu, em toda a linhagem deSUPERSEDES, o que a refuta, o que a apoia e em que ela se apoia (asSUPPORTSque chegam nela, asTRACES_TOeDEPENDS_ONque saem dela, e o passo doprovenance.source; aSUPPORTSque sai dela diz o que ela apoia, não em que ela se apoia), com o status de cada um.--format jsondákg-ssot.decision/1(spec/decision.schema.json).--revisitlista as decisões vivas (nemsupersedednemrefuted) que pedem revisão, pela atenção, cada uma com os motivos:pin-changed(desde a 4.40.0: um apoio, ou o nó que ele cita pelokg-node, a um salto, não bate mais com a versão que fixou, peloprovenance.node_sha256ou peloprovenance.sha256; é a mesma conferência dokg_trace check, e o item trazchanged,pin,source,recordedecurrent),evidence-due(algo em que ela se apoia passou doreverify_every),review-due(a própria decisão passou doreverify_every) econtested(ela recebeREFUTESe segue viva). A decisão com sóreverify_onespera o evento e sai à parte; a decisão com cadência e semverified_atsai emunstamped, e o apoio fora do corpus emunresolved, para nada sumir em silêncio. O apoio refutado ou substituído não é motivo: inferido das arestas, deu 1 aviso útil em 10 no corpus do framework onde o formato nasceu, e por isso a queda aparece no--decision, onde quem lê vê o status de cada apoio.--as-of AAAA-MM-DDmuda a data; o padrão é hoje.--format jsondákg-ssot.revisit/2(spec/revisit.schema.json;/2desde a 4.40.0, pelo motivo novo). A fixação que não se confere vai paraunresolved:pin_malformed(o hash não tem 64 hex minúsculos, ou umnode_sha256não tem o esquemakg-node) oupin_source_missing(a fonte fixada não existe mais) oupin_unchecked(a fonte é uma URL sem--with, um ref que anda, fica fora da raiz ou não é caminho). O nó fixado que sumiu do documento também acendepin-changed, com ocurrentnull. As fixações só se conferem com--root, e o apoio lido de outro repositório pelo--withainda não tem a fixação conferida, como nokg_trace check. É a aprovação de um documento controlado: a decisão é o testemunho, e uma evidência que aSUPPORTSfixa a versão aprovada.
kg-query --root . --open --sort attention --limit 10
kg-query --root . --decision D_RETRY_3
kg-query --root . --revisit
As opções rejeitadas não pedem campo novo: escreva cada opção como um nó, e a decisão escolhida refuta ou substitui as outras, com o motivo na narrativa. Um detector automático de contradições foi medido para esta versão e ficou de fora: ele achava afirmações irmãs, não contradições. A tensão é a refutação que alguém registra, e a atenção diz qual olhar primeiro.
Apoios em outro repositório (4.38.0)¶
O --with PREFIXO=PASTA vale também com --revisit e com --decision, como no --chain desde a 4.35.0: o apoio dado
por uma URL no provenance.source (<prefixo>/<commit>/<caminho>#<id>) é lido no commit fixado, de um clone local. O
prefixo acaba logo antes do commit: no GitHub ele inclui o /blob (https://github.com/acme/core/blob=../core). Sem o
/blob, a referência lida seria blob, que não fixa nada, e o apoio sai unpinned.
- O
--revisitjulga só as decisões do corpus local; um grafo lido de outro repositório é apoio, não decisão a revisitar. O apoio que não se resolve traz o motivo:no_with,not_found,not_pinned_url(uma fonte que não é URL com commit, comodono/repo@ref:caminho),unpinned(lido sem fixação real: o commit não está no clone, a pasta não é repositório git, ou a referência é branch ou tag) ououtside_corpus. O apoio sem fixação nunca é julgado pela árvore de trabalho. - O
upstream_changedlista, à parte e nunca como motivo de revisão, as decisões locais cujo apoio tem outro status, ou sumiu, na ponta do clone: o commit fixado ficou para trás. Inferido das arestas, o apoio caído deu 1 aviso útil em 10 (4.36.0); esta lista diz outra coisa, que a fonte andou desde que foi lida. - O
--decisionmostra cada apoio externo com o status no commit fixado,pinnede, emhead_status, na ponta. A ponta é oHEADlocal do clone: rodegit fetche o atualize antes, ou a lista fica vazia.
A fila de um dono: --where (4.39.0)¶
O --where CHAVE=VALOR deixa só os nós em que a chave tem o valor. A chave é owner ou um campo x_*, com ponto para
uma chave aninhada (x_raci.responsible); um escalar casa pelo texto, uma lista quando um dos itens casa, e o nó sem a
chave não casa. Repita para somar condições, e todas têm de valer. Vale com o --open, com os outros filtros e com o
--sort, e com o --revisit, que então julga só as decisões que casam; a saída JSON ecoa as condições em where, e a
saída humana diz que a lista está filtrada. Com o --revisit, todas as listas (as decisões, on_event, unstamped,
unresolved e upstream_changed) ficam só com as decisões que casam. A ferramenta kg_query do MCP aceita o mesmo
where. Uma chave fora de owner e x_*, ou uma condição sem =, é entrada quebrada (rc 2).
Não é o --where do kg_validate, que mostra de que nó sai cada código. Um número do grafo casa pelo valor como o
Python o escreve (owner: 1e3 casa owner=1000.0), o texto é comparado em NFC, então a mesma palavra acentuada casa
de qualquer jeito que foi digitada, e maiúscula e espaço contam. Uma lista de mapas não é percorrida, e um mapa nunca
casa.
É a fila com que uma sessão abre quando várias sessões dividem um grafo de coordenação, cada uma com o seu owner:
kg-query --root . --open --where owner=kg-ssot --sort attention
kg-query --root . --revisit --where owner=core
A saída JSON não traz o owner sem pedir: o --fields owner o devolve no extra de cada nó, e é assim que se conta a
fila de todos os donos numa consulta só. Uma página traz 50 nós por padrão e até 1000 com --limit; acima disso, siga
o next_cursor. Nó sem owner não está na fila de ninguém; conte-o à parte.
Quantos, por campo: --group-by (4.41.0)¶
O --group-by CAMPO conta os nós que a consulta acha, pelo valor do campo, em vez de listá-los. O campo é file (o
caminho do grafo), node_type, status, plane, layer, owner, um campo x_* (com ponto para chave aninhada, como
no --where) ou meta.<chave>, um campo do meta do grafo do nó (meta.id, meta.x_study_destination). Repita para
agrupar pela combinação, até 5 campos. O campo que falta vale null e forma o próprio grupo; a data sai em ISO, e a
lista ou o mapa sai como o texto JSON dele, de modo que cada nó cai num grupo só e as contagens somam o total. Todos
os grupos saem, sem página, da contagem maior para a menor. Os filtros valem antes. A saída JSON é o
kg-ssot.groups/2 (spec/groups.schema.json; o /1 até a 4.41.0), e o kg-ssot.query/1 não muda. Não combina com --semantic nem com
--cursor, nem com --decision, --chain ou --revisit (rc 2). A ferramenta kg_query do MCP aceita os mesmos
campos em group_by, onde file é o nome do grafo no armazém; os grupos não têm página, então a resposta maior que
o teto de tamanho do servidor é recusada, com o pedido de estreitar os filtros. true e 1 são grupos separados, como no
--where. Alguns valores caem num grupo só com o texto que os escreve: 1 e 1.0 são um grupo; uma data e a mesma
data escrita como texto são um; uma lista ou um mapa e o texto do JSON dele são um; e .nan ou .inf e o texto nan
ou inf são um. Na saída humana, o valor que não é texto sai em JSON (true, 1, null), e o texto que é JSON
válido, vazio, com espaço nas pontas, com dois espaços seguidos ou com caractere de controle sai entre aspas, de modo
que dois grupos diferentes nunca saem iguais.
kg-query --root . --type decision --open --group-by file # decisões abertas por grafo
kg-query --root . --group-by node_type --group-by status # a forma do corpus
kg-query --root . --type evidence --status confirmed --group-by meta.id --format json
Contar é pergunta para o leitor do contrato, não para código sobre o YAML: o leitor pula o vendor e as fixtures, põe em
unreadable o grafo que não parseia em vez de adivinhá-lo e aplica os mesmos filtros de toda consulta. O grau de entrada
de um nó e as outras contagens de arestas estão em "As arestas como sujeito", abaixo.
As arestas como sujeito: --edges (4.42.0)¶
O --edges faz a consulta contar e listar arestas em vez de nós: as internas (edges, escopo internal) e as externas
(external_edges, escopo external), na ordem do corpus. O --where e o --group-by passam a aceitar os campos de
aresta: edge_type, scope, file (o grafo que declara a aresta), meta.<chave> desse grafo, e from.<campo> e
to.<campo>, em que o campo da ponta é id, file, found (se a ponta existe no corpus), node_type, status,
plane, layer, owner, um campo x_* ou provenance.<chave>. A ponta fora do corpus guarda o id e o file e tem
os campos de nó nulos. Sem --group-by, a saída é a lista paginada kg-ssot.edges/1 (spec/edges.schema.json); com
ele, o kg-ssot.groups/2 com subject: edges. Os filtros de nó (--type, --status, --text, --around, --chain e
semelhantes) não combinam com --edges (rc 2): filtre as arestas com o --where. No --where de arestas, a chave que
falta casa null, como no grupo null (no --where de nós, a chave que falta nunca casa). A aresta externa sem from
tem o grafo inteiro como origem: o from.id é null, e o from.found diz se esse grafo está no corpus. A aresta cujas
pontas não são texto fica fora da contagem, como na leitura do grafo pelos outros comandos.
Duas opções de contagem valem com o --group-by, para nós e para arestas:
- O
--count-distinct CAMPOconta, em cada grupo, os valores distintos do campo em vez das linhas; repita para contar combinações distintas (um nó éto.filemaisto.id). O valor null não conta, como nocount(DISTINCT)do Cypher; a combinação com algum null também não conta, como noCOUNT(DISTINCT a, b)do SQL. A contagem pode então ser 0, e ocounteddizdistinct:<campos>. - O
--min-count Ndeixa só os grupos com contagem a partir de N, como oHAVINGdo SPARQL; as contagens deixam então de somar ototal.
kg-query --root . --edges --group-by edge_type --group-by scope # a forma das arestas
kg-query --root . --edges --where scope=internal --group-by to.file --group-by to.id \
--count-distinct from.id --min-count 9 # grau de entrada de 9 origens distintas
kg-query --root . --edges --where edge_type=SUPERSEDES --where from.status=confirmed \
--where to.status=superseded --group-by scope --count-distinct to.file --count-distinct to.id
kg-query --root . --where provenance.locality=web --group-by file # 4.42.0: provenance também nos nós
Desde a 4.42.0, o --where e o --group-by de nós também aceitam provenance.<chave>. Perguntas de dois saltos (uma
evidência que restringe uma decisão que leva a uma pergunta) ainda não saem de uma consulta só. A de ausência (um nó que
não recebe SUPPORTS) sai, desde a 4.43.0, na próxima seção.
Vencidos, grau, presença e texto (4.43.0)¶
O --due não recebe data, ao contrário do kg_validate --due. Ele lista os nós de qualquer tipo cujo verified_at mais reverify_every venceu até o --as-of (o padrão é hoje),
do mais atrasado para o menos e, na mesma data, na ordem do corpus. Cada nó ganha due_on e days_late (0 é o que vence
naquele dia). A conta é a do kg_validate --due, e por isso os dois concordam. O que aquele relatório traz de x_term,
x_mirror e x_doc_cycle segue lá, e também a lista dos nós que só têm reverify_on. O nó com reverify_every e
sem verified_at legível nunca vence, e o hint diz quantos são: isso inclui o verified_at com hora, que não é data no
contrato. A cadência fora da forma também nunca vence, nos dois leitores. Em grafo que já reprova no contrato, as duas
listas podem diferir: o id repetido sai uma vez aqui, o primeiro nó, e o id numérico não sai aqui, enquanto o
kg_validate --due lista cada linha. O --due é um filtro: combina com os outros e com o
--group-by, mas não com --sort impact, --sort attention nem --semantic, que pedem outra ordem (rc 2). A saída JSON é
o kg-ssot.query/2, o /1 mais esses campos; toda outra consulta segue em /1.
O --where de nós tem estas formas, e todas as condições têm de valer:
| Forma | Casa |
|---|---|
CHAVE=VALOR |
O texto do valor: x_n=12 casa o número 12, x_b=true o booleano. A lista casa quando um item dela casa. A chave que falta não casa |
CHAVE^=VALOR |
O texto que começa com o valor |
CHAVE*=VALOR |
O texto que contém o valor |
degree.in=N, degree.out=N, degree.total=N |
A contagem das arestas internas do nó, um número inteiro |
degree.in.TIPO=N (também out e total) |
A mesma contagem, de um tipo de aresta |
A chave é owner, um campo x_* (com ponto para chave aninhada), provenance.<chave> ou um grau; label^= é recusado
(rc 2), porque o --text e o --id-prefix respondem a isso. O --where do --edges aceita ^= e *= nos campos de
aresta, como from.id^=D_, e lá o grau não existe. A chave que ainda termina em ^ * $ ! < > ~ | depois de lido o operador
é recusada (rc 2), em nós e em arestas: x_n!=12 não é "diferente de", e esse operador não existe.
- Operadores de texto. Distinguem maiúsculas, com os dois lados em NFC. Só texto casa:
null, chave que falta, número, booleano e.nannunca casam, como noSTARTS WITHe noCONTAINSdo Cypher. A data é lida como o texto que a escreve, como num.kg.json, e por issox_when^=2026-11casa. Numa lista, basta um item de texto, que é a regra do=aqui (no Cypher, a lista dá null). O valor vazio casa qualquer texto e nada mais. A chave termina no primeiro=, e depois dele o valor é literal:x_f^=a=bprocuraa=b, eowner=^aprocura^a. - Grau. Conta as arestas internas do próprio grafo do nó, linha por linha: a aresta repetida conta de novo, o laço conta
em
ine emout, e por isso duas vezes emtotal, e a aresta cuja outra ponta falta conta para a ponta que existe. A aresta externa não conta, nem a que vem de outro grafo. Não é o grau da fórmula da atenção, que segue o radar e conta outras coisas. Se um grafo repete um id, as linhas se somam no primeiro nó, o único que a consulta guarda. O valor é um número inteiro com=:degree.in=x,degree.in=01edegree.in^=1são recusados (rc 2). O tipo de aresta que nenhuma aresta interna do corpus tem conta 0, e ohintavisa, porque o zero de um erro de grafia não pode passar por "não há". O mesmo vale para o--group-bye o--count-distinct. No--revisit, cuja saída não temhint, esse tipo é recusado (rc 2). Toda resposta com o grau diz nohintque a aresta vinda de outro grafo não conta. Assim,degree.in.SUPPORTS=0quer dizer sem aresta internaSUPPORTSno próprio grafo, e nada além disso: oSUPPORTSde outro grafo não conta, e--edges --where edge_type=SUPPORTS --where scope=externallista esses, para completar a resposta. - Presença. O
--has CAMPOe o--missing CAMPOaceitam qualquer campo do nó, com ponto para chave aninhada, até 10 de cada. O campo que falta e onullsão o mesmo: faltam. O texto vazio, o0, ofalse, a lista vazia e o mapa vazio têm valor, como noIS NOT NULLdo Cypher. O caminho não entra em lista. O mesmo campo nos dois é recusado (rc 2), e também todo caminho que começa comdegree, porque o grau é derivado e não é campo do nó. O caminho que nenhum nó do corpus tem com valor ganha umhint, porque o--has trceresponderia 0 e o--missing trce, o corpus inteiro. - Em que consultas. O
--due, o--hase o--missingfiltram as consultas de nós: com--edges,--decision,--chainou--revisit, dão rc 2. O--wherecom grau ou operador também vale no--revisit. - Agrupar por texto. O
--group-byaceita tambémlabelenarrative, com o texto inteiro, etruee1ficam em grupos separados. Com--min-count 2, acham os nós com o mesmo texto. O grupo une texto em NFC e em NFD numa chave NFC, e o nó segue como está. O nó sem o campo cai no gruponull, como em todo--group-by: o--has narrativeo deixa de fora. - Campos do agrupamento, em nós.
file,node_type,status,plane,layer,owner,label,narrative, um campox_*,provenance.<chave>,meta.<chave>, edegree.in,degree.outedegree.total, com o tipo de aresta opcional.
kg-query --root . --due --type evidence --where owner=ana # o que venceu, na mesa de um dono
kg-query --root . --due --as-of 2027-01-01 --group-by file # o que vai vencer, por grafo
kg-query --root . --type decision --status done --where degree.in.SUPPORTS=0 # decisões concluídas sem aresta interna SUPPORTS no próprio grafo
kg-query --root . --group-by degree.total # o histograma do grau
kg-query --root . --type evidence --missing trace --has provenance.source # evidência com fonte e sem trace
kg-query --root . --group-by label --min-count 2 # nós com o mesmo label
kg-query --root . --edges --where from.id^=D_ --group-by edge_type # o que sai das decisões, por um prefixo
A ferramenta kg_query do MCP aceita as mesmas formas em where e group_by, mais due, as_of, has e missing, com as
mesmas recusas nas mesmas palavras.
kg_diff.py: o diff por id entre revisões¶
Compara dois grafos, ou todos os grafos que mudaram entre duas revisões do git, alinhando os nós pelo id e as
arestas pelas pontas e o tipo. O que só existe de um lado foi acrescentado ou removido, e o que existe nos dois sai
com cada campo que mudou, antes e depois. A ordem dos nós no arquivo não conta; uma troca de id é remoção mais
acréscimo; uma lista dentro do nó é comparada inteira; a aresta que troca de tipo com as mesmas pontas sai como
mudança de edge_type. A revisão é lida pelo git show, sem checkout. Não é patch: o diff informa, não se aplica.
python3 -I -B vendor/kg-ssot/tools/kg_diff.py notice.kg.yaml erratum.kg.yaml
python3 -I -B vendor/kg-ssot/tools/kg_diff.py HEAD~1:docs/x.kg.yaml HEAD:docs/x.kg.yaml
python3 -I -B vendor/kg-ssot/tools/kg_diff.py --from origin/main --ignore verified_at # everything that changed on the branch
Saída real, entre dois casos da suíte:
valid-node-touched-only-as-target.kg.yaml (modificado)
~ meta.id: valid-chain → valid-node-touched-only-as-target
+ C_ALVO Nó sintético C_ALVO.
- C_B Nó sintético C_B.
+ aresta EV_A -SUPPORTS-> C_ALVO
- aresta C_B -SUPPORTS-> EV_A
· 1 grafo(s): 1 modificado(s), 0 acrescentado(s), 0 removido(s), 0 não lido(s); nós +1 -1 ~0; arestas +1 -1 ~0
O campo que sai de um caminho e entra noutro com o mesmo valor, num nó, numa aresta ou no meta, aparece como
renomeação (x_location → x_storage (renomeado)) quando o valor não é trivial e é único dos dois lados. No JSON, as
duas entradas ficam, cada uma com o valor do seu caminho, ligadas por renamed_to e renamed_from; o texto humano e
o resumo por campo mostram uma linha só. Com --from e sem caminhos, o vendor e as fixtures ficam de fora (também uma
pasta fixtures/ na raiz e, desde a 4.20.0, toda pasta com o carimbo .kg-ssot-version em qualquer dos lados), como
no kg_query: uma atualização do vendor muda as fixtures da suíte às centenas.
--exclude e --no-default-excludes valem do mesmo jeito. O meta sai junto com as outras chaves da raiz, os valores
se comparam na forma JSON do contrato, um arquivo renomeado sai como remoção mais acréscimo, e sem --to (ou com
--to WORKTREE, desde a 4.20.0) o lado de depois é a árvore de trabalho, e entram também os grafos novos ainda não
adicionados ao git. WORKTREE:CAMINHO lê o arquivo da árvore de trabalho, a partir da raiz do repositório como o
REV:CAMINHO e nunca fora dele (o ./ e o ../ do git, relativos ao diretório atual, não valem), para revisar uma
edição antes
do commit (HEAD:docs/x.kg.yaml WORKTREE:docs/x.kg.yaml); uma branch chamada WORKTREE se lê como
refs/heads/WORKTREE. O lado que não se lê (não é UTF-8, ou não parseia) sai em
unreadable e conta como diferença; a revisão que começa com - é entrada quebrada. Num argumento, o caminho que
existe no disco vence a leitura REV:CAMINHO.
--ignore CAMPO (repetível) tira um campo da comparação, com tudo o que fica abaixo dele (provenance tira os três;
no meta, meta.date): o verified_at que um gerador recarimba a cada execução esconde o sinal, e o resumo conta os
nós por campo mudado para mostrar isso. --format json dá o kg-ssot.diff/1 (spec/diff.schema.json). rc 0 sem
diferença, 1 com diferença, 2 na entrada quebrada.
kg_vocab.py: o vocabulário de domínio¶
Lê os termos do perfil x_term (capítulo 3) de um corpus, com --root ou com os arquivos, e faz quatro coisas: lista,
resolve um texto ou um código para o termo, exporta e confere contra eles os códigos escritos na prosa.
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py terms --root . --as-of 2027-05-01 # termos, sinônimos, o que vence
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py lookup "codigo de barras" --root . # o termo por trás de um texto
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py lookup "cod barras" --case "edital 12" --root .
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py export --to skos --root . > vocab.ttl
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py export --to ossie --root . > vocab.ossie.json
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py lookup rodada:O1 --root . # um código, qualificado pelo esquema
python3 -I -B vendor/kg-ssot/tools/kg_vocab.py lint --root . --baseline .kg-ssot/vocab-lint.json --external soc2='CC[0-9]+'
- O
lookupcompara sem maiúscula, acento nem espaço repetido. Casa o termo preferido ou um sinônimo. O sinônimo com escopo de caso só casa com--caseigual ao caso dele. O sinônimo aposentado ainda casa, marcado como aposentado, e o termo substituído vem com os termos que o substituem. rc 1 quando nada casa. - O
termsmostra quando cada sinônimo vence (verified_atmaisreverify_every, na data do--as-of, hoje por padrão). O sinônimo aposentado nunca vence. - O
export --to skosgrava Turtle em SKOS-XL com os nomes da ISO 25964 (iso-thes). O conceito ganhaskos:prefLabel, maisskos:altLabeleskos:hiddenLabelpara os sinônimos atuais de domínio, para um leitor de SKOS simples enxergá-los. Cada rótulo vira umskosxl:Labelcom a forma literal e a proveniência (dc:source,kg:locator,kg:sourceVersion,kg:verifiedAt,kg:reverifyEvery). O sinônimo de caso é ligado porkg:caseLabel, comkg:scopeekg:case, e não porskosxl:altLabel, que implicaria umskos:altLabelválido para o conceito inteiro. Um escopo que não se lê conta como um caso que nunca casa. O aposentado sai comoisothes:status "retired"e umaskos:historyNote. O sinônimo que repete um literal já dito no conceito vai porkg:repeatedLabel, e o conceito nunca tem o mesmo rótulo duas vezes. O termo substituído ganhadct:isReplacedBy. O IRI do conceito é<base><meta.id>#<id>, o mesmo que okg_prov_export.pydá ao nó (--base,urn:kg-ssot:graph:por padrão); semmeta.id, o caminho relativo dado, ou o nome do arquivo, nunca um caminho absoluto. - O
export --to ossiegrava um item por termo vivo para o Apache Ossie:name(o termo preferido),description,ai_context.synonyms(os sinônimos atuais de domínio) e uma entrada decustom_extensionsdoKG_SSOTcom a proveniência de cada sinônimo, porque o Ossie não tem campo para ela. Você junta cada item à entidade do seu modelo semântico (dataset, field ou metric) pelo nome. - O esquema e o código (4.23, contrato v4.14). O
termsdá a cada termo oschemeefetivo (ox_term.schemedeclarado, ou o grafo) e oscheme_declared. Olookupcasa também umanotation, exata (código é código:o1não éO1), sozinha ou qualificada comoesquema:notation. Oexport --to skosescreve oskos:inSchemede cada conceito e cada esquema uma vez comoskos:ConceptScheme. - O
lintconfere os códigos da prosa contra o vocabulário. Ele lê o label e a narrativa de todo nó, acha os códigos por uma regex (--pattern; a padrão pega de uma a três maiúsculas e de um a três dígitos, comoA7,O1eS14, ercom dígitos, comor16; não pegaREGRA 87,SAC-234nem ids de nó, que têm mecanismo próprio) e aponta: vocab.notation-duplicate: a mesma notation em dois termos vivos do mesmo esquema, no corpus inteiro;vocab.code-unresolved: um código que não é notation de nenhum termo vivo (umesquema:códigoprocura só naquele esquema);vocab.code-ambiguous: um código sem esquema que é notation em dois ou mais esquemas do próprio grafo ou, sem nenhum do próprio grafo, em dois ou mais esquemas do corpus. Escrevaesquema:código.
Um código resolve nesta ordem: um termo do próprio grafo, depois um sistema de fora, depois o corpus. O esquema de um
esquema:código é um slug colado antes do código, com borda de palavra (my_sa:T7 e urn:kg:E5 não são
qualificados). O esquema declarado e o esquema do grafo (o meta.id) dividem um espaço de nomes, aqui, no export
SKOS e nos dois leitores. As regras da repetição são as do leitor, e o código do próprio termo resolve no label dele.
O --external soc2='CC[0-9]+' (repetível) declara os códigos de um sistema de fora (SOC 2, CVE, ISO), que resolvem
sem termo. A regex que casa o texto vazio é recusada. A catraca é a do gate: --baseline <arquivo> --update grava os achados de hoje, e daí em diante
--baseline <arquivo> reprova só pelo achado novo ou acima da contagem da base, para os códigos que já existem não
travarem e os novos nascerem com nome; a base gravada com outro --pattern é recusada. A ocorrência que a base já
tinha no outro estado do mesmo código (sem termo ↔ ambíguo, mesmo arquivo e código) sai como reclassified e não
reprova: nomear o primeiro termo de um código que colide muda o estado das menções antigas, e quem nomeia não paga
por elas. rc 1 com achado que reprova ou com grafo que não se lê, 2 com entrada quebrada.
- Com --format json, o terms, o lookup e o lint usam o formato kg-ssot.vocab/1 (spec/vocab.schema.json). A ferramenta
não julga: o x_term fora do perfil é lido no que dá para ler, e o SHOULD acusa o resto.
kg_criterion.py: o critério pré-registrado (4.21.0)¶
O leitor já avisa, entre nós do mesmo grafo, o critério que não existe, o sha256 citado que não é o da versão e o
resultado medido depois de ver o dado (capítulo 3, x_criterion e x_measured_under). Ele não calcula hash nem lê
corpus, para que um leitor JavaScript fique sem criptografia. Esta ferramenta faz o resto, a partir da raiz do
repositório:
python3 -I -B vendor/kg-ssot/tools/kg_criterion.py digest --text "Passes with 9 of 10 replicas."
python3 -I -B vendor/kg-ssot/tools/kg_criterion.py digest --ref criteria/a7.tsv # o arquivo inteiro
python3 -I -B vendor/kg-ssot/tools/kg_criterion.py digest --ref data/judge-verdicts.json#CU1 # um item de lista
python3 -I -B vendor/kg-ssot/tools/kg_criterion.py check --root .
digestimprime osha256a escrever nox_criterion. As formas:text: UTF-8, com CRLF e CR sozinho trocados por LF, e sem o espaço (U+0020) no fim de cada linha. O TAB fica.refsem fragmento: os bytes do arquivo com CRLF trocado por LF, e mais nada. Um TSV guarda as colunas vazias, com TAB no fim da linha.refcom fragmento: o item na forma canônica JCS (RFC 8785), de um arquivo.json,.yamlou.yml. O fragmento é um JSON Pointer (#/criteria/0) ou oidde um item de lista (#CU1), único no arquivo.checkconfere cadax_criterioncom exatamente um derefetextcontra ele:integrity.criterion-digestquando osha256não bate;integrity.criterion-ref-unreadablequando o arquivo, o fragmento ou um valor não se lê: um NaN no JSON, um inteiro além de ±(2^53−1), uma chave repetida, uma chave de mapa YAML que não é texto, ou um caminho que não é arquivo comum ou passa de 64 MiB;integrity.criterion-malformedquando a versão não se confere: osha256não é texto (um hash só de dígitos, sem aspas, vira número), ou não há exatamente um derefetext, em texto. Ox_measured_underque não é um mapa com uma lista de citações em mapa, e a citação de outro grafo cujosha256não é texto (mesmo quando esse grafo não foi dado), saem do mesmo jeito. Desde a 4.21.1; antes, o check os pulava.- Citações de outro grafo. O
checksegue também cada citação<caminho>#<id>de umx_measured_under, pela mesma regra do leitor:integrity.criterion-unknown,integrity.criterion-sha-mismatch,integrity.measured-post-hoceintegrity.measured-order-unknown. - O que ele lê. Só com o
--root, ochecklê os grafos que o git rastreia sob ele, sem o vendor. Com arquivos, lê só eles, cada um pelo caminho relativo ao--root(a pasta atual, sem ele), que é também de onde parte cadaref, entãog/a.kg.yaml#C1acha o arquivo como quer que ele tenha sido digitado; a citação de um grafo que não foi dado é contada como sem conferir, não como achado, e o--excludedado vale para os arquivos também. - Caminhos. O
refnão pode sair da raiz. O último#separa o caminho do fragmento, e o fragmento vazio (a.json#) é o documento inteiro. Um.yamlpede um mapa na raiz. A marca de ordem de bytes no começo do--text-filenão faz parte do texto. - Saída e rc.
--format jsondá okg-ssot.criterion/1(spec/criterion.schema.json). rc 0 sem achado, 1 com achado ou com grafo que não se lê, e 2 na entrada quebrada. - O que ele não confere. O
checkconfere que cada versão bate hoje com o texto dela, não o histórico: editar um critério no lugar e escrever osha256novo passa, e só um resultado que cite osha256antigo mostra a troca. Revise nokg_diffumx_criterion.sha256que mudou (ele aparece como campo mudado do nó): uma régua nova é uma emenda, um nó novo comamends. - No CI. O leitor não calcula hash, então o critério editado no lugar sem
sha256novo só é pego aqui: se os seus grafos usamx_criterion, ponhapython3 -I -B vendor/kg-ssot/tools/kg_criterion.py check --root .ao lado do gate.
kg_audit.py: o pacote de auditoria por pergunta (4.22.0)¶
O auditor pede o pacote por pergunta: a resposta, as afirmações, a evidência com data, a lacuna e o dono, numa planilha.
O grafo já guarda tudo isso; esta ferramenta o dispõe, uma linha por item de questionário, em CSV, XLSX ou JSON. Ela
não julga a forma (isso é do kg_validate.py) e não escreve nada no grafo.
python3 -I -B vendor/kg-ssot/tools/kg_audit.py --root . --questionnaire "Q-2026" --as-of 2026-10-23
python3 -I -B vendor/kg-ssot/tools/kg_audit.py --root . --format xlsx --output pacote.xlsx
python3 -I -B vendor/kg-ssot/tools/kg_audit.py --root . --format csv --view evidence > evidencia.csv
- O item é o par (questionário, número) que o
x_questionnairenomeia (nameequestions). Quem entra: - a pergunta: o nó
questioncom o perfil; - a resposta: o nó (não
question) com o perfil do item, e o nó sem perfil ligado à pergunta porDEPENDS_ON, numa direção ou na outra (as duas convivem nos grafos reais; a resposta sai uma vez). O nó com perfil já disse a que item responde, então uma aresta nunca o puxa para outro item; - a evidência: o nó com
SUPPORTSpara uma resposta, ou comSUPPORTSouTRACES_TOpara a pergunta, para um pedido ou para um requisito de evidência do item, local ou de outro grafo (external_edges); uma pergunta, um pedido ou um requisito nunca é evidência; - a contradição: o nó com
REFUTESpara a resposta ou a pergunta. Só a resposta viva traz evidência e contradição: asupersededou arefuted(uma refutação já reconciliada) não fecha nem abre lacuna do item; - o pedido de evidência (
role: evidence_request) e o requisito de evidência (role: evidence_requirement, "que evidência este item exige", em coluna própria): nunca são tomados por pergunta nem por resposta. Cada evidência diz de que pedido veio (o pedido para o qual ela aponta porSUPPORTSouTRACES_TO). O item que só um pedido ou um requisito cita também sai, com as lacunas do que falta. - As lacunas são calculadas, nunca escritas à mão:
no-answer: sem resposta viva (a respostarefutedousupersededentra na lista, com o status, mas não conta);contradicted: umREFUTESde um nó que não érefutednemsuperseded;no-confirmed-evidence: nenhuma evidência comstatus: confirmed(a evidência não confirmada entra na lista, mas não fecha lacuna);evidence-unstamped: evidência confirmada semverified_atválido;evidence-cadence-invalid: evidência confirmada cujoreverify_everynão é duração, e que por isso nunca venceria;evidence-due: evidência confirmada cujoverified_atmais oreverify_everycai no--as-ofou antes (hoje, por padrão);verdict-open: sem veredito vivo, ou um que não émeetsnemnot_applicable.- Nada some em silêncio. O nó cujo
x_questionnairenão temnameou uma listaquestionsnão vazia, o nó comrolefora do enum e o número dequestionsque não é texto (questions: [7]sem aspas) não entram: saem emskipped, no próprio pacote (a planilha Pulados do XLSX, o--view skippedno CSV, o JSON), no stderr e na listagem humana, e o--fail-on-gapdá rc 1 com eles. - As suas colunas.
--column <chave>traz uma chave do nó de resposta (umax_sua, como a lista de cards de remediação) como mais uma coluna. Uma lista vira itens separados por;. - Os formatos.
csv: as perguntas (uma linha por item) ou, com--view evidence, a evidência (uma linha por item e evidência, com os problemas do carimbo) ou, com--view skipped, o que foi pulado, em UTF-8 com a marca de ordem de bytes para o Excel ler. A célula que começa com=,+,-,@, tab ou retorno de carro ganha um apóstrofo na frente, para nunca rodar como fórmula (a regra da OWASP contra injeção em CSV).xlsx: as três planilhas (perguntas, evidência, pulados) num arquivo, escrito só com a biblioteca padrão, todo valor como texto, o cabeçalho congelado e a célula cortada no teto do Excel, de 32.767 unidades UTF-16. O mesmo pacote dá os mesmos bytes.json:kg-ssot.audit/1(spec/audit.schema.json), com tudo aninhado.human: uma listagem curta para o terminal.- Saída e rc. rc 0 quando exportou, 1 quando um grafo não se lê (o pacote ficaria incompleto em silêncio) ou, com
--fail-on-gap, quando um item tem lacuna ou algo foi pulado, e 2 com entrada quebrada. Oxlsxpede--output. Dê o corpus com--root: a aresta para outro grafo se resolve pelo caminho a partir da raiz.
kg_trace.py: o trace que não resolve (4.25.0)¶
O trace: de um nó diz onde mora o "porquê" dele. O leitor confere que ele está lá, nunca que ainda aponta para algo:
o leitor não toca o disco, então roda em qualquer lugar, inclusive em JavaScript. Um trace: que aponta para um
arquivo movido ou renomeado passa no gate e engana quem volta a ele. Esta ferramenta confere a âncora a partir da raiz
do repositório, como o kg_criterion.py confere um sha256.
python3 -I -B vendor/kg-ssot/tools/kg_trace.py check --root .
python3 -I -B vendor/kg-ssot/tools/kg_trace.py check --root . --external-root memory/ --format json
- O que ele lê. Os
.kg.yamle.kg.jsonque o git rastreia sob a raiz, fora do vendor, dedocs/materials/e das pastas de fixture (fixture/,fixtures/,__fixtures__/,testdata/,__snapshots__/, em qualquer nível). O--excludesoma padrões, e o--no-default-excludestira os padrões. Um trace que não é texto conta comonot_text; um nó com id repetido ou fora de texto, que o gate já reprova no MUST, não é lido aqui. Se os seus grafos ancoram numa pasta de fora do repositório (a memória de uma sessão), o check fica vermelho até que você a declare com--external-root. Sem nenhum grafo lido, ele avisa. - O que é julgado. O texto do trace até o primeiro
:ou#(arquivo:linha,arquivo#seção), e só quando parece caminho do repositório. Fora de julgamento, e contados à parte, nunca em silêncio: absolute: um caminho absoluto ou uma URL (outra máquina ou rede);external_root: um prefixo que você declara com--external-root(uma pasta que mora fora do repositório);not_path: um caractere fora de[A-Za-z0-9_./-](um comando, prosa), um nome sem barra (palavra solta, chave de configuração), ou o primeiro segmento com ponto e sem começar por ponto (um hostname,support.example.com/...).
Um nome sem barra só entra quando é um arquivo rastreado na raiz do repositório com o nome exato (ARCHITECTURE.md), e
nenhum arquivo de mesmo nome fica ao lado do grafo nem na pasta pai dele. ./x e x são o mesmo alvo.
- Onde resolve. Na raiz do repositório, na pasta do grafo ou na pasta pai do grafo (esta só quando o grafo não está
na raiz). Um alvo que sai da raiz (../irmão/x) não resolve: o clone do CI não o tem.
- Saída e rc. Cada alvo que não resolve é integrity.trace-target-missing. --format json dá kg-ssot.trace/1
(spec/trace.schema.json). Desde a 4.25.1 ele lista também resolved_targets: para cada trace que resolve, o
arquivo e o id do nó, o alvo julgado, o path do que o check viu existir, relativo à raiz e com os links seguidos
(para que um link seguido de .. não aponte para outro arquivo), e o kind, file ou dir, que é o que um índice
de leitura precisa. Quando o trace passa por um link, o path é o destino real, não o texto (no Windows o sistema resolve o .. no
texto antes do link, e link/../x é julgado como x); o trace que cai na
própria raiz é o diretório .; o path nunca termina em barra, nem no diretório (o kind diz que é um); e o path guarda a caixa que o sistema de arquivos devolve. O campo é aditivo, e a saída da 4.25.0 segue válida. rc 0 quando todo trace
julgado resolve, 1 com um alvo faltando ou um grafo que não se lê,
2 com entrada quebrada, e 3 quando nenhum grafo foi lido (desde a 4.27: o corpus vazio não é "todo trace resolve"
para um CI que só olha o rc). Estas regras vêm do resolvedor de um adotante medido em milhares de traces; cada corte foi
pago por um falso positivo.
O locator JSON (4.35.0)¶
Quando um nó aponta para dentro de um arquivo JSON, o provenance.locator dele pode ser um JSON Pointer em forma de
fragmento (#/color/brand/primary, RFC 6901), e o kg_trace check o segue dentro do arquivo. Assim, um token ou uma
chave que some do JSON é pego, e não só o arquivo.
- O que é julgado. Um
provenance.locatorque é#ou começa com#/, contra um arquivo.jsondo repositório: o caminho doprovenance.sourcequando ele é caminho do repositório, senão o dotrace:. Um caminho de fonte que não existe mais já éintegrity.trace-locator-missing: o locator nunca é julgado em outro arquivo. Na fonte, um nome solto com extensão (tokens.json) é caminho, porque a fonte não é prosa; uma fonte que sai da raiz do repositório (../x.json) é contada comooutside_root, sem julgamento. O fragmento perde o percent-encoding, depois~1vira/e~0vira~; objeto se segue por chave e lista por índice decimal sem zero à esquerda. - Não julgado, contado: o locator que não é ponteiro (
not_pointer), a fonte por URL ou caminho absoluto (absolute) e o ponteiro sem arquivo.jsondo repositório para ler (no_json_file). - Saída e rc. O ponteiro que não resolve é
integrity.trace-locator-missing, e o JSON que não se lê éintegrity.trace-locator-unreadable; os dois dão rc 1. A saída JSON ganhalocators, aditivo e fora dorequired, então a saída da 4.25 segue válida. Ponha o caminho também notrace:: é ele que diz que o próprio arquivo sumiu.
O locator pelo esquema, e o hash da fonte (4.37.0)¶
Quando um nó declara provenance.scheme ou provenance.sha256 (contrato v4.17), o kg_trace check abre a fonte e os
confere.
- A fonte. O arquivo do repositório que o
provenance.sourcenomeia ou, com--with PREFIXO=PASTA, o arquivo que uma URL nomeia no commit da URL (<prefixo>/<commit>/<caminho>), lido de um clone local do outro repositório comgit show. A pasta tem de ser a raiz de um clone git, e a referência da URL, o nome de um commit: o commit que o clone não tem, ou a pasta que não é a raiz de um clone, émissing_source, e o nome de branch ou de tag (main,HEAD) éunpinned, porque anda. Uma fonte fixada nunca é conferida contra outra versão. A URL sem--withque case é contada comoabsolute. - O hash. O
provenance.sha256contra o conteúdo da fonte como o git o guarda: o arquivo rastreado e sem mudança desde o índice é lido do blob, então um checkout que troca as quebras de linha (core.autocrlf, o padrão do git no Windows) dá o mesmo hash que no Linux, e o mesmo degit show <commit>:<caminho> | sha256sum. O arquivo com mudança, ou fora do git, é lido do disco. A diferença éintegrity.trace-sha256-mismatch, a fonte mudou depois de lida. - O locator.
text(o intervalo de linhas ou de caracteres existe, com os caracteres em code points e cada quebra de linha contada como um, como pede a RFC 5147),csv(a linha, a coluna ou a célula existe, a coluna da célula contra a linha dela),quote(o texto exato aparece, com os espaços em sequência contados como um),kg-node(o nó existe no grafo) ejson-pointer(ele resolve numa fonte.json, nas duas formas da RFC 6901). Fora do alcance éintegrity.trace-locator-out-of-range; a citação que não está lá,integrity.trace-quote-missing. Não julgado, e contado, nunca como ok:pdf,mediaehtml(form_only, julgados pela forma no schema), o locator fora da gramática do esquema, o scheme fora da tabela ou osha256fora da forma (malformed, que o schema já alerta), o JSON Pointer numa fonte que não é JSON (not_json) e a fonte que não é UTF-8 (not_text). O ponteiro julgado aqui não é julgado de novo emlocators; o que não pôde ser julgado aqui (a fonte que sumiu, a âncora notrace:) segue julgado lá, como na 4.35.0. - Saída e rc. Cada falha dá rc 1. A saída JSON ganha
schemes, aditivo e fora dorequired.
O hash de um nó citado, e o digest (4.38.0)¶
Com scheme: kg-node, o provenance.node_sha256 (contrato v4.18) é comparado com o hash canônico do nó citado, lido no
commit fixado: a diferença é integrity.trace-node-sha256-mismatch, e a saída JSON conta node_sha256_checked. Uma
mudança em outro nó do mesmo arquivo não mexe nele. O kg_trace.py digest ARQUIVO#ID... imprime o hash a registrar; o
leitor JavaScript exporta nodeCanonical(doc, id), a mesma string canônica, para o SHA-256 da plataforma. O hash cobre o nó citado inteiro, verified_at incluído, então recarimbar o nó citado
derruba as pontes para ele, e é para isso que ele serve. O nó citado que se repete, ou que não tem forma JCS (um número
de valor inteiro fora de ±(2^53−1)), não tem hash a conferir e conta como not_canonical. Quando a ponte leva os dois
hashes, o sha256 do arquivo é conferido antes e a mudança num vizinho ainda a derruba: numa ponte kg-node, deixe só
o node_sha256. O digest lê a árvore de trabalho sob a raiz; para uma fonte fixada, leia antes o arquivo no commit
(git show <commit>:<caminho>).
Com --format json (4.39.0), o digest devolve kg-ssot.digest/2 (spec/digest.schema.json; /2 desde a 4.40.0):
um resultado por referência, com o node_sha256 e o status ok, not_found, file_missing, unreadable ou
no_canonical_form, e o mesmo código de saída do texto. O node_sha256 vem com ok e é null nos outros. Desde a
4.40.0, o arquivo que não existe sai file_missing (no texto, ARQUIVO AUSENTE), e o que não é UTF-8 sai
unreadable com o code encoding.not-utf8 (ou io.unreadable, quando existe e não se lê): cada um é resultado
da própria referência, e o lote segue. Desde a 4.39.1, o grafo que não parseia sai unreadable, com o code do leitor, e não mais
not_found: o nó não foi procurado, então não está ausente. Leia o valor pelo campo, em vez de partir a linha de texto. Só a chamada quebrada para o lote antes de qualquer
resultado, nos dois formatos, com rc 2: um caminho fora da raiz, ou uma referência sem ARQUIVO#ID.
O checador de projeção (kg_trace.py projection, 4.35.0)¶
Uma página de contexto em Markdown (negócio, técnico, design) pode declarar que é projeção de grafos: o
projection_of do frontmatter lista os grafos, e a página cita ids de nó entre crases. O checador torna essa
disciplina verificável.
python3 -I -B vendor/kg-ssot/tools/kg_trace.py projection --root .
python3 -I -B vendor/kg-ssot/tools/kg_trace.py projection --root . --with https://github.com/acme/core/blob=../core
---
projection_of:
- docs/graph/product.kg.yaml
- https://github.com/acme/core/blob/<sha>/docs/graph/core.kg.yaml # outro repositório, fixado no commit
projection_ignore: # nomes entre crases que não são id de nó
- BUILD_TOKEN
---
- O que é lido. Todo
.mdrastreado cujo frontmatter temprojection_of, ou as páginas que você nomear. A página nomeada semprojection_ofconta comono_projection; na varredura, o frontmatter sem ele nem é lido. - O que conta como id citado. Texto entre crases na forma de id (
D_ALGO,E_X_1027), na prosa ou dentro de um bloco de código. Um id em texto corrido, fora de crases, não é lido; uma página que declaraprojection_ofe não cita id entre crases sai emuncited, com um aviso, porque nada foi conferido. As palavras do próprio contrato (tipos de aresta comoTRACES_TO, valores de enum) e os nomes doprojection_ignoresão contados à parte. - Quatro baldes.
resolved(num grafo doprojection_of);elsewhere(só em outro grafo do repositório: aviso, não reprova);outside(oprojection_ofnomeia por URL um grafo de outro repositório que o--withnão leu: não julgado, contado);missing(integrity.projection-id-missing). --with PREFIXO=PASTAlê esse outro grafo de uma pasta local, no commit da URL quando a pasta é um repositório git que o tem, como nokg_query --chain. Uma URL que o--withcobre e que não se lê (ausente no ref, fora de UTF-8, que não é grafo) é erro de grafo (rc 1), nuncaoutside. Um grafo lido da árvore de trabalho, porque o ref da URL não está naquela pasta, sai emunpinned, com um aviso.- Saída e rc.
--format jsondákg-ssot.projection/1(spec/projection.schema.json). rc 0 semmissinge com todo grafo doprojection_oflido; 1 caso contrário; 2 entrada quebrada (frontmatter que não se lê,--rootque não é pasta).
As fontes ausentes: --strict-sources (4.44.0)¶
A fonte com provenance.scheme ou provenance.sha256 que não está lá, no repositório ou no commit de uma URL lida com --with,
é contada em skipped.missing_source, e o check segue dando rc 0: isso fica, por compatibilidade. O --strict-sources lista cada
uma (FONTE AUSENTE) e dá rc próprio, o 4, depois de qualquer achado de rc 1, e a saída JSON é o kg-ssot.trace/2, com
schemes.missing_sources. A fonte sem nenhum dos dois não se julga, com ou sem a opção, e isso inclui a URL com commit e sem hash:
para comparar essas, use o kg_refs.py pins.
O provenance.source na forma repo#id@commit não se lê aqui, e quem o compara é o kg_refs.py pins. Quando a forma ocorre, com ou
sem a opção, ela é contada numa linha própria da saída humana e em schemes.repo_refs, e a saída JSON é o kg-ssot.trace/2. Um
corpus sem essa forma e sem a opção recebe o mesmo kg-ssot.trace/1 de antes.
kg_refs.py: as citações entre repositórios (4.44.0)¶
Quem cita este nó, antes de você apagá-lo? E o que mudou no que você cita? A citação fixada segue válida no commit dela, e por isso nada avisa o grafo que cita. Esta ferramenta pergunta, sobre uma lista que você declara, e é consulta, não alarme: a mudança nunca altera o código de saída, e nenhuma opção faz isso.
kg-refs cited-by billing:D_LIMIT --repo billing=../billing --repo platform=../platform
kg-refs cited-by billing:D_LIMIT --repo billing=../billing --repo platform=../platform --at billing=abc1234 # um estado anterior
kg-refs pins --repos repos.txt --ignore '*' --format json
A lista é o --repo NOME=PASTA, repetível, ou o --repos ARQUIVO, com um NOME=PASTA por linha (# comenta, e a pasta relativa
parte do arquivo). Sem lista, a ferramenta recusa (rc 2): não adivinha repositórios. Um apelido é mais um nome para a mesma
pasta: quando os seus grafos escrevem um repositório de dois jeitos, repita o --repo (--repo billing=../billing --repo
billing-old=../billing), e os dois nomes dão no mesmo clone, lido uma vez. O --known NOME, repetível, declara a sua rede, com pasta
ou sem: o nome que também tem pasta na lista está simplesmente coberto, com o dono escrito ou não. A pasta é lida só pelos objetos do git, no HEAD ou no commit do --at, sem rede e sem a árvore de trabalho, e por isso
o que você não commitou não conta. Os grafos seguem as regras de arquivo do kg_query (o vendor e as fixtures ficam de fora;
--exclude, --no-default-excludes). Só os campos de nó são lidos.
As formas lidas, em qualquer campo de nó:
| Forma | Exemplo |
|---|---|
URL, inclusive raw |
https://host/dono/repo/blob/<commit>/caminho.kg.yaml#ID |
| Dono e repositório | dono/repo@<sha>:caminho |
| Repositório e commit | repo@<sha>:caminho#ID, ou repo@<sha>#ID |
| Repositório, caminho, commit | repo:caminho@<sha>#ID |
| Sem caminho | repo#ID@<commit>, com o provenance.node_sha256, se houver |
| Fonte e locator | provenance.source com o commit, e o id no provenance.locator |
Sem caminho, o id é procurado nos grafos daquele commit, e dois nós dão ambiguous, nunca um palpite. Num arquivo que não é
grafo, o # é uma seção, não um id. O id ou o caminho que só aparece na prosa ao lado da citação, e existe naquele commit, fica
marcado inferred; o id que existe também no repositório de quem cita não entra: ali ele nomeia o nó de lá. A citação sem commit (uma branch na URL, repo:caminho)
não se compara, e é contada em unpinned.
O cited-by REPO:ID lista cada campo que cita, com a forma e a certainty: explicit (uma citação do nó, ou uma aresta no
repositório dele), same_repo ou by_id (uma menção pelo id), local_homonym (uma menção, e o id existe também onde ela está
escrita, e pode nomear o nó de lá), e contains_file (uma citação do arquivo inteiro que contém o nó, no estado lido). A citação do
mesmo id em outro repositório da lista é contada em elsewhere; a menção ao lado da citação de outra coisa segue sendo menção. Todo
commit fixado de uma citação do nó ou do arquivo dele se confere. O target.found_in vazio quer dizer que o nó não está naquele estado: o --at lê um anterior, e quem cita continua aparecendo.
O pins compara cada citação, no commit fixado, com a ponta, e não esconde nada: o summary.groups só diz o que olhar
primeiro, com gone e status no alto, e o file aparece como o resto. O --from REPO lista só as citações escritas naquele
repositório, e o --status gone,changed lista só esses status. Os dois só recortam a lista, e dizem isso numa linha: a varredura, o
summary e o rc seguem os da lista inteira. As arestas do nó se comparam no repositório inteiro, nos dois commits: a aresta que outro
grafo acrescenta, como um REFUTES, dá changed edges, e o nó que mudou de lugar dá same, mesmo quando uma aresta passa a escrevê-lo
como caminho#ID ou fica no grafo de onde ele saiu.
| Status | O que quer dizer |
|---|---|
same |
Igual: nada mudou no nó ou no arquivo, mesmo que ele tenha mudado de lugar (moved_to) |
changed |
Mudou, com as classes: status, text (a mudança só de espaço fica aqui, não em stamp_only), edges, other, file (a citação de um arquivo sem nó), stamp_only (só o verified_at e os campos do eixo dele). O nó traz também o current_status, o status dele hoje |
gone |
Sumiu: não está na ponta |
ambiguous |
Mais de um nó tem o id, no commit ou na ponta; candidates os lista |
unchecked |
Não conferido, com o reason: commit_missing, tip_behind_pin (a ponta local é mais velha que o commit fixado, e comparar acusaria na direção errada), no_access (a forma repo#ID@commit de um repositório que conta e não tem pasta aqui), no_target (o repositório inteiro naquele commit, sem id nem caminho ao lado que exista lá), path_absent_at_commit, id_absent_at_commit, hash_mismatch_at_commit, no_graphs (um nó de um repositório sem grafo na ponta, ou um arquivo que sumiu de uma ponta sem grafo com o commit fixado fora do histórico dela, o que não prova nada sobre nenhum dos dois), um grafo que escreve o id e não se lê, no commit ou na ponta (graph_unreadable_at_commit, graph_unreadable_at_tip), ou objetos que faltam ali, como num clone parcial, e por isso ninguém sabe qual grafo escreve o id (objects_missing_at_commit, objects_missing_at_tip). Um grafo que não se lê e não escreve o id não muda nada |
O relation diz como o commit fixado se liga à ponta: on_history, ou apart (fora do histórico dela, como depois de um rebase,
e mesmo assim comparado). O hash é o node_digest do kg_trace.py digest.
A cobertura é a regra central, porque resposta vazia não prova ausência. Toda saída traz scanned e not_scanned, com o
motivo, e o rc repete o código de saída:
| rc | Quando |
|---|---|
| 0 | A varredura da lista declarada foi completa, e todo citado foi conferido |
| 2 | Erro de uso, inclusive sem lista |
| 3 | Parcial: pasta ausente, que não é a raiz de um clone, ou sem o commit; nenhum grafo lido onde era preciso (no_graphs: as regras de arquivo tiraram todos, com --exclude '*' ou só fixtures/; o --at leu um estado sem nenhum; o repositório do alvo do cited-by, ou um que o pins cita por um nó, não tem nenhum; ou a varredura inteira não tem nenhum; o membro comum da lista, não citado ou citado só por um arquivo, sem grafo nenhum não é parcial, e fica em scanned com graphs: 0); commit ou grafo que não se lê; commit fixado ausente, em qualquer forma (num clone raso, detail: shallow); um repositório do --known, que não tem pasta; ou nome citado com commit que não está na lista |
| 4 | Só no pins, numa varredura completa: algo ambiguous ou unchecked |
O nome fora da lista só conta com commit. Com caminho ou id, como em billing@<sha>:caminho, é um repositório. O nome@<sha> sozinho
precisa de um dono na frente (que não seja um ponto nem um remoto, como em origin/main) ou de um hífen no nome, e por isso 53@<sha> e
tools@<sha> são só texto. Daí há três caminhos. Sem opção nenhuma, ele entra em not_scanned, o caminho seguro: você fica sabendo quem
não leu. Com o --known, só a lista e os conhecidos contam no rc: o resto vai a mentioned_undeclared, com a contagem, e cada citação
com id vai a outside_citations, com quem cita, o campo e o id, sem mudar o rc. O --ignore NOME tira o nome falso, e aceita glob; o
--ignore '*' declara a lista completa. A cobertura vence o rc 4. Num corpus com citações do repositório inteiro (repo@<sha> sem
alvo), o pins dá rc 4 por elas.
A saída humana põe primeiro, em bloco próprio, o que a lista declarou e falhou, e os nomes de fora numa linha com a contagem; os nomes estão no JSON.
O limite: acha citação errada, não citação que falta. Lê o que os grafos escreveram, nas pastas da lista. A afirmação de ausência que não cita nada não aparece, nem a citação fora de um campo de nó, num comentário ou num arquivo que não é grafo. Não diz nada de um repositório de que você não tem a pasta, e não foi testado por um terceiro sem acesso a elas. Não faz ninguém rodá-lo. O servidor MCP não o tem: ele lê outras pastas.
Mais cinco limites, para você não ler na resposta mais do que ela diz:
- Dois homônimos na outra ponta da aresta contam como um. Essa ponta se compara só pelo id, e por isso a aresta do nó que passa a apontar, ou deixa de apontar, para um nó de mesmo id em outro arquivo não muda o veredito do nó.
- Um membro comum na branch errada parece um membro sem grafo. Ele fica em
scannedcomgraphs: 0e rc 0: olhe oscannedse esperava grafos ali. Só o repositório do alvo e o--atdão rc 3 por isso. - A identidade é só pelo nome. Um fork com outro dono e o mesmo nome de repositório dá na pasta daquele nome; a ferramenta compara commits, não confere o dono.
- Vazio com rc 0 quer dizer entre os listados. É uma resposta completa sobre a lista que você declarou, não sobre o mundo. O rc 0
não diz nada de um repositório fora da lista, ou que você tirou com o
--ignore. Nocited-by, orepo@<sha>sem caminho e sem id também fica de fora, mesmo com o commit ausente: nada o liga ao nó. - Um grafo inteiro reordenado sai
changed file, sem mudar o rc. A citação do arquivo compara o objeto, e por isso não sabe que os nós são os mesmos; a citação do nó dásame, porque compara o nó.
kg_vector.py: o modelo e o índice da busca por semelhança (4.26.0)¶
O kg_query --semantic acha o nó quando a pergunta usa outras palavras que não as do label. É opcional: precisa do
extra vector (pip install "kg-ssot[vector]", que traz onnxruntime, tokenizers e numpy; no vendor, instale o
tools/requirements-vector.txt com --require-hashes). Sem ele, o kit roda como sempre, e só o --semantic e este
script recusam, com rc 3 e rc 2 e o que instalar. Nada aqui entra em veredito: o contrato, o gate e o --text não
dependem do vetor.
python3 -I -B vendor/kg-ssot/tools/kg_vector.py fetch # o modelo, uma vez (cerca de 250 MB)
python3 -I -B vendor/kg-ssot/tools/kg_vector.py index --root . # o cache, refeito só onde um nó mudou
python3 -I -B vendor/kg-ssot/tools/kg_query.py --root . --text "busca vetorial" --semantic "vale a pena adotar busca vetorial?"
- O modelo é um arquivo ONNX com a revisão e o sha256 fixados no script (
kg_vector.py models): o padrão é o modelo multilíngue que a bancada mediu (cerca de 50 línguas, 128 tokens). Ofetcho baixa do Hugging Face para$KG_SSOT_MODEL_DIR(ou.cache/kg-ssot/models na pasta pessoal) e confere o sha256 antes de pôr no lugar; cada carga confere de novo, e a consulta nunca vai à rede. Sem rede, ponha os arquivos lá. - O texto de um nó é o label e a narrative, juntos por um espaço; o vetor é a média dos tokens, normalizada. Ele reproduz o embedder da bancada: nos 403 nós dela, o menor cosseno foi 0,9999999.
- O índice é um cache em
<raiz>/.kg-ssot/vector/<modelo>/index.npz, com um.gitignoreque o deixa fora do git: um vetor por nó e o sha256 do texto que o gerou, num arquivo só, trocado de uma vez e conferido na carga (forma, dimensão, valores finitos, chaves únicas); o índice que não passa na conferência é ignorado e refeito. O nó editado depois do índice não fica com o vetor velho: a consulta o embute na hora e o conta emembedded_now, e o próximoindexrefaz só o que mudou. Sem índice, a consulta embute todo candidato (nos 403 nós da bancada, cerca de 15 s; com o índice, cerca de 2 s). Num corpus de 6.385 nós, o primeiroindexlevou cerca de 10 minutos em 8 núcleos, o seguinte 7 s, e uma consulta híbrida com o índice 7 s. Dê aoindexe à consulta os mesmos--exclude: o nó que o índice deixou de fora é embutido na hora a cada consulta. - No servidor MCP, o
kg_queryaceitasemanticquando o servidor tem o extra e o modelo; senão a ferramenta responde com um erro que diz o que falta, nunca só com o texto. O servidor não tem índice em disco: ele embute cada nó uma vez por processo e guarda o vetor na memória pelo sha256 do texto, então a primeira chamada semântica num armazém grande é a lenta. - O nó sem texto (sem label e sem narrative) vai para o fim da ordem do vetor: o vetor de um texto vazio não diz
nada sobre ele. O pacote MCPB, a imagem GHCR e o pacote npm não levam
o vetor: ele vem pelo pacote PyPI com o extra, ou pelo vendor com o
tools/requirements-vector.txt.
rc 0 feito; 2 com entrada quebrada, extra ou modelo ausente, arquivo que não confere, ou download que falhou.
kg_prov_export.py: a proveniência em PROV-O¶
Exporta um .kg.yaml para PROV-O em JSON-LD, para quem já usa ferramentas do
ecossistema W3C PROV. Cada nó vira uma prov:Entity. Um nó com provenance ganha prov:wasDerivedFrom apontando
para a fonte e uma prov:Derivation cuja atividade carrega o method; o locator e a locality vão como
propriedades kg:. Uma fonte que é inteira uma URL http(s) vira a própria IRI; qualquer outra fonte (um caminho, um
comando, duas URLs juntas) vira um nó em branco rotulado, um por texto distinto, para que nós com a mesma fonte
apontem para a mesma entidade.
python3 -I -B vendor/kg-ssot/tools/kg_prov_export.py \
vendor/kg-ssot/spec/conformance/fixtures/latest/form/method-vocabulary/valid-method-leitura.kg.yaml
Trecho da saída, o nó com provenance:
"@id": "urn:kg-ssot:graph:valid-method-leitura#EV_A",
"@type": "prov:Entity",
...
"prov:wasDerivedFrom": {
"@id": "_:source0"
},
"prov:qualifiedDerivation": {
"@type": "prov:Derivation",
"prov:entity": {
"@id": "_:source0",
"rdfs:label": "Fonte A"
},
"prov:hadActivity": {
"@type": "prov:Activity",
"rdfs:label": "leitura: seção 3 do documento A"
},
"kg:locator": "caso de conformidade"
}
O que ele não exporta, de propósito:
- As arestas, por padrão.
SUPPORTS,REFUTESe as outras são relações de evidência, e derivação em PROV é relação de produção. Exportá-las comowasDerivedFromdiria algo que o grafo não diz. Com--edges, elas saem de outro jeito, descrito abaixo. - O carimbo como tempo de geração.
verified_atsai comokg:verifiedAt(tipadoxsd:datequando é uma data), não comoprov:generatedAtTime: em PROV, o tempo de geração marca quando a entidade passou a existir, não quando foi conferida.
As arestas, com --edges¶
--edges exporta todas as arestas, de edges e de external_edges, sem perda e sem derivação falsa:
- Toda aresta vira um recurso
kg:Edge. Ele levakg:edgeType,kg:from,kg:to, o rótulo comordfs:labele qualquer outra chave (trigger,x_*) comokg:<chave>, com o nome da chave em percent-encoding, para uma chavex_com espaço ainda dar uma IRI válida. Um valor que não é texto vai como JSON, tipadokg:json. Assim, qualquer tipo, inclusive os da camada domain, volta inteiro a partir do RDF. - A ponta externa (
<caminho>#<id>) vai como o literalkg:fromRefoukg:toRef, porque a IRI de um nó de outro grafo depende dometa.iddaquele arquivo. Uma ponta que não é id de nó válido (entrada que o contrato recusa, como um caminho absoluto) também vai como literal, para nunca virar uma IRI quebrada que cada leitor RDF lê de um jeito. Umfromausente (o grafo inteiro) vai comokg:fromGraph true. - Entre dois nós locais, três tipos ganham também uma tripla-padrão:
SUPPORTSganhacito:supports, da CiTO;REFUTESganhacito:refutes;SUPERSEDESganhadcterms:replaces, dos DCMI Terms.
Os outros tipos não ganham nenhuma, porque a PROV não tem par para nenhum dos três e não se achou termo-padrão para
o resto.
- Nenhuma aresta gera prov:wasDerivedFrom.
Sem a opção, a saída é byte a byte a de antes. Antes da release, o mapeamento foi medido em 213 grafos e 9.988 arestas, contra critérios registrados antes da medição. Todas as arestas voltaram a partir do RDF lido pelo rdflib, e o custo foi de 1,7 vez o da exportação simples.
O documento inteiro, com --full¶
--full implica --edges e exporta também todo campo de cada nó e do meta. Assim, o YAML carregado pode ser
remontado a partir do RDF:
- Todo campo de cada nó vai como
kg:<chave>, inclusive os que já saem por outro termo (label,status,provenance…). Texto vai como literal simples, e qualquer outro valor (número, booleano, nulo, lista, mapa) vai como JSON, tipadokg:json. - Um recurso
kg:Graphem<base><meta.id>leva cada chave dometacomokg:<chave>, e cada chave extra do topo (asx_*) comokg:top-<chave>. - Um valor que não cabe em JSON faz o exportador sair com rc 2, nomeando o campo.
A ordem dos itens e a forma de superfície (aspas, comentários, estilo) não são guardadas. Antes da release, o --full
foi medido em 209 grafos, 7.778 nós e 9.906 arestas, contra critérios registrados antes da medição. O YAML carregado
voltou igual a partir do RDF em todos os grafos, e exportar o corpus inteiro do core levou 0,25 s e 19,7 MB num runner
de CI.
Para ler a saída em ferramentas RDF, instale o extra rdf: pip install "kg-ssot[rdf]" (rdflib) ou, no vendor, o
tools/requirements-rdf.txt com --require-hashes. Ele fica fora do perfil gate, e o exportador em si não precisa
dele. O CI lê com o rdflib a saída --edges da suíte inteira e dos grafos do repo, e reconstrói toda aresta só a
partir do RDF:
import rdflib, subprocess
out = subprocess.run(["kg-prov-export", "--edges", "grafo.kg.yaml"], capture_output=True, text=True, check=True).stdout
g = rdflib.Graph().parse(data=out, format="json-ld")
print(len(g), "triplas")
O exportador não valida o grafo: rode o kg_validate.py antes. --base IRI troca o prefixo das IRIs dos nós (o
default é urn:kg-ssot:graph:, e cada nó fica em <base><meta.id>#<id>); o vocabulário kg: mora em
urn:kg-ssot:vocab#, fora desse espaço. rc 2 quando o arquivo é ilegível, não é um documento YAML único ou tem
meta ou nodes fora da forma.
kg_migrate.py: o migrador por perfil¶
Propõe a provenance que o contrato v4 pede aos nós confirmed ou PROD que ainda não a têm, a partir de um
perfil que o adotante escreve e versiona: uma lista de regras em ordem (spec/kg-migrate-profile.schema.json),
em que a primeira que casa decide. Há um exemplo comentado em spec/migrate-examples/company-brain.profile.yaml,
medido sobre um corpus real de 25 grafos e 1.024 nós.
python3 -I -B vendor/kg-ssot/tools/kg_migrate.py plan --profile kg-migrate.profile.yaml --out proposals.tsv
python3 -I -B vendor/kg-ssot/tools/kg_migrate.py apply --profile kg-migrate.profile.yaml
- O
plannão escreve nada. Imprime as contagens por regra e grava, com--out, a planilha de propostas, uma linha por nó, com o que vai para quem revisa e por quê. Leia a planilha antes de rodar oapply. - Cada regra faz uma de três coisas: propor a
provenance, rebaixar o nó paraunverifiable(a fonte é irrecuperável, que é o padrão do contrato) ou só mandar o nó para revisão. O nó que nenhuma regra casa também vai para revisão e não recebe nada. - O
whencasa pelo que o nó já traz: o trace (vazio, URL, caminho, glob, existência na árvore, expressão regular), o tipo, o status, o plano, overified_against, osource_kinde as arestasSUPPORTSque chegam ao nó, comnoteany_of. - Os templates usam os campos do nó:
{trace},{trace_path},{trace_line},{verified_against},{supports}e{source_kind}, e{a|b}pega o primeiro que não for vazio. Onotedo perfil vai no fim de todo detalhe domethod, por exemplonão reverificado na migração de 2026-10-20(o note do perfil de exemplo). - O
scopesdáverified_scopesó por lista de ids, nunca por padrão de texto. - O perfil escolhe dentro do contrato e nunca o relaxa. Toda proposta é conferida contra o schema MUST e o schema
SHOULD de forma antes de virar proposta. A que reprovaria vai para revisão com o motivo, e o template que resolve
para vazio também. Um aviso de coerência que a proposta acende não a barra, porque o aviso diz a verdade sobre o
nó. Por exemplo, um
testimonynum nó PROD acendeintegrity.testimony-in-prod: a proposta segue, e o nó vai para quem revisa com o aviso nomeado. - O
applygrava arquivo por arquivo, por texto, e os comentários ficam. Depois ele relê o arquivo e confere a invariância: cada nó fica igual ao de antes, fora as chaves que a ação autoriza, e o diff só acrescenta linhas, fora a linha do status rebaixado. Um arquivo que ganharia código MUST novo, ou aviso SHOULD que o plan não anunciou, fica como estava. A quebra de linha do arquivo é mantida (CRLF fica CRLF), e o arquivo com quebras misturadas é recusado. Rodar de novo não muda nada. - Sem lista de grafos, a ferramenta lê os
.kg.yamldogit ls-files. Ficam de fora os padrões dokg_gate.py(*/fixtures/*edocs/materials/*), ovendor/*, toda pasta com o carimbo.kg-ssot-version(desde a 4.20.0) e os globs de--exclude, com a semântica do gate:fnmatchsobre o caminho relativo, em que*atravessa/; o**/do dialeto do perfil, que casa zero pastas, também vale.--no-default-excludestira os padrões. Atenção: notrace_path_globdo perfil,*não atravessa/. Um grafo da lista que não parseia vira linha de revisão e não aborta a rodada. - O nó cujo
verified_scopejá é o do perfil sai comosame, separado dos nós que mudam, e oapplynão o toca. - O caminho do trace é léxico e fica dentro da raiz do repositório. Ele é normalizado (
./,a/../b), e o que sai da raiz com../deixa de ser caminho. Um caminho absoluto ou com~conta como inexistente, sem consultar o disco da máquina.
O que fica fora, de propósito, porque é julgamento: reescrever label, mudar plano, re-medir, refutar, reconciliar com
SUPERSEDES e promover um nó unverifiable. rc 0 quando o plano sai ou o apply termina; rc 1 quando o apply
recusou algum arquivo, que fica como estava e com o motivo no relatório; rc 2 com entrada quebrada (perfil fora do
schema, regra ou id repetido, expressão regular inválida, when que casaria todo nó, grafo dado na linha de comando
que não parseia). O migrador viaja no vendor e não no pacote do PyPI nem no do npm.
keys: a chave estranha vira x_<chave> (4.19.0)¶
python3 -I -B vendor/kg-ssot/tools/kg_migrate.py keys # o plano: o que seria renomeado e o que vai para revisão
python3 -I -B vendor/kg-ssot/tools/kg_migrate.py keys --apply # grava as renomeações
Uma chave que o contrato não conhece reprova no MUST. O keys a renomeia para x_<chave>, o prefixo de extensão, no
topo, no meta, nos nós, nas arestas e nas arestas externas, pelo texto, então os comentários ficam. As chaves
conhecidas vêm do schema MUST da release, nunca de uma lista no código. A chave vai para quem revisa quando não é slug,
quando x_<chave> já existe no mesmo mapa, ou quando o nome novo é um perfil canônico (severity viraria
x_severity, cujo valor o perfil julga). Com --apply, cada arquivo só é gravado se a releitura for o documento de
antes com só as chaves renomeadas, se o diff tocar só essas linhas e se o arquivo não ganhar código MUST nem aviso
SHOULD. A quebra de linha fica como estava, o arquivo com quebras misturadas é recusado, e rodar de novo não muda nada.
Medido numa cópia dos 15 grafos do core que reprovavam no MUST por chave estranha: com o meta.title conhecido desde o
v4.11 e 76 chaves renomeadas em 12 arquivos, 14 passam; o que sobra também não tem proveniência. Rodado pelo core no
próprio corpus (2026-10-23), com os excludes do gate: o gate foi de 170 para 182 de 184 grafos no MUST, e a dívida
herdada de 14 grafos para 2, os dois sem proveniência.
nest: as suas chaves x_ viram um perfil canônico (4.21.0)¶
python3 -I -B vendor/kg-ssot/tools/kg_migrate.py nest --map mapa-nest.yaml # o plano
python3 -I -B vendor/kg-ssot/tools/kg_migrate.py nest --map mapa-nest.yaml --apply # grava os perfis
O keys dá o prefixo x_ à chave estranha; o nest vai um passo além e leva as chaves x_ que um adotante inventou
para os perfis canônicos de nó do capítulo 3, x_mirror, x_doc_cycle e x_questionnaire. O mapa
(spec/kg-migrate-nest.schema.json) tem uma regra por perfil. A regra vale para o nó que tem a chave when, e cada
campo do perfil vem de uma fonte: const (um valor fixo), move (uma chave x_ que sai do nó) ou copy (qualquer
chave, que fica, como verified_at). A fonte pode passar por match (uma regex com um grupo, lida como o ECMA-262 a
lê: \w e \d em ASCII, \s com os espaços Unicode, . sem quebra de linha, $ só no fim do texto; o que os dois
motores leem diferente é entrada quebrada, e o valor com caractere além de U+FFFF vai para quem revisa) e por uma tabela
de valores, values para o valor exato e prefixes para o prefixo mais longo que casa. Com raw: true, o valor
original de cada chave movida que tinha texto além do código (entrou por prefixo, ou a regex cortou parte dele) fica em
<perfil>.x_raw; o valor exato da tabela não fica. A chave movida por match ou prefixes perde o texto que o código
deixa de fora, então essa regra pede raw: true (sem ele, o mapa é entrada quebrada):
nest_version: "1"
nest:
- into: x_mirror
when: x_board_status
fields:
system: {const: board}
state: {move: x_board_status}
ref: {copy: verified_against, match: "/t/([a-z0-9]+)$"}
url: {copy: verified_against}
synced_at: {copy: verified_at}
- into: x_doc_cycle
when: x_issuer
raw: true
fields:
issuer: {move: x_issuer}
state: {move: x_in_force, values: {vigente: in_force}, prefixes: {"vigente (": in_force}}
Nada é chutado. O nó vai para quem revisa, sem ser tocado, quando um valor não tem código na tabela, quando falta a
fonte de um campo obrigatório, quando o nó já tem o perfil, quando o perfil que ele ganharia não passa no schema SHOULD
do perfil (um synced_at que não é data, um state fora do enum) quando o nó está em estilo de fluxo ({...}), quando
a chave when dele é null, quando um valor é .nan ou .inf, ou quando a edição do texto daquele nó não releria igual
(um valor movido com âncora &x ou alias *x). O plano ensaia a edição do arquivo inteiro uma vez, e só a divide
quando ela falha, então o nó que ela não sabe editar vai para quem revisa ali, e o --apply grava os outros. O
--apply grava cada arquivo sob as mesmas conferências do keys: a releitura tem de ser o documento de antes com só as
chaves movidas e os perfis novos, e o arquivo não pode ganhar código MUST nem aviso SHOULD. A quebra de linha fica, os
comentários fora das chaves movidas ficam, e rodar de novo não muda nada. Medido numa cópia dos dois grafos de um
adotante (108 nós com 168 dessas chaves): 106 perfis montados, 2 para quem revisa (dois x_in_force com texto livre que
o mapa não cobria), os dois arquivos gravados com o mesmo veredito de antes.