kg-ssot 4.20.0 → 4.21.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/core.d.mts +1 -1
- package/index.d.mts +10 -2
- package/kg.mjs +108 -1
- package/must.mjs +1 -1
- package/package.json +1 -1
- package/should.mjs +1 -1
- package/spec/kg-contract-v4.schema.json +1 -1
- package/spec/kg-contract-v4.should.schema.json +211 -1
- package/spec/release.json +5 -2
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, coherenceCodes, indexTexts, parse, verdictOf, warnings, yamlProfileCodes } from './index.mjs';
|
|
3
|
+
export { codes, coherenceCodes, criterionCodes, indexTexts, measuredOrder, parse, verdictOf, warnings, yamlProfileCodes } from './index.mjs';
|
package/index.d.mts
CHANGED
|
@@ -46,10 +46,18 @@ export function checkWarnings(text: string, options?: Options): string[];
|
|
|
46
46
|
export function verdictOf(text: string, validate: MustValidator,
|
|
47
47
|
options?: Options & { validateShould?: MustValidator }): Verdict;
|
|
48
48
|
|
|
49
|
-
/** Os avisos de coerência de um documento já lido (integrity.testimony-in-prod, verified-before-fact, untraced-decision, refutes-unreconciled
|
|
50
|
-
* Com o corpus, a REFUTES que chega de outro grafo pelas external_edges também conta. */
|
|
49
|
+
/** Os avisos de coerência de um documento já lido (integrity.testimony-in-prod, verified-before-fact, untraced-decision, refutes-unreconciled,
|
|
50
|
+
* e desde o v4.12 os de criterionCodes). Com o corpus, a REFUTES que chega de outro grafo pelas external_edges também conta. */
|
|
51
51
|
export function coherenceCodes(doc: Record<string, unknown>, corpus?: Corpus | null): string[];
|
|
52
52
|
|
|
53
|
+
/** Os avisos do critério pré-registrado (v4.12), só entre nós do mesmo grafo: integrity.criterion-unknown,
|
|
54
|
+
* criterion-amends-mismatch, criterion-sha-mismatch, measured-post-hoc e measured-order-unknown. Sem hash e sem corpus. */
|
|
55
|
+
export function criterionCodes(doc: Record<string, unknown>): string[];
|
|
56
|
+
|
|
57
|
+
/** A ordem entre o measured_at de um resultado e o registered_at da versão citada: 'post-hoc' (antes), 'unknown'
|
|
58
|
+
* (indecidível: o mesmo dia só com datas, ou até um dia quando uma é só data) ou null (depois, ou fora da forma). */
|
|
59
|
+
export function measuredOrder(measured: unknown, registered: unknown): 'post-hoc' | 'unknown' | null;
|
|
60
|
+
|
|
53
61
|
/** Os avisos do perfil JSON num .kg.yaml já lido (yaml.unsafe-integer, yaml.lone-surrogate, yaml.depth-over-128). */
|
|
54
62
|
export function yamlProfileCodes(doc: unknown): string[];
|
|
55
63
|
|
package/kg.mjs
CHANGED
|
@@ -487,6 +487,7 @@ export function formCodes(err, doc) {
|
|
|
487
487
|
}
|
|
488
488
|
let kind = FORM_TYPE[err.keyword] || err.keyword;
|
|
489
489
|
if (err.keyword === 'not' && isObj(err.schema) && 'pattern' in err.schema) kind = 'pattern';
|
|
490
|
+
if (err.keyword === 'not' && isObj(err.schema) && 'required' in err.schema) kind = 'exclusive'; // v4.12: ref e text
|
|
490
491
|
if (err.keyword === 'required') return [`form.${kind}.${scope}.${[...prefix, err.params.missingProperty].join('.')}`];
|
|
491
492
|
if (err.keyword === 'additionalProperties') {
|
|
492
493
|
return [`form.${kind}.${scope}.${[...prefix, err.params.additionalProperty].join('.')}`];
|
|
@@ -670,7 +671,7 @@ function containsValue(value, hit) {
|
|
|
670
671
|
|
|
671
672
|
/** Os avisos de coerência, que cruzam campos ou arestas (coherence_located da referência), um por ocorrência:
|
|
672
673
|
* integrity.testimony-in-prod, integrity.verified-before-fact, integrity.untraced-decision e
|
|
673
|
-
* integrity.refutes-unreconciled. */
|
|
674
|
+
* integrity.refutes-unreconciled; desde o v4.12, os do critério pré-registrado (criterionCodes). */
|
|
674
675
|
export function coherenceCodes(doc, corpus = null) {
|
|
675
676
|
const out = [];
|
|
676
677
|
const edges = Array.isArray(doc.edges) ? doc.edges : [];
|
|
@@ -710,6 +711,112 @@ export function coherenceCodes(doc, corpus = null) {
|
|
|
710
711
|
out.push('integrity.refutes-unreconciled');
|
|
711
712
|
}
|
|
712
713
|
}
|
|
714
|
+
return out.concat(criterionCodes(doc));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// A data ou a data-hora RFC 3339 dos perfis do critério (v4.12), a mesma regex do schema SHOULD (STAMP da referência).
|
|
718
|
+
const STAMP = new RegExp('^([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])'
|
|
719
|
+
+ '(?:[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(?:\\.([0-9]+))?([Zz]|([+-])([01][0-9]|2[0-3]):([0-5][0-9])))?$');
|
|
720
|
+
|
|
721
|
+
/** Os dias desde 1970-01-01 no calendário gregoriano proléptico (_days da referência), só com inteiros. */
|
|
722
|
+
function civilDays(y0, m, d) {
|
|
723
|
+
const y = y0 - (m <= 2 ? 1 : 0);
|
|
724
|
+
const era = Math.floor((y >= 0 ? y : y - 399) / 400);
|
|
725
|
+
const yoe = y - era * 400;
|
|
726
|
+
const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
|
|
727
|
+
return era * 146097 + yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy - 719468;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** O match de STAMP com o dia que existe no calendário; null fora da forma ou num dia que não existe (_stamp). */
|
|
731
|
+
function stamp(value) {
|
|
732
|
+
const mt = typeof value === 'string' ? STAMP.exec(value) : null;
|
|
733
|
+
if (!mt) return null;
|
|
734
|
+
const [y, m, d] = [1, 2, 3].map((i) => Number(mt[i]));
|
|
735
|
+
const leap = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
|
|
736
|
+
return d <= [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1] ? mt : null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/** [segundos desde 1970 em UTC, fração em dígitos sem zero à direita] de uma data-hora de STAMP (_instant). */
|
|
740
|
+
function instant(mt) {
|
|
741
|
+
const [y, mo, d, h, mi, sec] = [1, 2, 3, 4, 5, 6].map((i) => Number(mt[i]));
|
|
742
|
+
const off = mt[9] === undefined ? 0 : (mt[9] === '+' ? 1 : -1) * (Number(mt[10]) * 60 + Number(mt[11]));
|
|
743
|
+
return [civilDays(y, mo, d) * 86400 + h * 3600 + mi * 60 + sec - off * 60, (mt[7] || '').replace(/0+$/, '')];
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** 'post-hoc', 'unknown' ou null entre o measured_at de um resultado e o registered_at da versão citada
|
|
747
|
+
* (measured_order da referência): as duas com hora, o instante; as duas só data, o dia escrito; uma só data, o dia em
|
|
748
|
+
* UTC com um dia de folga, que é indecidível. */
|
|
749
|
+
export function measuredOrder(measured, registered) {
|
|
750
|
+
const mm = stamp(measured);
|
|
751
|
+
const mr = stamp(registered);
|
|
752
|
+
if (!mm || !mr) return null;
|
|
753
|
+
if (mm[4] !== undefined && mr[4] !== undefined) {
|
|
754
|
+
const im = instant(mm);
|
|
755
|
+
const ir = instant(mr);
|
|
756
|
+
if (im[0] !== ir[0]) return im[0] < ir[0] ? 'post-hoc' : null;
|
|
757
|
+
return im[1].padEnd(ir[1].length, '0') < ir[1].padEnd(im[1].length, '0') ? 'post-hoc' : null;
|
|
758
|
+
}
|
|
759
|
+
const day = (mt) => (mt[4] !== undefined ? Math.floor(instant(mt)[0] / 86400) : civilDays(Number(mt[1]), Number(mt[2]), Number(mt[3])));
|
|
760
|
+
const gap = day(mm) - day(mr);
|
|
761
|
+
if (mm[4] === undefined && mr[4] === undefined) return gap < 0 ? 'post-hoc' : gap === 0 ? 'unknown' : null;
|
|
762
|
+
return gap <= -2 ? 'post-hoc' : gap <= 1 ? 'unknown' : null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** Os ids das versões num ciclo de amends, em tempo linear (_amends_cycles da referência). */
|
|
766
|
+
function amendsCycles(criteria) {
|
|
767
|
+
const seen = new Set();
|
|
768
|
+
const cycles = new Set();
|
|
769
|
+
for (const start of criteria.keys()) {
|
|
770
|
+
const path = [];
|
|
771
|
+
const pos = new Map();
|
|
772
|
+
let cur = start;
|
|
773
|
+
while (typeof cur === 'string' && criteria.has(cur) && !seen.has(cur)) {
|
|
774
|
+
seen.add(cur);
|
|
775
|
+
pos.set(cur, path.length);
|
|
776
|
+
path.push(cur);
|
|
777
|
+
cur = get(criteria.get(cur), 'amends');
|
|
778
|
+
}
|
|
779
|
+
if (typeof cur === 'string' && pos.has(cur)) for (const id of path.slice(pos.get(cur))) cycles.add(id);
|
|
780
|
+
}
|
|
781
|
+
return cycles;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Os avisos do critério pré-registrado (v4.12), só entre nós do mesmo grafo (criterion_located da referência):
|
|
785
|
+
* integrity.criterion-unknown, integrity.criterion-amends-mismatch, integrity.criterion-sha-mismatch,
|
|
786
|
+
* integrity.measured-post-hoc e integrity.measured-order-unknown. O leitor não calcula hash nem lê o corpus. */
|
|
787
|
+
export function criterionCodes(doc) {
|
|
788
|
+
const out = [];
|
|
789
|
+
const criteria = new Map();
|
|
790
|
+
for (const n of located(doc, 'nodes')) {
|
|
791
|
+
if (typeof get(n, 'id') === 'string' && isObj(get(n, 'x_criterion')) && !criteria.has(n.id)) criteria.set(n.id, n.x_criterion);
|
|
792
|
+
}
|
|
793
|
+
const cycles = amendsCycles(criteria);
|
|
794
|
+
for (const n of located(doc, 'nodes')) {
|
|
795
|
+
const crit = get(n, 'x_criterion');
|
|
796
|
+
if (isObj(crit) && typeof get(crit, 'amends') === 'string') {
|
|
797
|
+
const prior = criteria.get(crit.amends);
|
|
798
|
+
if (prior === undefined) out.push('integrity.criterion-unknown');
|
|
799
|
+
else if ((typeof get(crit, 'id') === 'string' && typeof get(prior, 'id') === 'string' && crit.id !== prior.id)
|
|
800
|
+
|| cycles.has(get(n, 'id'))) {
|
|
801
|
+
out.push('integrity.criterion-amends-mismatch');
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const under = get(n, 'x_measured_under');
|
|
805
|
+
if (!isObj(under) || !Array.isArray(get(under, 'criteria'))) continue;
|
|
806
|
+
for (const item of under.criteria) {
|
|
807
|
+
const ref = isObj(item) ? get(item, 'criterion') : null;
|
|
808
|
+
if (typeof ref !== 'string' || isExternal(ref)) continue; // a citação externa é do kg_criterion.py
|
|
809
|
+
const target = criteria.get(ref);
|
|
810
|
+
if (target === undefined) { out.push('integrity.criterion-unknown'); continue; }
|
|
811
|
+
if (typeof get(item, 'sha256') === 'string' && typeof get(target, 'sha256') === 'string' && item.sha256 !== target.sha256) {
|
|
812
|
+
out.push('integrity.criterion-sha-mismatch');
|
|
813
|
+
}
|
|
814
|
+
const sameRound = typeof get(under, 'round') === 'string' && get(under, 'round') === get(target, 'round');
|
|
815
|
+
const order = measuredOrder(get(under, 'measured_at'), get(target, 'registered_at'));
|
|
816
|
+
if (sameRound || order === 'post-hoc') out.push('integrity.measured-post-hoc');
|
|
817
|
+
else if (order === 'unknown') out.push('integrity.measured-order-unknown');
|
|
818
|
+
}
|
|
819
|
+
}
|
|
713
820
|
return out;
|
|
714
821
|
}
|
|
715
822
|
|