kg-ssot 4.9.1 → 4.11.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
@@ -3,16 +3,16 @@
3
3
  **O leitor JavaScript do contrato do KG-SSOT, o grafo de conhecimento verificável: toda afirmação diz de onde veio,
4
4
  quando foi conferida e o que depende dela.**
5
5
 
6
- Este pacote julga um grafo `.kg.yaml` ou `.kg.json` contra o contrato vigente e devolve o veredito MUST com os
7
- códigos estáveis da suíte de conformidade. Ele traz:
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
8
  - o **núcleo do leitor**, sem `fs` e sem processo;
9
- - o **validador do contrato pré-compilado**, que não usa `new Function` nem `eval`. Por isso roda em motores sem
10
- geração de código, como o Hermes do React Native e do Expo, e no Node;
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
11
  - os **schemas** do contrato e do veredito.
12
12
 
13
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 e os códigos esperados. O CI confere isso pelo pacote
15
- instalado a cada mudança no leitor.
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.
16
16
 
17
17
  ## Instalar
18
18
 
@@ -27,19 +27,23 @@ import { verdict } from 'kg-ssot';
27
27
 
28
28
  verdict(texto); // .kg.yaml (o default)
29
29
  verdict(texto, { format: 'json' }); // .kg.json
30
- // { verdict: 'accept', codes: [], warnings: null, detail: '' }
31
- // { verdict: 'reject', codes: ['form.range.node.impact'], warnings: null, detail: '' }
30
+ // { verdict: 'accept', codes: [], warnings: [], detail: '' }
31
+ // { verdict: 'accept', codes: [], warnings: ['yaml.unquoted-date'], detail: '' }
32
+ // { verdict: 'reject', codes: ['form.range.node.impact'], warnings: [], detail: '' }
32
33
  ```
33
34
 
34
35
  - `verdict(texto, { format, corpus })` devolve o veredito no contrato de runner da suíte: `verdict` é `accept` ou
35
- `reject`, e `codes` traz os códigos MUST, ordenados e sem repetição.
36
- - `check(texto, { format, corpus })` devolve só os códigos (vazio quando passa).
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.
37
40
  - `kg-ssot/core` é o núcleo sem o validador, para quem compila o schema por conta própria. `kg-ssot/node` traz os
38
41
  auxiliares de Node, como `corpusIndex(dir)`, que indexa os `.kg.yaml` de um diretório para conferir as
39
42
  `external_edges`. Os schemas ficam em `kg-ssot/spec/*`.
40
43
 
41
- Este leitor mede só o MUST: `warnings` sai nulo. Para os avisos SHOULD, a linha de cada ocorrência e as dicas de
42
- correção, use o leitor de referência em Python.
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. 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.
43
47
 
44
48
  ## TypeScript e CommonJS
45
49
 
@@ -55,11 +59,13 @@ npx kg-ssot-validate grafo.kg.yaml outro.kg.json
55
59
 
56
60
  ```text
57
61
  PASSA grafo.kg.yaml
62
+ AVISO yaml.unquoted-date
58
63
  REPROVA outro.kg.json
59
64
  MUST form.range.node.impact · integrity.orphan-node
60
65
  ```
61
66
 
62
- O rc é 0 quando todos passam, 1 quando algum reprova e 2 com arquivo ilegível ou opção desconhecida. Com `--json`, sai uma linha JSON por
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
63
69
  arquivo. Com `--corpus DIR`, a ponta externa das `external_edges` é conferida contra os `.kg.yaml` sob `DIR`.
64
70
 
65
71
  ## Num app React Native ou Expo
package/cli.mjs CHANGED
@@ -4,7 +4,7 @@
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). --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 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
8
8
  // externa das external_edges contra os .kg.yaml sob DIR.
9
9
  import { readFileSync } from 'node:fs';
10
10
 
@@ -60,5 +60,6 @@ for (const file of files) {
60
60
  else if (out.verdict === 'accept') process.stdout.write(`PASSA ${file}\n`);
61
61
  else if (out.verdict === 'reject') process.stdout.write(`REPROVA ${file}\n MUST ${out.codes.join(' · ')}\n`);
62
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`);
63
64
  }
64
65
  process.exit(rc);
package/core.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  // Tipos de kg-ssot/core: o núcleo do leitor, sem o validador pré-compilado (para quem compila o schema).
2
2
  export type { Corpus, Format, MustValidator, Options, Verdict } from './index.mjs';
3
- export { codes, indexTexts, parse, verdictOf } from './index.mjs';
3
+ export { codes, coherenceCodes, indexTexts, parse, verdictOf, warnings, yamlProfileCodes } from './index.mjs';
package/index.d.mts CHANGED
@@ -11,15 +11,16 @@ export interface Options {
11
11
  corpus?: Corpus | null;
12
12
  }
13
13
 
14
- /** O veredito no contrato de runner da suíte. Este leitor mede só o MUST: warnings é sempre null. */
14
+ /** O veredito no contrato de runner da suíte: codes são os MUST (reprovam) e warnings os SHOULD (só avisam). */
15
15
  export interface Verdict {
16
16
  verdict: 'accept' | 'reject';
17
17
  codes: string[];
18
- warnings: null;
18
+ /** Os avisos SHOULD; null só no verdictOf sem validateShould (o leitor não mediu o SHOULD). */
19
+ warnings: string[] | null;
19
20
  detail: string;
20
21
  }
21
22
 
22
- /** Um validador do schema MUST compilado (o pré-compilado do pacote ou um do ajv). */
23
+ /** Um validador de schema compilado (o pré-compilado do pacote ou um do ajv), MUST ou SHOULD. */
23
24
  export interface MustValidator {
24
25
  (data: unknown): boolean;
25
26
  errors?: unknown[] | null;
@@ -28,14 +29,32 @@ export interface MustValidator {
28
29
  /** O validador MUST do contrato vigente, pré-compilado sem new Function nem eval. */
29
30
  export const validateMust: MustValidator;
30
31
 
31
- /** O veredito do contrato vigente para o texto de um grafo. */
32
+ /** O validador SHOULD do contrato vigente, pré-compilado sem new Function nem eval. */
33
+ export const validateShould: MustValidator;
34
+
35
+ /** O veredito do contrato vigente para o texto de um grafo, com os avisos SHOULD. */
32
36
  export function verdict(text: string, options?: Options): Verdict;
33
37
 
34
38
  /** Os códigos MUST do texto de um grafo, ordenados e sem repetição; vazio quando passa. */
35
39
  export function check(text: string, options?: Options): string[];
36
40
 
37
- /** O veredito com um validador MUST qualquer (o núcleo, para quem compila o schema). */
38
- export function verdictOf(text: string, validate: MustValidator, options?: Options): Verdict;
41
+ /** Os códigos SHOULD (avisos) do texto de um grafo, ordenados e sem repetição; vazio quando não há aviso. */
42
+ export function checkWarnings(text: string, options?: { format?: Format }): string[];
43
+
44
+ /** O veredito com um validador MUST qualquer (o núcleo, para quem compila o schema); com validateShould, mede também
45
+ * os avisos. */
46
+ export function verdictOf(text: string, validate: MustValidator,
47
+ options?: Options & { validateShould?: MustValidator }): Verdict;
48
+
49
+ /** Os avisos de coerência de um documento já lido (integrity.testimony-in-prod, verified-before-fact, untraced-decision, refutes-unreconciled). */
50
+ export function coherenceCodes(doc: Record<string, unknown>): string[];
51
+
52
+ /** Os avisos do perfil JSON num .kg.yaml já lido (yaml.unsafe-integer, yaml.lone-surrogate, yaml.depth-over-128). */
53
+ export function yamlProfileCodes(doc: unknown): string[];
54
+
55
+ /** Os códigos SHOULD com os validadores MUST e SHOULD quaisquer. */
56
+ export function warnings(text: string, validate: MustValidator, validateShould: MustValidator,
57
+ options?: { format?: Format }): string[];
39
58
 
40
59
  /** Os códigos MUST com um validador MUST qualquer. */
41
60
  export function codes(text: string, validate: MustValidator, options?: Options): string[];
package/index.mjs CHANGED
@@ -1,17 +1,24 @@
1
- // Entrada do pacote npm kg-ssot: o núcleo do leitor (kg.mjs, sem fs nem geração de código) com o validador MUST do
2
- // contrato já pré-compilado (must.mjs, sem new Function nem eval). Serve a Node e a bundles de React Native/Expo.
1
+ // Entrada do pacote npm kg-ssot: o núcleo do leitor (kg.mjs, sem fs nem geração de código) com os validadores MUST e
2
+ // SHOULD do contrato já pré-compilados (must.mjs e should.mjs, sem new Function nem eval). Serve a Node e a bundles
3
+ // de React Native/Expo.
3
4
  //
4
5
  // import { verdict } from 'kg-ssot';
5
- // verdict(texto, { format: 'yaml' }) // { verdict: 'accept' | 'reject', codes: [...], warnings: null, detail: '' }
6
+ // verdict(texto, { format: 'yaml' }) // { verdict: 'accept' | 'reject', codes: [...], warnings: [...], detail: '' }
6
7
  import validateMust from './must.mjs';
7
- import { codes as codesWith, verdictOf } from './kg.mjs';
8
+ import validateShould from './should.mjs';
9
+ import { codes as codesWith, verdictOf, warnings as warningsWith } from './kg.mjs';
8
10
 
9
11
  export * from './kg.mjs';
10
- export { validateMust };
12
+ export { validateMust, validateShould };
11
13
 
12
- /** O veredito do contrato vigente para o texto de um grafo; format é 'yaml' (o default) ou 'json'. */
14
+ /** O veredito do contrato vigente para o texto de um grafo (MUST e SHOULD); format é 'yaml' (o default) ou 'json'. */
13
15
  export function verdict(text, { format = 'yaml', corpus = null } = {}) {
14
- return verdictOf(text, validateMust, { format, corpus });
16
+ return verdictOf(text, validateMust, { format, corpus, validateShould });
17
+ }
18
+
19
+ /** Os códigos SHOULD (avisos) do texto de um grafo, ordenados e sem repetição; vazio quando não há aviso. */
20
+ export function checkWarnings(text, { format = 'yaml' } = {}) {
21
+ return warningsWith(text, validateMust, validateShould, { format });
15
22
  }
16
23
 
17
24
  /** Os códigos MUST do texto de um grafo, ordenados e sem repetição; vazio quando passa. */
package/kg.mjs CHANGED
@@ -9,8 +9,11 @@
9
9
  // - formCodes: os erros do JSON Schema traduzidos para form.<tipo>.<escopo>.<campo> (form_codes);
10
10
  // - unsafeIntegerCodes: o inteiro fora de ±(2^53−1) no .kg.json, form.type no caminho do .nan (unsafe_integer_codes);
11
11
  // - integrity: ids únicos, pontas de aresta, órfãos e external_edges (integrity_located);
12
- // - codes: tudo junto, ordenado e sem repetição (code_counts/codes).
13
- import { parseAllDocuments, isAlias, isMap, isPair, isScalar, isSeq } from 'yaml';
12
+ // - codes: tudo junto, ordenado e sem repetição (code_counts/codes);
13
+ // - warnings: os avisos SHOULD (warnings da referência): o schema SHOULD sem o que o MUST já acusa, a data sem
14
+ // aspas, os três avisos do perfil no .kg.yaml (yaml.unsafe-integer, yaml.lone-surrogate, yaml.depth-over-128)
15
+ // e os de coerência (coherence_located).
16
+ import { Composer, Parser, isAlias, isMap, isPair, isScalar, isSeq } from 'yaml';
14
17
 
15
18
  // format: date é asserção (ADR 002): AAAA-MM-DD que existe no calendário, ano de 0001 a 9999, como o
16
19
  // datetime.date.fromisoformat do leitor de referência. É uma regex, e não uma função, para que o validador
@@ -24,13 +27,22 @@ export const DATE_RE = new RegExp(
24
27
  export const AJV_OPTIONS = { allErrors: true, strict: false, verbose: true, formats: { date: DATE_RE } };
25
28
 
26
29
  const MAX_DEPTH = 1000; // coleções abertas de uma vez (MAX_DEPTH da referência)
27
- // coleções aninhadas que o perfil do modelo admite (PROFILE_DEPTH da referência): MUST no .kg.json. No .kg.yaml a
28
- // referência só avisa (yaml.depth-over-128), e este leitor não mede aviso: lá ele deixa passar.
30
+ // coleções aninhadas que o perfil do modelo admite (PROFILE_DEPTH da referência): MUST no .kg.json. No .kg.yaml é
31
+ // só aviso (yaml.depth-over-128), medido por warnings().
29
32
  const PROFILE_DEPTH = 128;
30
33
  const SLUG = /^[A-Za-z_][A-Za-z0-9_-]*$/;
31
34
  const EXTERNAL_REF = /^(?:[A-Za-z0-9_][A-Za-z0-9_.-]*\/)*[A-Za-z0-9_][A-Za-z0-9_.-]*\.kg\.yaml#[A-Za-z][A-Za-z0-9_]*$/;
32
35
  const CORE_INT = /^(?:[-+]?[0-9]+|0o[0-7]+|0x[0-9a-fA-F]+)$/;
33
36
  const Y = 'tag:yaml.org,2002:';
37
+ // O timestamp do YAML 1.1 (o resolvedor implícito do PyYAML, que o Yaml12CoreLoader da referência mantém): no 1.2
38
+ // core ele é texto, mas um leitor 1.1 o leria como data, e por isso a forma sem aspas avisa (yaml.unquoted-date).
39
+ const YAML11_TIMESTAMP = new RegExp('^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
40
+ + '|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?(?:[Tt]|[ \\t]+)[0-9][0-9]?:[0-9][0-9]:[0-9][0-9](?:\\.[0-9]*)?'
41
+ + '(?:[ \\t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$');
42
+
43
+ // O !!timestamp explícito aceito pelo perfil (STRICT_TIMESTAMP da referência): a interseção do construtor do PyYAML com
44
+ // o do yaml, sem tab entre a data e a hora (decisão de contrato de 2026-10-19).
45
+ const STRICT_TIMESTAMP = /^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:(?:[Tt]| +)[0-9]{1,2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?(?: *(?:Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$/;
34
46
 
35
47
  class ParseError extends Error {
36
48
  constructor(code) { super(code); this.code = code; }
@@ -96,7 +108,11 @@ function scalarOf(node) {
96
108
  const big = src.startsWith('0o') ? BigInt(src) : BigInt(src.replace(/^\+/, ''));
97
109
  return { value: Number(big), key: big.toString(), int: true };
98
110
  }
99
- if (tag === `${Y}timestamp`) return { value: String(node.source ?? value), key: String(node.source ?? value) };
111
+ if (tag === `${Y}timestamp`) {
112
+ const src = String(node.source ?? value);
113
+ if (!STRICT_TIMESTAMP.test(src)) throw new ParseError('parse.yaml-error'); // !!timestamp sem data na forma estrita
114
+ return { value: src, key: src };
115
+ }
100
116
  if (tag === `${Y}binary`) return { value: BYTES, key: "b''" };
101
117
  if (value === null || value === undefined) return { value: null, key: 'null' };
102
118
  if (typeof value === 'boolean') return { value, key: String(value) };
@@ -137,9 +153,16 @@ function tooDeep(root) {
137
153
  // Converte a árvore de um documento no valor que a referência constrói. Alias compartilha o valor (como o
138
154
  // PyYAML), âncora repetida e alias sem âncora são erro de composição, chave composta é erro, e chave repetida
139
155
  // é comparada na forma JSON (1 e "1" colidem; 1 e 1.0, não).
140
- function convertDocument(doc) {
156
+ function convertDocument(doc, marks = {}) {
141
157
  const anchors = new Map();
142
158
  const built = new Map();
159
+ // a data sem aspas, em chave ou valor: escalar plain na forma do timestamp 1.1, ou com a tag !!timestamp
160
+ const noteDate = (node) => {
161
+ if (node.tag === `${Y}timestamp`
162
+ || (!node.tag && node.type === 'PLAIN' && typeof node.value === 'string' && YAML11_TIMESTAMP.test(node.value))) {
163
+ marks.unquotedDate = true;
164
+ }
165
+ };
143
166
  const walk = (node) => {
144
167
  if (node === null || node === undefined) return null;
145
168
  if (isAlias(node)) {
@@ -152,6 +175,7 @@ function convertDocument(doc) {
152
175
  anchors.set(node.anchor, node);
153
176
  }
154
177
  if (isScalar(node)) {
178
+ noteDate(node);
155
179
  const v = scalarOf(node).value;
156
180
  built.set(node, v);
157
181
  return v;
@@ -179,6 +203,7 @@ function convertDocument(doc) {
179
203
  let key = 'null';
180
204
  if (keyNode !== null && keyNode !== undefined) {
181
205
  if (!isScalar(keyNode)) throw new ParseError('parse.yaml-error'); // chave composta (mapa ou lista)
206
+ noteDate(keyNode);
182
207
  key = scalarOf(keyNode).key;
183
208
  }
184
209
  if (seen.has(key)) throw new ParseError('parse.duplicate-key');
@@ -216,23 +241,43 @@ const codePoints = (text) => {
216
241
  return n;
217
242
  };
218
243
 
219
- /** O parse do .kg.yaml no perfil do contrato: { doc } ou { code: 'parse.*' }. */
244
+ // O que o perfil recusa já na leitura dos tokens, como o parser puro do PyYAML da referência
245
+ // (decisão de contrato de 2026-10-19, «o mais estrito nos três»): o tab fora de aspas, de bloco e de comentário (no
246
+ // espaço entre tokens ou dentro de um escalar plain) e a tag ! sem nome, também na forma !<!>. Sem recursão.
247
+ function strictTokenError(tokens) {
248
+ const stack = [...tokens];
249
+ while (stack.length) {
250
+ const t = stack.pop();
251
+ if (Array.isArray(t)) { for (const x of t) stack.push(x); continue; } // sem spread: lista enorme estoura a pilha
252
+ if (t === null || typeof t !== 'object') continue;
253
+ if (typeof t.source === 'string') {
254
+ if ((t.type === 'space' || t.type === 'scalar' || t.type === 'directive') && t.source.includes('\t')) return true;
255
+ if (t.type === 'tag' && (t.source === '!' || t.source === '!<!>')) return true;
256
+ }
257
+ for (const v of Object.values(t)) if (v !== null && typeof v === 'object') stack.push(v);
258
+ }
259
+ return false;
260
+ }
261
+
262
+ /** O parse do .kg.yaml no perfil do contrato: { doc, unquotedDate } ou { code: 'parse.*' }. */
220
263
  export function parseYaml(text) {
221
264
  let docs;
222
265
  try {
223
- docs = parseAllDocuments(text, {
224
- version: '1.2', schema: 'core', uniqueKeys: false, merge: false, maxAliasCount: -1, prettyErrors: false,
225
- });
266
+ const tokens = Array.from(new Parser().parse(text));
267
+ if (strictTokenError(tokens)) return { code: 'parse.yaml-error' };
268
+ docs = Array.from(new Composer({
269
+ version: '1.2', schema: 'core', uniqueKeys: false, merge: false, maxAliasCount: -1,
270
+ }).compose(tokens));
226
271
  } catch {
227
272
  return { code: 'parse.yaml-error' }; // aninhamento que estoura a pilha do parser, entre outros
228
273
  }
229
- if (!Array.isArray(docs)) docs = [docs]; // EmptyStream é lista; um documento só também vem como lista
230
274
  const raw = [];
275
+ const marks = {};
231
276
  try {
232
277
  for (const d of docs) {
233
278
  if (d.errors.length) return { code: 'parse.yaml-error' };
234
279
  if (d.contents && tooDeep(d.contents)) return { code: 'parse.yaml-error' };
235
- raw.push(convertDocument(d));
280
+ raw.push(convertDocument(d, marks));
236
281
  }
237
282
  } catch (exc) {
238
283
  if (exc instanceof ParseError) return { code: exc.code };
@@ -243,7 +288,7 @@ export function parseYaml(text) {
243
288
  if (!present.length) return { code: 'parse.empty-document' };
244
289
  if (raw.length !== 1) return { code: 'parse.not-single-document' };
245
290
  if (!isObj(raw[0])) return { code: 'parse.root-not-mapping' };
246
- return { doc: raw[0] };
291
+ return { doc: raw[0], unquotedDate: Boolean(marks.unquotedDate) };
247
292
  }
248
293
 
249
294
  // ---------------------------------------------------------------- JSON
@@ -403,8 +448,9 @@ export function formCodes(err, doc) {
403
448
  let scope = path.length ? scopeOf(path) : 'top';
404
449
  const prefix = scope !== 'top' ? innerNames(path) : path.filter(notIndex);
405
450
  if (err.propertyName !== undefined) { // dentro de propertyNames: o nome da chave
406
- if (err.keyword === 'pattern') return [`form.pattern.${scope}.${[...prefix, 'key'].join('.')}`];
407
- if (!SLUG.test(err.propertyName)) return [];
451
+ // só o nome que não é slug é form.pattern (<<, 1.0, chave com espaço). O ajv achata os ramos do anyOf (enum OU
452
+ // ^x_): o pattern ^x_ que falha num nome slug é a mesma chave desconhecida, como no form_codes da referência
453
+ if (!SLUG.test(err.propertyName)) return err.keyword === 'pattern' ? [`form.pattern.${scope}.${[...prefix, 'key'].join('.')}`] : [];
408
454
  return [`form.unknown-key.${scope}.${[...prefix, err.propertyName].join('.')}`];
409
455
  }
410
456
  let kind = FORM_TYPE[err.keyword] || err.keyword;
@@ -531,13 +577,20 @@ export function integrity(doc, corpus = null, refs = ['trigger', 'on']) {
531
577
  /** Os códigos MUST de um documento já parseado: lista ordenada, sem repetição. format 'json': o inteiro inseguro
532
578
  * também reprova (no YAML ele só avisaria, e este leitor não mede aviso). */
533
579
  export function docCodes(doc, validate, corpus = null, format = 'yaml') {
534
- const out = new Set();
535
- if (!validate(doc)) for (const err of validate.errors || []) for (const c of formCodes(err, doc)) out.add(c);
580
+ return docCodesWithForm(doc, validate, corpus, format).codes;
581
+ }
582
+
583
+ // docCodes e, à parte, os form.* que o próprio validador MUST acusou (o `must` do warning_counts da referência, sem
584
+ // os do .nan nem os do inteiro inseguro).
585
+ function docCodesWithForm(doc, validate, corpus, format) {
586
+ const mustForm = new Set();
587
+ if (!validate(doc)) for (const err of validate.errors || []) for (const c of formCodes(err, doc)) mustForm.add(c);
588
+ const out = new Set(mustForm);
536
589
  for (const c of nonFiniteCodes(doc)) out.add(c);
537
590
  if (format === 'json') for (const c of unsafeIntegerCodes(doc)) out.add(c);
538
591
  for (const c of integrity(doc, corpus)) out.add(c);
539
592
  if (located(doc, 'edges').some((e) => has(e, 'on'))) out.add('yaml.forbidden-key-on');
540
- return [...out].sort();
593
+ return { codes: [...out].sort(), mustForm };
541
594
  }
542
595
 
543
596
  /** O parse do formato pedido: 'yaml' (o default) ou 'json'. */
@@ -562,8 +615,109 @@ export function indexTexts(items) {
562
615
  return index;
563
616
  }
564
617
 
565
- /** A linha do contrato de runner para um texto: { verdict, codes, warnings: null, detail }. */
618
+ // ---------------------------------------------------------------- avisos (SHOULD)
619
+
620
+ const ISO_PREFIX = /^[0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?$/;
621
+ const TESTIMONY_METHOD = /^testemunho: /;
622
+ // Só espaço no sentido do str.isspace do Python (o strip da referência): o trim() do JS tira o U+FEFF, que o Python
623
+ // não tira, e não tira \x1c a \x1f nem o \x85, que o Python tira
624
+ const PY_BLANK = /^[\t-\r\x1c-\x20\x85\xa0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*$/;
625
+
626
+ /** Algum VALOR sob value (ele incluído; as chaves não) satisfaz hit? Sem recursão (_contains da referência). */
627
+ function containsValue(value, hit) {
628
+ const stack = [value];
629
+ while (stack.length) {
630
+ const v = stack.pop();
631
+ if (hit(v)) return true;
632
+ if (Array.isArray(v)) stack.push(...v);
633
+ else if (isObj(v)) stack.push(...Object.values(v));
634
+ }
635
+ return false;
636
+ }
637
+
638
+ /** Os avisos de coerência, que cruzam campos ou arestas (coherence_located da referência), um por ocorrência:
639
+ * integrity.testimony-in-prod, integrity.verified-before-fact, integrity.untraced-decision e
640
+ * integrity.refutes-unreconciled. */
641
+ export function coherenceCodes(doc) {
642
+ const out = [];
643
+ const edges = Array.isArray(doc.edges) ? doc.edges : [];
644
+ const traced = new Set(edges.filter((e) => isObj(e) && get(e, 'edge_type') === 'TRACES_TO' && typeof get(e, 'from') === 'string')
645
+ .map((e) => e.from));
646
+ const refuted = new Set(edges.filter((e) => isObj(e) && get(e, 'edge_type') === 'REFUTES' && typeof get(e, 'to') === 'string')
647
+ .map((e) => e.to));
648
+ for (const n of located(doc, 'nodes')) {
649
+ const prov = isObj(get(n, 'provenance')) ? n.provenance : {};
650
+ const method = get(prov, 'method');
651
+ if (get(n, 'plane') === 'PROD'
652
+ && (get(n, 'evidence_class') === 'testimony' || (typeof method === 'string' && TESTIMONY_METHOD.test(method)))) {
653
+ out.push('integrity.testimony-in-prod');
654
+ }
655
+ const vf = get(n, 'valid_from');
656
+ const va = get(n, 'verified_at');
657
+ if (typeof vf === 'string' && typeof va === 'string' && ISO_PREFIX.test(vf) && ISO_PREFIX.test(va)) {
658
+ const k = Math.min(vf.length, va.length);
659
+ if (va.slice(0, k) < vf.slice(0, k)) out.push('integrity.verified-before-fact');
660
+ }
661
+ const trace = get(n, 'trace');
662
+ if (get(n, 'node_type') === 'decision' && !['superseded', 'refuted'].includes(get(n, 'status'))
663
+ && !(typeof trace === 'string' && !PY_BLANK.test(trace)) && !Object.keys(prov).length
664
+ && !(typeof get(n, 'id') === 'string' && traced.has(n.id))) {
665
+ out.push('integrity.untraced-decision');
666
+ }
667
+ const status = get(n, 'status');
668
+ if (typeof get(n, 'id') === 'string' && refuted.has(n.id) && !['superseded', 'refuted'].includes(status)
669
+ && !(status === 'done' && get(n, 'node_type') === 'question')) {
670
+ out.push('integrity.refutes-unreconciled');
671
+ }
672
+ }
673
+ return out;
674
+ }
675
+
676
+ /** Os avisos do perfil JSON no .kg.yaml (yaml_profile_warnings): no .kg.json eles são MUST e reprovam antes. */
677
+ export function yamlProfileCodes(doc) {
678
+ const out = [];
679
+ if (containsValue(doc, isUnsafeInteger)) out.push('yaml.unsafe-integer');
680
+ if (hasLoneSurrogate(doc)) out.push('yaml.lone-surrogate');
681
+ if (collectionDepth(doc) > PROFILE_DEPTH) out.push('yaml.depth-over-128');
682
+ return out;
683
+ }
684
+
685
+ /** Os códigos SHOULD de um texto (warnings da referência), ordenados e sem repetição: o que o schema SHOULD acusa e o
686
+ * MUST não, a data sem aspas e os avisos do perfil (só no .kg.yaml) e os de coerência. Texto que não parseia não
687
+ * recebe aviso. */
688
+ export function warnings(text, validate, validateShould, { format = 'yaml' } = {}) {
689
+ const parsed = parse(text, format);
690
+ return parsed.code ? [] : warningsOf(parsed, validate, validateShould, format);
691
+ }
692
+
693
+ // Os avisos de um texto já parseado. mustCodes: os form.* do MUST, se quem chama já os tem (o verdictOf, para não
694
+ // validar duas vezes).
695
+ function warningsOf(parsed, validate, validateShould, format, mustCodes = null) {
696
+ const { doc } = parsed;
697
+ let must = mustCodes;
698
+ if (must === null) {
699
+ must = new Set();
700
+ if (!validate(doc)) for (const err of validate.errors || []) for (const c of formCodes(err, doc)) must.add(c);
701
+ }
702
+ const out = new Set();
703
+ if (!validateShould(doc)) for (const err of validateShould.errors || []) for (const c of formCodes(err, doc)) if (!must.has(c)) out.add(c);
704
+ if (format !== 'json') {
705
+ if (parsed.unquotedDate) out.add('yaml.unquoted-date');
706
+ for (const c of yamlProfileCodes(doc)) out.add(c);
707
+ }
708
+ for (const c of coherenceCodes(doc)) out.add(c);
709
+ return [...out].sort();
710
+ }
711
+
712
+ /** A linha do contrato de runner para um texto: { verdict, codes, warnings, detail }. Sem validateShould, warnings
713
+ * é null: o leitor não mediu o SHOULD. */
566
714
  export function verdictOf(text, validate, opts = {}) {
567
- const found = codes(text, validate, opts);
568
- return { verdict: found.length ? 'reject' : 'accept', codes: found, warnings: null, detail: '' };
715
+ const { format = 'yaml', corpus = null, validateShould = null } = opts;
716
+ const parsed = parse(text, format); // um parse só para o MUST e o SHOULD (o parse é quase todo o custo)
717
+ if (parsed.code) {
718
+ return { verdict: 'reject', codes: [parsed.code], warnings: validateShould ? [] : null, detail: '' };
719
+ }
720
+ const { codes: found, mustForm } = docCodesWithForm(parsed.doc, validate, corpus, format);
721
+ const warned = validateShould ? warningsOf(parsed, validate, validateShould, format, mustForm) : null;
722
+ return { verdict: found.length ? 'reject' : 'accept', codes: found, warnings: warned, detail: '' };
569
723
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kg-ssot",
3
- "version": "4.9.1",
3
+ "version": "4.11.0",
4
4
  "description": "Leitor JS do contrato do grafo de conhecimento verificável KG-SSOT (.kg.yaml e .kg.json): validador pré-compilado, sem eval, para Node e React Native/Expo.",
5
5
  "keywords": [
6
6
  "knowledge-graph",
@@ -36,7 +36,14 @@
36
36
  "types": "./core.d.mts",
37
37
  "default": "./kg.mjs"
38
38
  },
39
- "./must": "./must.mjs",
39
+ "./must": {
40
+ "types": "./validator.d.mts",
41
+ "default": "./must.mjs"
42
+ },
43
+ "./should": {
44
+ "types": "./validator.d.mts",
45
+ "default": "./should.mjs"
46
+ },
40
47
  "./node": {
41
48
  "types": "./node.d.mts",
42
49
  "default": "./run.mjs"
@@ -53,6 +60,8 @@
53
60
  "kg.mjs",
54
61
  "core.d.mts",
55
62
  "must.mjs",
63
+ "should.mjs",
64
+ "validator.d.mts",
56
65
  "run.mjs",
57
66
  "node.d.mts",
58
67
  "cli.mjs",
package/run.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  // Runner da matriz para o leitor JS do contrato (tools/runners/ajv/kg.mjs + ajv 2020-12 + yaml 2.x).
2
2
  // Contrato de runner em spec/conformance/README.md: imprime UMA linha JSON {verdict, codes, warnings, detail}.
3
3
  //
4
- // node tools/runners/ajv/run.mjs <caso> [--corpus DIR] [--precompiled ARQ]
4
+ // node tools/runners/ajv/run.mjs <caso> [--corpus DIR] [--precompiled ARQ] [--precompiled-should ARQ]
5
5
  //
6
- // O caso pode ser .kg.yaml (ou .yaml) ou .kg.json. O schema MUST do contrato vigente é lido de spec/ a cada
7
- // execução, sem cópia, e compilado pelo ajv; com --precompiled, o validador vem de um módulo gerado por
8
- // standalone.mjs (ajv/dist/standalone), sem compilar nada em tempo de execução. --corpus DIR liga a
9
- // conferência da ponta externa das external_edges contra os .kg.yaml sob DIR (vazio: o caso não é de corpus).
10
- // warnings é null: este leitor não mede o SHOULD.
6
+ // O caso pode ser .kg.yaml (ou .yaml) ou .kg.json. Os schemas MUST e SHOULD do contrato vigente são lidos de spec/
7
+ // a cada execução, sem cópia, e compilados pelo ajv; com --precompiled e --precompiled-should, os validadores vêm de
8
+ // módulos gerados por standalone.mjs (ajv/dist/standalone), sem compilar nada em tempo de execução. --corpus DIR
9
+ // liga a conferência da ponta externa das external_edges contra os .kg.yaml sob DIR (vazio: o caso não é de corpus).
10
+ // warnings são os avisos SHOULD, como os da referência.
11
11
  import { execFileSync } from 'node:child_process';
12
12
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
13
13
  import { dirname, join, relative, resolve, sep } from 'node:path';
@@ -20,17 +20,20 @@ import { AJV_OPTIONS, indexTexts, verdictOf } from './kg.mjs';
20
20
  const HERE = dirname(fileURLToPath(import.meta.url));
21
21
  export const ROOT = existsSync(join(HERE, 'spec', 'release.json')) ? HERE : resolve(HERE, '../../..');
22
22
  export const CONTRACT_MUST = join(ROOT, 'spec', 'kg-contract-v4.schema.json');
23
+ export const CONTRACT_SHOULD = join(ROOT, 'spec', 'kg-contract-v4.should.schema.json');
23
24
 
24
25
  function arg(name) {
25
26
  const i = process.argv.indexOf(name);
26
27
  return i > 0 ? process.argv[i + 1] : undefined;
27
28
  }
28
29
 
29
- export async function compileMust() {
30
+ export async function compileMust(schema = CONTRACT_MUST) {
30
31
  const { default: Ajv2020 } = await import('ajv/dist/2020.js');
31
- return new Ajv2020(AJV_OPTIONS).compile(JSON.parse(readFileSync(CONTRACT_MUST, 'utf8')));
32
+ return new Ajv2020(AJV_OPTIONS).compile(JSON.parse(readFileSync(schema, 'utf8')));
32
33
  }
33
34
 
35
+ export const compileShould = () => compileMust(CONTRACT_SHOULD);
36
+
34
37
  // Os .kg.yaml do corpus: os rastreados, se DIR é repo git (como o gate); senão, todo .kg.yaml sob DIR (corpus_index).
35
38
  function corpusFiles(root) {
36
39
  try {
@@ -69,13 +72,15 @@ async function main() {
69
72
  try {
70
73
  const file = process.argv[2];
71
74
  const pre = arg('--precompiled');
75
+ const preShould = arg('--precompiled-should');
72
76
  const validate = pre ? (await import(pathToFileURL(resolve(pre)).href)).default : await compileMust();
77
+ const validateShould = preShould ? (await import(pathToFileURL(resolve(preShould)).href)).default : await compileShould();
73
78
  // UTF-8 inválido é erro; o BOM fica no texto (ignoreBOM), como no read_text da referência: quem descarta UM BOM é o
74
79
  // parse, e um segundo BOM tem de chegar até ele
75
80
  const text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(readFileSync(file));
76
81
  const root = arg('--corpus');
77
82
  const corpus = root ? corpusIndex(root) : null;
78
- out = verdictOf(text, validate, { format: file.endsWith('.json') ? 'json' : 'yaml', corpus });
83
+ out = verdictOf(text, validate, { format: file.endsWith('.json') ? 'json' : 'yaml', corpus, validateShould });
79
84
  } catch (exc) {
80
85
  out = { verdict: 'error', codes: null, warnings: null, detail: `${exc?.name}: ${exc?.message}` };
81
86
  }
package/should.mjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";export const validate = validate20;export default validate20;const schema31 = {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"urn:kg-ssot:spec:kg-contract-v4.should.schema.json","title":"kg.yaml — contrato v4, nível SHOULD","description":"O contrato v4 do .kg.yaml, nível SHOULD (alerta, não reprova), o vigente. É o SHOULD v3 menos o que o v4 promoveu a MUST: a chave desconhecida sem x_ no topo, no meta, no nó e na aresta e o nó confirmed ou PROD sem provenance. Segue alertando: nome de chave que não é slug ([A-Za-z_][A-Za-z0-9_-]*), como form.pattern.<escopo>.key, que sem x_ também reprova no MUST; nó PROD sem verified_at; label do nó acima de 280 caracteres, form.range.node.label, porque a narrativa vai para narrative; a chave desconhecida dentro de provenance, fechada em source, locator, method e x_; e o grafo sem nós (nodes: []), como form.range.top.nodes: é válido, como o primeiro uso de um app, mas o aviso pega o parse que falhou em silêncio. No v4, provenance.method alerta (form.pattern.node.provenance.method) quando não segue a forma canônica '<classe>: <detalhe>', com a classe em medição, leitura, juízes, derivado, testemunho; reprova no v5. Medição feita por terceiro e lida pelo autor do grafo é testemunho. No v4.1 entram quatro avisos que o próprio arquivo decide. O schema acusa a claim com verified_at e sem verified_against (form.required.node.verified_against): carimbo sem alvo não distingue verificado de declarado. Só a claim: evidence é a própria âncora, artifact nomeia o alvo, question não afirma e a origem da decision é cobrada por integrity.untraced-decision. O leitor acusa três avisos que cruzam campos ou arestas, por isso não cabem no schema: integrity.testimony-in-prod (nó PROD cuja base é testemunho), integrity.verified-before-fact (verified_at anterior a valid_from) e integrity.untraced-decision (decision viva sem trace, sem provenance e sem aresta TRACES_TO saindo dela). No v4.2 entra provenance.locality, opcional, com o enum repo (arquivo versionado no próprio repo), web (endereço público), host (só existe numa máquina: journal local, caminho fora do repo) ou pessoa (relato de alguém): o valor fora do enum alerta como form.enum.node.provenance.locality. Serve para medir o que é reverificável por terceiros e para barrar host na publicação. No v4.4 entra o quarto aviso do leitor, integrity.refutes-unreconciled: nó que recebe REFUTES e segue vivo (nem superseded nem refuted; a question done já está respondida, e não alerta). O grafo afirma e refuta a mesma coisa; o radar do framework onde o formato nasceu reprova o caso como contradição. Aqui é SHOULD agora e vira MUST numa versão futura, depois que os adotantes medirem a própria dívida.","type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_-]*$"},"properties":{"meta":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_-]*$"}},"nodes":{"type":"array","items":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_-]*$"},"properties":{"label":{"maxLength":280},"provenance":{"propertyNames":{"allOf":[{"pattern":"^[A-Za-z_][A-Za-z0-9_-]*$"},{"anyOf":[{"enum":["locality","locator","method","source"]},{"pattern":"^x_"}]}]},"properties":{"method":{"pattern":"^(?:medição|leitura|juízes|derivado|testemunho): [^ \t\n\r]"},"locality":{"enum":["host","pessoa","repo","web"]}}}},"allOf":[{"if":{"properties":{"plane":{"const":"PROD"}},"required":["plane"]},"then":{"required":["verified_at"]}},{"if":{"properties":{"node_type":{"const":"claim"}},"required":["node_type","verified_at"]},"then":{"required":["verified_against"]}}]},"minItems":1},"edges":{"type":"array","items":{"type":"object","propertyNames":{"pattern":"^[A-Za-z_][A-Za-z0-9_-]*$"}}}}};const pattern4 = new RegExp("^[A-Za-z_][A-Za-z0-9_-]*$", "u");const pattern8 = new RegExp("^x_", "u");const pattern9 = new RegExp("^(?:medição|leitura|juízes|derivado|testemunho): [^ \t\n\r]", "u");import func1_mod from "ajv/dist/runtime/ucs2length.js";const func1 = func1_mod.default;function validate20(data, {instancePath="", parentData, parentDataProperty, rootData=data, dynamicAnchors={}}={}){/*# sourceURL="urn:kg-ssot:spec:kg-contract-v4.should.schema.json" */;let vErrors = null;let errors = 0;const evaluated0 = validate20.evaluated;if(evaluated0.dynamicProps){evaluated0.props = undefined;}if(evaluated0.dynamicItems){evaluated0.items = undefined;}if(data && typeof data == "object" && !Array.isArray(data)){for(const key0 in data){const _errs1 = errors;if(typeof key0 === "string"){if(!pattern4.test(key0)){const err0 = {instancePath,schemaPath:"#/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z_][A-Za-z0-9_-]*$"},message:"must match pattern \""+"^[A-Za-z_][A-Za-z0-9_-]*$"+"\"",schema:"^[A-Za-z_][A-Za-z0-9_-]*$",parentSchema:schema31.propertyNames,data:key0,propertyName:key0};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}}var valid0 = _errs1 === errors;if(!valid0){const err1 = {instancePath,schemaPath:"#/propertyNames",keyword:"propertyNames",params:{propertyName: key0},message:"property name must be valid",schema:schema31.propertyNames,parentSchema:schema31,data};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}if(data.meta !== undefined){let data0 = data.meta;if(data0 && typeof data0 == "object" && !Array.isArray(data0)){for(const key1 in data0){const _errs4 = errors;if(typeof key1 === "string"){if(!pattern4.test(key1)){const err2 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z_][A-Za-z0-9_-]*$"},message:"must match pattern \""+"^[A-Za-z_][A-Za-z0-9_-]*$"+"\"",schema:"^[A-Za-z_][A-Za-z0-9_-]*$",parentSchema:schema31.properties.meta.propertyNames,data:key1,propertyName:key1};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}var valid2 = _errs4 === errors;if(!valid2){const err3 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/propertyNames",keyword:"propertyNames",params:{propertyName: key1},message:"property name must be valid",schema:schema31.properties.meta.propertyNames,parentSchema:schema31.properties.meta,data:data0};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}}else {const err4 = {instancePath:instancePath+"/meta",schemaPath:"#/properties/meta/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema31.properties.meta.type,parentSchema:schema31.properties.meta,data:data0};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}if(data.nodes !== undefined){let data1 = data.nodes;if(Array.isArray(data1)){if(data1.length < 1){const err5 = {instancePath:instancePath+"/nodes",schemaPath:"#/properties/nodes/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items",schema:1,parentSchema:schema31.properties.nodes,data:data1};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}const len0 = data1.length;for(let i0=0; i0<len0; i0++){let data2 = data1[i0];const _errs10 = errors;let valid6 = true;const _errs11 = errors;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){let missing0;if((data2.plane === undefined) && (missing0 = "plane")){const err6 = {};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}else {if(data2.plane !== undefined){if("PROD" !== data2.plane){const err7 = {};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}}}var _valid0 = _errs11 === errors;errors = _errs10;if(vErrors !== null){if(_errs10){vErrors.length = _errs10;}else {vErrors = null;}}if(_valid0){const _errs13 = errors;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.verified_at === undefined){const err8 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/allOf/0/then/required",keyword:"required",params:{missingProperty: "verified_at"},message:"must have required property '"+"verified_at"+"'",schema:schema31.properties.nodes.items.allOf[0].then.required,parentSchema:schema31.properties.nodes.items.allOf[0].then,data:data2};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}var _valid0 = _errs13 === errors;valid6 = _valid0;}if(!valid6){const err9 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/allOf/0/if",keyword:"if",params:{failingKeyword: "then"},message:"must match \"then\" schema",schema:schema31.properties.nodes.items.allOf[0].if,parentSchema:schema31.properties.nodes.items.allOf[0],data:data2};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}const _errs15 = errors;let valid8 = true;const _errs16 = errors;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){let missing1;if(((data2.node_type === undefined) && (missing1 = "node_type")) || ((data2.verified_at === undefined) && (missing1 = "verified_at"))){const err10 = {};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}else {if(data2.node_type !== undefined){if("claim" !== data2.node_type){const err11 = {};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}}}var _valid1 = _errs16 === errors;errors = _errs15;if(vErrors !== null){if(_errs15){vErrors.length = _errs15;}else {vErrors = null;}}if(_valid1){const _errs18 = errors;if(data2 && typeof data2 == "object" && !Array.isArray(data2)){if(data2.verified_against === undefined){const err12 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/allOf/1/then/required",keyword:"required",params:{missingProperty: "verified_against"},message:"must have required property '"+"verified_against"+"'",schema:schema31.properties.nodes.items.allOf[1].then.required,parentSchema:schema31.properties.nodes.items.allOf[1].then,data:data2};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}var _valid1 = _errs18 === errors;valid8 = _valid1;}if(!valid8){const err13 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/allOf/1/if",keyword:"if",params:{failingKeyword: "then"},message:"must match \"then\" schema",schema:schema31.properties.nodes.items.allOf[1].if,parentSchema:schema31.properties.nodes.items.allOf[1],data:data2};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}if(data2 && typeof data2 == "object" && !Array.isArray(data2)){for(const key2 in data2){const _errs19 = errors;if(typeof key2 === "string"){if(!pattern4.test(key2)){const err14 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z_][A-Za-z0-9_-]*$"},message:"must match pattern \""+"^[A-Za-z_][A-Za-z0-9_-]*$"+"\"",schema:"^[A-Za-z_][A-Za-z0-9_-]*$",parentSchema:schema31.properties.nodes.items.propertyNames,data:key2,propertyName:key2};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}var valid10 = _errs19 === errors;if(!valid10){const err15 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid",schema:schema31.properties.nodes.items.propertyNames,parentSchema:schema31.properties.nodes.items,data:data2};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}}if(data2.label !== undefined){let data5 = data2.label;if(typeof data5 === "string"){if(func1(data5) > 280){const err16 = {instancePath:instancePath+"/nodes/" + i0+"/label",schemaPath:"#/properties/nodes/items/properties/label/maxLength",keyword:"maxLength",params:{limit: 280},message:"must NOT have more than 280 characters",schema:280,parentSchema:schema31.properties.nodes.items.properties.label,data:data5};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}if(data2.provenance !== undefined){let data6 = data2.provenance;if(data6 && typeof data6 == "object" && !Array.isArray(data6)){for(const key3 in data6){const _errs22 = errors;if(typeof key3 === "string"){if(!pattern4.test(key3)){const err17 = {instancePath:instancePath+"/nodes/" + i0+"/provenance",schemaPath:"#/properties/nodes/items/properties/provenance/propertyNames/allOf/0/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z_][A-Za-z0-9_-]*$"},message:"must match pattern \""+"^[A-Za-z_][A-Za-z0-9_-]*$"+"\"",schema:"^[A-Za-z_][A-Za-z0-9_-]*$",parentSchema:schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[0],data:key3,propertyName:key3};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}const _errs25 = errors;let valid14 = false;const _errs26 = errors;if(!((((key3 === "locality") || (key3 === "locator")) || (key3 === "method")) || (key3 === "source"))){const err18 = {instancePath:instancePath+"/nodes/" + i0+"/provenance",schemaPath:"#/properties/nodes/items/properties/provenance/propertyNames/allOf/1/anyOf/0/enum",keyword:"enum",params:{allowedValues: schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[1].anyOf[0].enum},message:"must be equal to one of the allowed values",schema:schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[1].anyOf[0].enum,parentSchema:schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[1].anyOf[0],data:key3,propertyName:key3};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}var _valid2 = _errs26 === errors;valid14 = valid14 || _valid2;const _errs27 = errors;if(typeof key3 === "string"){if(!pattern8.test(key3)){const err19 = {instancePath:instancePath+"/nodes/" + i0+"/provenance",schemaPath:"#/properties/nodes/items/properties/provenance/propertyNames/allOf/1/anyOf/1/pattern",keyword:"pattern",params:{pattern: "^x_"},message:"must match pattern \""+"^x_"+"\"",schema:"^x_",parentSchema:schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[1].anyOf[1],data:key3,propertyName:key3};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}}var _valid2 = _errs27 === errors;valid14 = valid14 || _valid2;if(!valid14){const err20 = {instancePath:instancePath+"/nodes/" + i0+"/provenance",schemaPath:"#/properties/nodes/items/properties/provenance/propertyNames/allOf/1/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf",schema:schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[1].anyOf,parentSchema:schema31.properties.nodes.items.properties.provenance.propertyNames.allOf[1],data:key3,propertyName:key3};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}else {errors = _errs25;if(vErrors !== null){if(_errs25){vErrors.length = _errs25;}else {vErrors = null;}}}var valid12 = _errs22 === errors;if(!valid12){const err21 = {instancePath:instancePath+"/nodes/" + i0+"/provenance",schemaPath:"#/properties/nodes/items/properties/provenance/propertyNames",keyword:"propertyNames",params:{propertyName: key3},message:"property name must be valid",schema:schema31.properties.nodes.items.properties.provenance.propertyNames,parentSchema:schema31.properties.nodes.items.properties.provenance,data:data6};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}if(data6.method !== undefined){let data7 = data6.method;if(typeof data7 === "string"){if(!pattern9.test(data7)){const err22 = {instancePath:instancePath+"/nodes/" + i0+"/provenance/method",schemaPath:"#/properties/nodes/items/properties/provenance/properties/method/pattern",keyword:"pattern",params:{pattern: "^(?:medição|leitura|juízes|derivado|testemunho): [^ \t\n\r]"},message:"must match pattern \""+"^(?:medição|leitura|juízes|derivado|testemunho): [^ \t\n\r]"+"\"",schema:"^(?:medição|leitura|juízes|derivado|testemunho): [^ \t\n\r]",parentSchema:schema31.properties.nodes.items.properties.provenance.properties.method,data:data7};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}}}if(data6.locality !== undefined){let data8 = data6.locality;if(!((((data8 === "host") || (data8 === "pessoa")) || (data8 === "repo")) || (data8 === "web"))){const err23 = {instancePath:instancePath+"/nodes/" + i0+"/provenance/locality",schemaPath:"#/properties/nodes/items/properties/provenance/properties/locality/enum",keyword:"enum",params:{allowedValues: schema31.properties.nodes.items.properties.provenance.properties.locality.enum},message:"must be equal to one of the allowed values",schema:schema31.properties.nodes.items.properties.provenance.properties.locality.enum,parentSchema:schema31.properties.nodes.items.properties.provenance.properties.locality,data:data8};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}}}}else {const err24 = {instancePath:instancePath+"/nodes/" + i0,schemaPath:"#/properties/nodes/items/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema31.properties.nodes.items.type,parentSchema:schema31.properties.nodes.items,data:data2};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}}else {const err25 = {instancePath:instancePath+"/nodes",schemaPath:"#/properties/nodes/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema31.properties.nodes.type,parentSchema:schema31.properties.nodes,data:data1};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;}}if(data.edges !== undefined){let data9 = data.edges;if(Array.isArray(data9)){const len1 = data9.length;for(let i1=0; i1<len1; i1++){let data10 = data9[i1];if(data10 && typeof data10 == "object" && !Array.isArray(data10)){for(const key4 in data10){const _errs34 = errors;if(typeof key4 === "string"){if(!pattern4.test(key4)){const err26 = {instancePath:instancePath+"/edges/" + i1,schemaPath:"#/properties/edges/items/propertyNames/pattern",keyword:"pattern",params:{pattern: "^[A-Za-z_][A-Za-z0-9_-]*$"},message:"must match pattern \""+"^[A-Za-z_][A-Za-z0-9_-]*$"+"\"",schema:"^[A-Za-z_][A-Za-z0-9_-]*$",parentSchema:schema31.properties.edges.items.propertyNames,data:key4,propertyName:key4};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}var valid18 = _errs34 === errors;if(!valid18){const err27 = {instancePath:instancePath+"/edges/" + i1,schemaPath:"#/properties/edges/items/propertyNames",keyword:"propertyNames",params:{propertyName: key4},message:"property name must be valid",schema:schema31.properties.edges.items.propertyNames,parentSchema:schema31.properties.edges.items,data:data10};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}}else {const err28 = {instancePath:instancePath+"/edges/" + i1,schemaPath:"#/properties/edges/items/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema31.properties.edges.items.type,parentSchema:schema31.properties.edges.items,data:data10};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}}else {const err29 = {instancePath:instancePath+"/edges",schemaPath:"#/properties/edges/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema31.properties.edges.type,parentSchema:schema31.properties.edges,data:data9};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}}}else {const err30 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema31.type,parentSchema:schema31,data};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}validate20.errors = vErrors;return errors === 0;}validate20.evaluated = {"props":{"meta":true,"nodes":true,"edges":true},"dynamicProps":false,"dynamicItems":false};
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "urn:kg-ssot:spec:kg-contract-v4.should.schema.json",
4
4
  "title": "kg.yaml — contrato v4, nível SHOULD",
5
- "description": "O contrato v4 do .kg.yaml, nível SHOULD (alerta, não reprova), o vigente. É o SHOULD v3 menos o que o v4 promoveu a MUST: a chave desconhecida sem x_ no topo, no meta, no nó e na aresta e o nó confirmed ou PROD sem provenance. Segue alertando: nome de chave que não é slug ([A-Za-z_][A-Za-z0-9_-]*), como form.pattern.<escopo>.key, que sem x_ também reprova no MUST; nó PROD sem verified_at; label do nó acima de 280 caracteres, form.range.node.label, porque a narrativa vai para narrative; a chave desconhecida dentro de provenance, fechada em source, locator, method e x_; e o grafo sem nós (nodes: []), como form.range.top.nodes: é válido, como o primeiro uso de um app, mas o aviso pega o parse que falhou em silêncio. No v4, provenance.method alerta (form.pattern.node.provenance.method) quando não segue a forma canônica '<classe>: <detalhe>', com a classe em medição, leitura, juízes, derivado, testemunho; reprova no v5. Medição feita por terceiro e lida pelo autor do grafo é testemunho. No v4.1 entram quatro avisos que o próprio arquivo decide. O schema acusa a claim com verified_at e sem verified_against (form.required.node.verified_against): carimbo sem alvo não distingue verificado de declarado. Só a claim: evidence é a própria âncora, artifact nomeia o alvo, question não afirma e a origem da decision é cobrada por integrity.untraced-decision. O leitor acusa três avisos que cruzam campos ou arestas, por isso não cabem no schema: integrity.testimony-in-prod (nó PROD cuja base é testemunho), integrity.verified-before-fact (verified_at anterior a valid_from) e integrity.untraced-decision (decision viva sem trace, sem provenance e sem aresta TRACES_TO saindo dela). No v4.2 entra provenance.locality, opcional, com o enum repo (arquivo versionado no próprio repo), web (endereço público), host (só existe numa máquina: journal local, caminho fora do repo) ou pessoa (relato de alguém): o valor fora do enum alerta como form.enum.node.provenance.locality. Serve para medir o que é reverificável por terceiros e para barrar host na publicação.",
5
+ "description": "O contrato v4 do .kg.yaml, nível SHOULD (alerta, não reprova), o vigente. É o SHOULD v3 menos o que o v4 promoveu a MUST: a chave desconhecida sem x_ no topo, no meta, no nó e na aresta e o nó confirmed ou PROD sem provenance. Segue alertando: nome de chave que não é slug ([A-Za-z_][A-Za-z0-9_-]*), como form.pattern.<escopo>.key, que sem x_ também reprova no MUST; nó PROD sem verified_at; label do nó acima de 280 caracteres, form.range.node.label, porque a narrativa vai para narrative; a chave desconhecida dentro de provenance, fechada em source, locator, method e x_; e o grafo sem nós (nodes: []), como form.range.top.nodes: é válido, como o primeiro uso de um app, mas o aviso pega o parse que falhou em silêncio. No v4, provenance.method alerta (form.pattern.node.provenance.method) quando não segue a forma canônica '<classe>: <detalhe>', com a classe em medição, leitura, juízes, derivado, testemunho; reprova no v5. Medição feita por terceiro e lida pelo autor do grafo é testemunho. No v4.1 entram quatro avisos que o próprio arquivo decide. O schema acusa a claim com verified_at e sem verified_against (form.required.node.verified_against): carimbo sem alvo não distingue verificado de declarado. Só a claim: evidence é a própria âncora, artifact nomeia o alvo, question não afirma e a origem da decision é cobrada por integrity.untraced-decision. O leitor acusa três avisos que cruzam campos ou arestas, por isso não cabem no schema: integrity.testimony-in-prod (nó PROD cuja base é testemunho), integrity.verified-before-fact (verified_at anterior a valid_from) e integrity.untraced-decision (decision viva sem trace, sem provenance e sem aresta TRACES_TO saindo dela). No v4.2 entra provenance.locality, opcional, com o enum repo (arquivo versionado no próprio repo), web (endereço público), host (só existe numa máquina: journal local, caminho fora do repo) ou pessoa (relato de alguém): o valor fora do enum alerta como form.enum.node.provenance.locality. Serve para medir o que é reverificável por terceiros e para barrar host na publicação. No v4.4 entra o quarto aviso do leitor, integrity.refutes-unreconciled: nó que recebe REFUTES e segue vivo (nem superseded nem refuted; a question done já está respondida, e não alerta). O grafo afirma e refuta a mesma coisa; o radar do framework onde o formato nasceu reprova o caso como contradição. Aqui é SHOULD agora e vira MUST numa versão futura, depois que os adotantes medirem a própria dívida.",
6
6
  "type": "object",
7
7
  "propertyNames": {
8
8
  "pattern": "^[A-Za-z_][A-Za-z0-9_-]*$"
package/spec/release.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "4.9.1",
3
- "tag": "kg-ssot-v4.9.1",
2
+ "version": "4.11.0",
3
+ "tag": "kg-ssot-v4.11.0",
4
4
  "contract": {
5
5
  "must": "spec/kg-contract-v4.schema.json",
6
6
  "should": "spec/kg-contract-v4.should.schema.json"
@@ -38,7 +38,8 @@
38
38
  "tools/runners/ajv/package-lock.json",
39
39
  "packaging/mcpb/",
40
40
  "packaging/mirror/",
41
- "packaging/mcp-registry/"
41
+ "packaging/mcp-registry/",
42
+ "packaging/oci/"
42
43
  ],
43
44
  "profiles": {
44
45
  "gate": [
@@ -0,0 +1,5 @@
1
+ // Tipos de kg-ssot/must e kg-ssot/should: o validador pré-compilado do schema MUST ou SHOULD do contrato vigente.
2
+ import type { MustValidator } from './index.mjs';
3
+
4
+ declare const validate: MustValidator;
5
+ export default validate;