kg-ssot 4.17.2 → 4.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,85 +1,138 @@
1
1
  # kg-ssot
2
2
 
3
- **O leitor JavaScript do contrato do KG-SSOT, o grafo de conhecimento verificável: toda afirmação diz de onde veio,
4
- quando foi conferida e o que depende dela.**
3
+ [![npm](https://img.shields.io/npm/v/kg-ssot)](https://www.npmjs.com/package/kg-ssot)
4
+ [![license](https://img.shields.io/npm/l/kg-ssot)](https://github.com/marciocar/kg-ssot/blob/main/LICENSE)
5
+ [![node](https://img.shields.io/node/v/kg-ssot)](https://github.com/marciocar/kg-ssot)
5
6
 
6
- Este pacote julga um grafo `.kg.yaml` ou `.kg.json` contra o contrato vigente e devolve o veredito MUST e os avisos
7
- SHOULD, com os códigos estáveis da suíte de conformidade. Ele traz:
8
- - o **núcleo do leitor**, sem `fs` e sem processo;
9
- - os **validadores do contrato pré-compilados**, MUST e SHOULD, que não usam `new Function` nem `eval`. Por isso rodam
10
- em motores sem geração de código, como o Hermes do React Native e do Expo, e no Node;
11
- - os **schemas** do contrato e do veredito.
7
+ **The JavaScript reader for the KG-SSOT contract, the verifiable knowledge graph. Every claim says where it came
8
+ from, when someone verified it, and what depends on it.**
12
9
 
13
- O leitor de referência é o pacote Python [`kg-ssot`](https://pypi.org/project/kg-ssot/). Este leitor passa em todos
14
- os casos da suíte de conformidade (`latest/`), com o veredito, os códigos e os avisos esperados. O CI confere isso
15
- pelo pacote instalado a cada mudança no leitor.
10
+ `kg-ssot` checks a `.kg.yaml` or `.kg.json` graph against the current contract (v4). It returns the MUST verdict and
11
+ the SHOULD warnings, with the stable codes of the conformance suite. The package ships:
16
12
 
17
- ## Instalar
13
+ - **The reader core**, which uses neither `fs` nor `process`.
14
+ - **Precompiled contract validators**, MUST and SHOULD, that never call `new Function` or `eval`. They run on engines
15
+ without runtime code generation, like Hermes in React Native and Expo, and on Node.
16
+ - **The schemas** of the contract and of the verdict.
17
+
18
+ The reference reader is the Python package [`kg-ssot`](https://pypi.org/project/kg-ssot/). This reader passes every
19
+ case in the conformance suite (`latest/`) with the expected verdict, codes, and warnings. On every change to the
20
+ reader, CI runs the whole suite against the installed tarball.
21
+
22
+ ## Install
18
23
 
19
24
  ```bash
20
25
  npm install kg-ssot
21
26
  ```
22
27
 
23
- ## No código
28
+ On Node, you need version 22.12 or later. The package is ESM and ships its own TypeScript types.
29
+
30
+ ## Use it in code
24
31
 
25
32
  ```js
26
33
  import { verdict } from 'kg-ssot';
27
34
 
28
- verdict(texto); // .kg.yaml (o default)
29
- verdict(texto, { format: 'json' }); // .kg.json
35
+ verdict(text); // .kg.yaml (the default)
36
+ verdict(text, { format: 'json' }); // .kg.json
30
37
  // { verdict: 'accept', codes: [], warnings: [], detail: '' }
31
38
  // { verdict: 'accept', codes: [], warnings: ['yaml.unquoted-date'], detail: '' }
32
- // { verdict: 'reject', codes: ['form.range.node.impact'], warnings: [], detail: '' }
39
+ // { verdict: 'reject', codes: ['form.range.node.impact', 'form.type.node.impact'], warnings: [], detail: '' }
33
40
  ```
34
41
 
35
- - `verdict(texto, { format, corpus })` devolve o veredito no contrato de runner da suíte: `verdict` é `accept` ou
36
- `reject`, `codes` traz os códigos MUST e `warnings` os avisos SHOULD, os dois ordenados e sem repetição. Um aviso
37
- não reprova: o grafo com aviso continua `accept`.
38
- - `check(texto, { format, corpus })` devolve só os códigos MUST (vazio quando passa), e `checkWarnings(texto,
39
- { format })` só os avisos.
40
- - `kg-ssot/core` é o núcleo sem o validador, para quem compila o schema por conta própria. `kg-ssot/node` traz os
41
- auxiliares de Node, como `corpusIndex(dir)`, que indexa os `.kg.yaml` de um diretório para conferir as
42
- `external_edges`. Os schemas ficam em `kg-ssot/spec/*`.
42
+ - `verdict(text, { format, corpus })` returns the verdict in the suite's runner contract. `verdict` is `accept` or
43
+ `reject`, `codes` lists the MUST codes, and `warnings` lists the SHOULD codes. Both lists are sorted, with no
44
+ duplicates. A warning never fails a graph: a graph with warnings is still `accept`.
45
+ - `check(text, { format, corpus })` returns only the MUST codes (empty when the graph passes).
46
+ `checkWarnings(text, { format, corpus })` returns only the warnings.
47
+ - `corpus` lets the reader verify the far end of each `external_edges` entry. It is a
48
+ `Map<path, Set<nodeId>>`. Build it with `corpusIndex(dir)` from `kg-ssot/node`, or, without a file system, with
49
+ `indexTexts([[path, text], ...])` from the main entry.
50
+
51
+ ### Entry points
52
+
53
+ | Import | What you get |
54
+ |---|---|
55
+ | `kg-ssot` | `verdict`, `check`, `checkWarnings`, the precompiled `validateMust` and `validateShould`, and the core |
56
+ | `kg-ssot/core` | The core without the validators, for when you compile the schema yourself (`verdictOf`, `codes`, `warnings`, `parse`, `indexTexts`) |
57
+ | `kg-ssot/must`, `kg-ssot/should` | The precompiled MUST and SHOULD validators alone |
58
+ | `kg-ssot/node` | Node helpers: `corpusIndex(dir)` indexes the `.kg.yaml` and `.kg.json` graphs under a directory (the tracked ones, in a git repository); `compileMust()` compiles the schema with `ajv` at runtime |
59
+ | `kg-ssot/spec/*` | The contract and verdict schemas |
43
60
 
44
- Desde a 4.10.0, este leitor também mede os avisos SHOULD, com os mesmos códigos do leitor de referência em todos os
45
- casos da suíte, e desde a 4.13.0 trata o `\r` como quebra de linha, como a referência. Desde a 4.11.0, as três construções raras do YAML em que ele divergia da referência (a tag `!` sem
46
- nome, o `!!timestamp` com texto que não é data e o tab fora de aspas) reprovam nos dois, com casos na suíte. A linha de cada ocorrência e as dicas de correção continuam só no leitor de referência em Python.
61
+ Since 4.10.0, this reader also measures the SHOULD warnings, with the same codes as the reference reader on every
62
+ suite case. Since 4.13.0, it treats `\r` as a line break, like the reference. Since 4.11.0, the three rare YAML
63
+ constructs where it used to diverge from the reference (the unnamed `!` tag, a `!!timestamp` whose text is not a
64
+ date, and a tab outside quotes) now fail in both, and the suite has cases for them. Only the Python reference reader
65
+ reports the line of each finding and the fix hints.
47
66
 
48
- ## TypeScript e CommonJS
67
+ ## TypeScript and CommonJS
49
68
 
50
- O pacote traz os tipos (`index.d.mts`, `core.d.mts` e `node.d.mts`) e funciona com `moduleResolution` `bundler` ou
51
- `nodenext`, o que cobre o template TypeScript do Expo. É um pacote ESM. Num projeto CommonJS, o `require('kg-ssot')`
52
- funciona a partir do Node 22.12, que é o mínimo declarado em `engines`.
69
+ The package ships types (`index.d.mts`, `core.d.mts`, `node.d.mts`, and `validator.d.mts`). It works with
70
+ `moduleResolution` set to `bundler` or `nodenext`, which covers the Expo TypeScript template. It is an ESM package.
71
+ In a CommonJS project, `require('kg-ssot')` works from Node 22.12, the minimum declared in `engines`.
53
72
 
54
- ## Na linha de comando
73
+ ## Use it from the command line
55
74
 
56
75
  ```bash
57
- npx kg-ssot-validate grafo.kg.yaml outro.kg.json
76
+ npx kg-ssot-validate decisions.kg.yaml graph.kg.yaml other.kg.json
58
77
  ```
59
78
 
60
79
  ```text
61
- PASSA grafo.kg.yaml
62
- AVISO yaml.unquoted-date
63
- REPROVA outro.kg.json
64
- MUST form.range.node.impact · integrity.orphan-node
80
+ PASS decisions.kg.yaml
81
+ PASS graph.kg.yaml
82
+ WARNING yaml.unquoted-date
83
+ FAIL other.kg.json
84
+ MUST form.range.node.impact · form.type.node.impact
85
+ ```
86
+
87
+ The exit code is 0 when every file passes, 1 when any file fails, and 2 on broken input: an unreadable file, invalid
88
+ UTF-8, an unknown option, or a `--corpus` that is not a directory. Warnings never change the exit code.
89
+
90
+ - `--json` prints one JSON line per file, in the suite's runner contract:
91
+
92
+ ```text
93
+ {"file":"graph.kg.yaml","verdict":"accept","codes":[],"warnings":["yaml.unquoted-date"],"detail":""}
94
+ {"file":"other.kg.json","verdict":"reject","codes":["form.range.node.impact","form.type.node.impact"],"warnings":[],"detail":""}
95
+ ```
96
+
97
+ - `--corpus DIR` verifies the far end of each `external_edges` entry against the `.kg.yaml` and `.kg.json` graphs
98
+ under `DIR`.
99
+
100
+ The human output is in English. The CLI speaks Brazilian Portuguese when you set `KG_SSOT_LANG=pt-BR` (or when your
101
+ locale is `pt_BR`):
102
+
103
+ ```bash
104
+ KG_SSOT_LANG=pt-BR npx kg-ssot-validate graph.kg.yaml
65
105
  ```
66
106
 
67
- O rc é 0 quando todos passam, 1 quando algum reprova e 2 com arquivo ilegível ou opção desconhecida. Os avisos não
68
- mudam o rc. Com `--json`, sai uma linha JSON por
69
- arquivo. Com `--corpus DIR`, a ponta externa das `external_edges` é conferida contra os `.kg.yaml` sob `DIR`.
107
+ The codes and the `--json` output are data, so they never change language. The documentation in Portuguese is in
108
+ [README.pt-BR](https://github.com/marciocar/kg-ssot/blob/main/README.pt-BR.md).
109
+
110
+ ## Use it in a React Native or Expo app
70
111
 
71
- ## Num app React Native ou Expo
112
+ The package is ESM, and the main entry imports nothing from Node. Your bundler (Metro, esbuild) resolves the two
113
+ dependencies, `yaml` and the `ajv` runtime. The validators come precompiled, so nothing has to generate code on the
114
+ device. We ran the conformance suite on Hermes, and it returns the same verdicts as Node.
115
+
116
+ ```js
117
+ import { verdict, indexTexts } from 'kg-ssot';
118
+
119
+ const corpus = indexTexts([['other.kg.yaml', otherText]]); // optional, for external_edges
120
+ const result = verdict(graphText, { corpus });
121
+ if (result.verdict === 'reject') console.warn(result.codes);
122
+ ```
72
123
 
73
- O pacote é ESM e a entrada não importa nada do Node. Um bundler (Metro, esbuild) resolve as dependências, `yaml` e o
74
- runtime do `ajv`. O validador já vem compilado, então nada precisa gerar código no aparelho. O comportamento foi
75
- medido no Hermes: os mesmos vereditos do Node.
124
+ ## Documentation
76
125
 
77
- ## Documentação
126
+ - [Manual](https://github.com/marciocar/kg-ssot/tree/main/docs/manual): the format, the contract, the tools, and
127
+ common problems
128
+ - [Conformance suite](https://github.com/marciocar/kg-ssot/blob/main/spec/conformance/README.md)
129
+ - [Adoption guide](https://github.com/marciocar/kg-ssot/blob/main/ADOPTING.md)
130
+ - [Changelog](https://github.com/marciocar/kg-ssot/blob/main/CHANGELOG.md)
131
+ - [Issues](https://github.com/marciocar/kg-ssot/issues)
78
132
 
79
- - [Manual](https://github.com/marciocar/kg-ssot/tree/main/docs/manual): o formato, o contrato, as ferramentas e os problemas comuns
80
- - [Suíte de conformidade](https://github.com/marciocar/kg-ssot/blob/main/spec/conformance/README.md)
81
- - [Guia de adoção](https://github.com/marciocar/kg-ssot/blob/main/ADOPTING.md)
133
+ ## License
82
134
 
83
- ## Licença
135
+ Apache-2.0. See [`LICENSE`](https://github.com/marciocar/kg-ssot/blob/main/LICENSE) and
136
+ [`NOTICE`](https://github.com/marciocar/kg-ssot/blob/main/NOTICE).
84
137
 
85
- Apache-2.0. Veja `LICENSE` e `NOTICE`.
138
+ Documentação em português: [README.pt-BR](https://github.com/marciocar/kg-ssot/blob/main/README.pt-BR.md).
package/cli.mjs CHANGED
@@ -4,14 +4,60 @@
4
4
  // kg-ssot-validate <arquivo>... [--json] [--corpus DIR]
5
5
  //
6
6
  // rc 0 quando todos passam no MUST, 1 quando algum reprova, 2 com entrada quebrada (arquivo ilegível, UTF-8
7
- // inválido). Os avisos SHOULD saem numa linha AVISO e não mudam o rc. --json imprime uma linha JSON por arquivo, no contrato de runner da suíte. --corpus confere a ponta
7
+ // inválido). Os avisos SHOULD saem numa linha WARNING e não mudam o rc. --json imprime uma linha JSON por arquivo, no contrato de runner da suíte. --corpus confere a ponta
8
8
  // externa das external_edges contra os .kg.yaml e .kg.json sob DIR.
9
+ //
10
+ // A saída humana sai em inglês (a língua-fonte) ou na tradução pt-BR, negociada como no kg_i18n do Python:
11
+ // KG_SSOT_LANG, LANGUAGE, LC_ALL, LC_MESSAGES e LANG, parando na primeira definida; C, POSIX e vazio são o inglês.
12
+ // Como no gettext, o LANGUAGE só vale quando o locale efetivo (LC_ALL, LC_MESSAGES, LANG) não é o C.
13
+ // As frases ficam também no catálogo locale/pt_BR/LC_MESSAGES/kg-ssot.po (referência #: packaging/npm/cli.mjs), para o tradutor ter um lugar
14
+ // só. A saída --json é dado e não se traduz.
9
15
  import { readFileSync } from 'node:fs';
10
16
 
11
17
  import { verdict } from './index.mjs';
12
18
  import { corpusIndex } from './run.mjs';
13
19
 
14
- const USAGE = 'uso: kg-ssot-validate <arquivo.kg.yaml|arquivo.kg.json>... [--json] [--corpus DIR]\n';
20
+ // A tradução pt-BR (igual às entradas do cli.mjs no kg-ssot.po); a chave é o msgid em inglês.
21
+ const PT_BR = {
22
+ 'usage: kg-ssot-validate <file.kg.yaml|file.kg.json>... [--json] [--corpus DIR]':
23
+ 'uso: kg-ssot-validate <arquivo.kg.yaml|arquivo.kg.json>... [--json] [--corpus DIR]',
24
+ 'BROKEN INPUT': 'ENTRADA QUEBRADA',
25
+ '--corpus without a directory': '--corpus sem diretório',
26
+ 'unknown option: {option}': 'opção desconhecida: {option}',
27
+ 'corpus {root} is not a directory': 'corpus {root} não é um diretório',
28
+ PASS: 'PASSA',
29
+ FAIL: 'REPROVA',
30
+ ERROR: 'ERRO',
31
+ WARNING: 'AVISO',
32
+ };
33
+
34
+ // true quando a língua do ambiente é o português (pt, pt_BR e afins); o inglês e língua sem tradução dão false.
35
+ function wantsPortuguese(vars) {
36
+ const isC = (v) => { const t = v.trim().split('.')[0].split('@')[0].toLowerCase(); return !t || t === 'c' || t === 'posix'; };
37
+ const effective = ['LC_ALL', 'LC_MESSAGES', 'LANG'].map((n) => vars[n]).find((v) => v);
38
+ const localeIsC = !effective || isC(effective);
39
+ for (const name of ['KG_SSOT_LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
40
+ const value = vars[name];
41
+ if (!value || (name === 'LANGUAGE' && localeIsC)) continue;
42
+ for (const raw of name === 'LANGUAGE' ? value.split(':') : [value]) {
43
+ const tag = raw.trim().split('.')[0].split('@')[0].replace(/-/g, '_').toLowerCase();
44
+ if (!tag || tag === 'c' || tag === 'posix') return false;
45
+ const lang = tag.split('_')[0];
46
+ if (lang === 'pt') return true;
47
+ if (lang === 'en') return false;
48
+ }
49
+ return false; // a variável definida decide, mesmo sem tradução: o inglês
50
+ }
51
+ return false;
52
+ }
53
+
54
+ const TABLE = wantsPortuguese(process.env) ? PT_BR : {};
55
+ const _ = (msgid, vals = {}) =>
56
+ (TABLE[msgid] ?? msgid).replace(/\{(\w+)\}/g, (m, k) => (k in vals ? String(vals[k]) : m));
57
+ // o rótulo da linha do arquivo, com a coluna do nome alinhada
58
+ const label = (msgid) => `${_(msgid).padEnd(7)} `;
59
+
60
+ const USAGE = `${_('usage: kg-ssot-validate <file.kg.yaml|file.kg.json>... [--json] [--corpus DIR]')}\n`;
15
61
  const args = process.argv.slice(2);
16
62
  let json = false;
17
63
  let corpusDir = null;
@@ -25,12 +71,12 @@ for (let i = 0; i < args.length; i += 1) {
25
71
  else if (a === '--corpus') {
26
72
  corpusDir = args[++i];
27
73
  if (!corpusDir) {
28
- process.stdout.write(`ENTRADA QUEBRADA --corpus sem diretório\n${USAGE}`);
74
+ process.stdout.write(`${_('BROKEN INPUT')} ${_('--corpus without a directory')}\n${USAGE}`);
29
75
  process.exit(2);
30
76
  }
31
77
  } else if (a === '--') files.push(...args.slice(i + 1)), (i = args.length);
32
78
  else if (a.startsWith('-')) {
33
- process.stdout.write(`ENTRADA QUEBRADA opção desconhecida: ${a}\n${USAGE}`);
79
+ process.stdout.write(`${_('BROKEN INPUT')} ${_('unknown option: {option}', { option: a })}\n${USAGE}`);
34
80
  process.exit(2);
35
81
  } else files.push(a);
36
82
  }
@@ -43,7 +89,8 @@ let corpus = null;
43
89
  try {
44
90
  corpus = corpusDir ? corpusIndex(corpusDir) : null;
45
91
  } catch (exc) {
46
- process.stdout.write(`ENTRADA QUEBRADA ${exc.message}\n`);
92
+ const msg = exc?.kgMsgid ? _(exc.kgMsgid, exc.kgArgs) : exc?.message;
93
+ process.stdout.write(`${_('BROKEN INPUT')} ${msg}\n`);
47
94
  process.exit(2);
48
95
  }
49
96
  for (const file of files) {
@@ -57,9 +104,9 @@ for (const file of files) {
57
104
  if (out.verdict === 'error') rc = 2;
58
105
  else if (out.verdict === 'reject' && rc === 0) rc = 1;
59
106
  if (json) process.stdout.write(`${JSON.stringify({ file, ...out })}\n`);
60
- else if (out.verdict === 'accept') process.stdout.write(`PASSA ${file}\n`);
61
- else if (out.verdict === 'reject') process.stdout.write(`REPROVA ${file}\n MUST ${out.codes.join(' · ')}\n`);
62
- else process.stdout.write(`ERRO ${file}\n ${out.detail}\n`);
63
- if (!json && out.warnings?.length) process.stdout.write(` AVISO ${out.warnings.join(' · ')}\n`);
107
+ else if (out.verdict === 'accept') process.stdout.write(`${label('PASS')}${file}\n`);
108
+ else if (out.verdict === 'reject') process.stdout.write(`${label('FAIL')}${file}\n MUST ${out.codes.join(' · ')}\n`);
109
+ else process.stdout.write(`${label('ERROR')}${file}\n ${out.detail}\n`);
110
+ if (!json && out.warnings?.length) process.stdout.write(` ${_('WARNING')} ${out.warnings.join(' · ')}\n`);
64
111
  }
65
112
  process.exit(rc);
package/kg.mjs CHANGED
@@ -493,7 +493,8 @@ export function formCodes(err, doc) {
493
493
  }
494
494
  scope = path.length >= 2 ? scopeOf(path) : 'top';
495
495
  if (path.length && typeof path[path.length - 1] === 'number') {
496
- const lead = scope === 'top' ? path.slice(0, -1).filter(notIndex) : [];
496
+ // num item de lista dentro do nó (x_term.labels, v4.11), o caminho até a lista entra no código
497
+ const lead = scope === 'top' ? path.slice(0, -1).filter(notIndex) : path.length > 2 ? innerNames(path.slice(0, -1)) : [];
497
498
  return [`form.${kind}.${scope}.${[...lead, 'item'].join('.')}`];
498
499
  }
499
500
  const names = scope !== 'top' ? innerNames(path) : path.filter(notIndex);