@orkastery/cli 0.4.3 → 0.5.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/adapters/README.md +11 -0
- package/adapters/claude-code/.claude-plugin/plugin.json +3 -1
- package/adapters/claude-code/agents/ork-check.md +1 -1
- package/adapters/claude-code/agents/ork-go.md +2 -2
- package/adapters/claude-code/agents/ork-goal.md +1 -1
- package/adapters/claude-code/agents/ork-master.md +1 -1
- package/adapters/claude-code/agents/ork-plan.md +1 -1
- package/adapters/claude-code/agents/ork-ship.md +1 -1
- package/adapters/claude-code/commands/master.md +11 -8
- package/adapters/claude-code/commands/ork.md +8 -0
- package/adapters/claude-code/commands/ship.md +2 -2
- package/adapters/codex/skills/ork/SKILL.md +12 -1
- package/adapters/hermes/README.md +3 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +17 -2
- package/adapters/hermes/bin/ork-brain.sh +3 -0
- package/adapters/hermes/bin/ork-maestro.sh +4 -1
- package/adapters/hermes/bin/ork-network-roadmap.sh +11 -0
- package/adapters/hermes/bin/ork-roadmap-status.sh +9 -0
- package/adapters/hermes/hermes.plugin.json +2 -0
- package/adapters/hermes/hitl-ingress/__init__.py +36 -3
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +19 -20
- package/adapters/openclaw/README.md +34 -2
- package/adapters/openclaw/dist/hitl-ingress.js +37 -3
- package/adapters/openclaw/dist/index.js +92 -20
- package/adapters/openclaw/openclaw.plugin.json +10 -1
- package/adapters/openclaw/package.json +2 -2
- package/adapters/openclaw/src/hitl-ingress.ts +32 -3
- package/adapters/openclaw/src/index.ts +108 -20
- package/assets/docs/padroes/documentacao-de-produto.md +2 -2
- package/assets/docs/padroes/roadmap-de-produto.md +1 -1
- package/assets/docs/produto/_modelo-feature.md +2 -0
- package/assets/orkmind_bridge.py +214 -1
- package/dist/adapters/claude-bg.js +6 -3
- package/dist/board.js +74 -0
- package/dist/branch-de-estado.js +5 -4
- package/dist/busca-semantica.js +161 -0
- package/dist/ci.js +141 -10
- package/dist/company-brain-cli.js +27 -2
- package/dist/company-brain-context.js +132 -0
- package/dist/company-brain-dossie.js +376 -0
- package/dist/company-brain-mcp.js +4 -1
- package/dist/conducao.js +71 -18
- package/dist/decisao-autonoma.js +42 -0
- package/dist/docs.js +64 -22
- package/dist/doctor.js +33 -0
- package/dist/entrega-pr.js +109 -5
- package/dist/experiencia-instalacao.js +256 -0
- package/dist/experiencia.js +72 -0
- package/dist/fabrica-estado.js +43 -10
- package/dist/fechamento.js +105 -0
- package/dist/forja.js +435 -0
- package/dist/gates.js +9 -1
- package/dist/hitl-classificacao.js +11 -1
- package/dist/hitl-contract.js +50 -7
- package/dist/hitl-curto.js +158 -0
- package/dist/hitl-gates.js +67 -1
- package/dist/hitl-lote.js +30 -15
- package/dist/hitl-presentation.js +10 -7
- package/dist/hitl-resumo.js +33 -12
- package/dist/hitl-texto-livre.js +150 -0
- package/dist/hosts.js +86 -1
- package/dist/index.js +602 -38
- package/dist/indice-vetorial.js +317 -0
- package/dist/init.js +13 -0
- package/dist/integracoes-locais.js +1 -1
- package/dist/intelligence-benchmark-contract.js +570 -0
- package/dist/intelligence-graph-cli.js +430 -0
- package/dist/intelligence-graph-contract.js +577 -0
- package/dist/intelligence-graph-extract-md.js +469 -0
- package/dist/intelligence-graph-extract-ts.js +810 -0
- package/dist/intelligence-graph-extract.js +369 -0
- package/dist/intelligence-graph-index.js +512 -0
- package/dist/intelligence-graph-parsers.js +327 -0
- package/dist/intelligence-graph-query.js +344 -0
- package/dist/intelligence-graph-repo.js +187 -0
- package/dist/leases.js +84 -0
- package/dist/ledger.js +14 -0
- package/dist/maestro-cli.js +10 -1
- package/dist/maestro-contract.js +5 -1
- package/dist/maestro-discovery.js +16 -2
- package/dist/maestro-snapshot.js +6 -2
- package/dist/manifest.js +142 -5
- package/dist/master-batch.js +3 -2
- package/dist/master-digest.js +2 -2
- package/dist/master-nota.js +203 -0
- package/dist/master.js +4 -0
- package/dist/mcp-experiencia.js +164 -0
- package/dist/mcp-git.js +4 -0
- package/dist/mcp-install.js +3 -1
- package/dist/mcp-maestro.js +3 -2
- package/dist/mcp-server.js +84 -5
- package/dist/memoria.js +147 -1
- package/dist/network-roadmap.js +907 -0
- package/dist/ocupacao.js +38 -2
- package/dist/onboarding.js +113 -7
- package/dist/orkmind.js +165 -9
- package/dist/phase.js +166 -13
- package/dist/policies.js +2 -0
- package/dist/projeto-alvo.js +551 -0
- package/dist/prompts.js +1 -1
- package/dist/pulse-consentimento.js +4 -2
- package/dist/pulse-delivery.js +5 -0
- package/dist/pulse-resposta.js +674 -49
- package/dist/pulse.js +21 -0
- package/dist/retry.js +51 -8
- package/dist/roadmap-reservas.js +262 -20
- package/dist/roadmap-status.js +213 -0
- package/dist/thread-close.js +3 -0
- package/dist/verify.js +4 -3
- package/eval/casos/orchestration-experience-pt-br.json +48 -0
- package/eval/casos/orchestration-experience.json +48 -0
- package/package.json +1 -1
- package/schemas/code-artifact-graph.v1.schema.json +581 -0
- package/schemas/graph-benchmark.v1.schema.json +2240 -0
- package/schemas/maestro-snapshot.schema.json +23 -0
- package/skills/README.md +7 -5
- package/skills/core/onboarding/SKILL.md +7 -0
- package/skills/core/orchestration-experience/SKILL.md +113 -0
- package/skills/core/orchestration-experience-pt-br/SKILL.md +112 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +2 -2
- package/skills/phases/go-implementation/SKILL.md +2 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.digestDoGrafo = exports.digestDosExtratores = exports.sha256DoCanonico = exports.grafoSchema = exports.diagnosticoSchema = exports.arestaSchema = exports.noSchema = exports.evidenciaSchema = exports.spanSchema = exports.spanDePdfSchema = exports.spanDeTextoSchema = exports.snapshotSchema = exports.extratorSchema = exports.entradaDoManifestoSchema = exports.acessoSchema = exports.MATRIZ_DE_ARESTAS = exports.ESQUEMAS_DE_AUTORIDADE_RESERVADOS = exports.TIPOS_DE_DIAGNOSTICO = exports.CLASSE_DE_CONFIANCA = exports.METODOS_DE_EXTRACAO = exports.TIPOS_DE_ARESTA = exports.TIPOS_DE_NO = exports.GRAFO_LIMITES = exports.GRAFO_SCHEMA = void 0;
|
|
4
|
+
exports.compararUtf8 = compararUtf8;
|
|
5
|
+
exports.canonico = canonico;
|
|
6
|
+
exports.idDoSnapshot = idDoSnapshot;
|
|
7
|
+
exports.idDoNo = idDoNo;
|
|
8
|
+
exports.idDaAresta = idDaAresta;
|
|
9
|
+
exports.derivarIds = derivarIds;
|
|
10
|
+
exports.caminhoValido = caminhoValido;
|
|
11
|
+
exports.validarGrafo = validarGrafo;
|
|
12
|
+
exports.conferirFontes = conferirFontes;
|
|
13
|
+
/**
|
|
14
|
+
* I-31 KG1 (D1 a D5, D9, D10): contrato `ork.code-artifact-graph/v1` do grafo deterministico
|
|
15
|
+
* de codigo e artefatos.
|
|
16
|
+
*
|
|
17
|
+
* O grafo e projecao local e descartavel: a mesma entrada, configuracao e versao de extrator
|
|
18
|
+
* determinam as mesmas identidades e o mesmo conteudo canonico, em qualquer ordem de insercao.
|
|
19
|
+
* Modulo puro (D10): sem filesystem, rede, subprocesso, parser, banco ou modelo. Bytes de fonte
|
|
20
|
+
* chegam por argumento. Nao amplia o Company Brain v1 nem pluga o adaptador semantico (D9).
|
|
21
|
+
*/
|
|
22
|
+
const node_crypto_1 = require("node:crypto");
|
|
23
|
+
const zod_1 = require("zod");
|
|
24
|
+
exports.GRAFO_SCHEMA = 'ork.code-artifact-graph/v1';
|
|
25
|
+
/** Tetos do contrato: acima deles o payload e recusado, nunca truncado. */
|
|
26
|
+
exports.GRAFO_LIMITES = Object.freeze({
|
|
27
|
+
manifesto: 100_000, nos: 100_000, arestas: 500_000, evidenciasPorAresta: 64, aclRefs: 32,
|
|
28
|
+
extratores: 32, diagnosticos: 100_000, caminho: 1024, fragmento: 512, paginas: 100_000,
|
|
29
|
+
});
|
|
30
|
+
exports.TIPOS_DE_NO = ['file', 'symbol', 'section', 'artifact'];
|
|
31
|
+
exports.TIPOS_DE_ARESTA = ['contains', 'declares', 'imports', 'calls', 'references', 'derived_from'];
|
|
32
|
+
/** D4: so metodo deterministico sustenta aresta. Embedding, similaridade e inferencia ficam fora. */
|
|
33
|
+
exports.METODOS_DE_EXTRACAO = ['ast', 'structured', 'explicit-link', 'text-location'];
|
|
34
|
+
exports.CLASSE_DE_CONFIANCA = 'EXTRACTED';
|
|
35
|
+
/** D3: o que o extrator nao resolveu vira diagnostico, nunca aresta nem no externo fabricado. */
|
|
36
|
+
exports.TIPOS_DE_DIAGNOSTICO = ['unresolved-import', 'dynamic-resolution', 'unsupported-language'];
|
|
37
|
+
/** D4: o indice nao e autoridade de fato. Estes esquemas de autoridade apontam para o proprio grafo. */
|
|
38
|
+
exports.ESQUEMAS_DE_AUTORIDADE_RESERVADOS = ['graph', 'index', 'kg', 'snap', 'node', 'edge'];
|
|
39
|
+
const nenhum = [];
|
|
40
|
+
/** D3: matriz fechada, tipo de origem para os tipos de destino permitidos. */
|
|
41
|
+
exports.MATRIZ_DE_ARESTAS = Object.freeze({
|
|
42
|
+
contains: { file: ['section'], symbol: ['symbol'], section: nenhum, artifact: nenhum },
|
|
43
|
+
declares: { file: ['symbol'], symbol: nenhum, section: nenhum, artifact: nenhum },
|
|
44
|
+
imports: { file: ['file', 'symbol'], symbol: ['file', 'symbol'], section: nenhum, artifact: nenhum },
|
|
45
|
+
calls: { file: nenhum, symbol: ['symbol'], section: nenhum, artifact: nenhum },
|
|
46
|
+
references: { file: exports.TIPOS_DE_NO, symbol: exports.TIPOS_DE_NO, section: exports.TIPOS_DE_NO, artifact: exports.TIPOS_DE_NO },
|
|
47
|
+
derived_from: { file: nenhum, symbol: nenhum, section: nenhum, artifact: ['file', 'section', 'artifact'] },
|
|
48
|
+
});
|
|
49
|
+
const inteiro = zod_1.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
|
50
|
+
const linha = zod_1.z.number().int().min(1).max(Number.MAX_SAFE_INTEGER).nullable();
|
|
51
|
+
const sha256 = zod_1.z.string().regex(/^[a-f0-9]{64}$/);
|
|
52
|
+
const idDeSnapshot = zod_1.z.string().regex(/^snap-[a-f0-9]{64}$/);
|
|
53
|
+
const idDeNo = zod_1.z.string().regex(/^node-[a-f0-9]{64}$/);
|
|
54
|
+
const idDeAresta = zod_1.z.string().regex(/^edge-[a-f0-9]{64}$/);
|
|
55
|
+
const tenant = zod_1.z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/);
|
|
56
|
+
const aclRef = zod_1.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$/);
|
|
57
|
+
const autoridade = zod_1.z.string().regex(/^[a-z][a-z0-9+.-]{0,31}:[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/);
|
|
58
|
+
const versaoDaFonte = zod_1.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:+-]{0,127}$/);
|
|
59
|
+
const idDeExtrator = zod_1.z.string().regex(/^[a-z][a-z0-9._-]{0,63}$/);
|
|
60
|
+
const versaoDeExtrator = zod_1.z.string().regex(/^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/);
|
|
61
|
+
const caminho = zod_1.z.string().min(1).max(exports.GRAFO_LIMITES.caminho);
|
|
62
|
+
const textoCurto = zod_1.z.string().min(1).max(exports.GRAFO_LIMITES.fragmento);
|
|
63
|
+
/** D5: restricao de acesso. Referencias ordenadas, sem repeticao; nenhuma concede permissao. */
|
|
64
|
+
exports.acessoSchema = zod_1.z.object({
|
|
65
|
+
tenant_id: tenant, acl_refs: zod_1.z.array(aclRef).min(1).max(exports.GRAFO_LIMITES.aclRefs),
|
|
66
|
+
}).strict();
|
|
67
|
+
exports.entradaDoManifestoSchema = zod_1.z.object({
|
|
68
|
+
path: caminho, authority: autoridade, source_hash: sha256, source_version: versaoDaFonte,
|
|
69
|
+
size_bytes: inteiro, access: exports.acessoSchema,
|
|
70
|
+
}).strict();
|
|
71
|
+
exports.extratorSchema = zod_1.z.object({ extractor_id: idDeExtrator, extractor_version: versaoDeExtrator }).strict();
|
|
72
|
+
/** D2: snapshot e manifesto, configuracao e extratores. Nome de branch nao identifica entrada. */
|
|
73
|
+
exports.snapshotSchema = zod_1.z.object({
|
|
74
|
+
snapshot_id: idDeSnapshot,
|
|
75
|
+
revision: zod_1.z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/).nullable(),
|
|
76
|
+
revision_unavailable_reason: zod_1.z.string().regex(/^[a-z][a-z0-9-]{0,63}$/).nullable(),
|
|
77
|
+
source_manifest: zod_1.z.array(exports.entradaDoManifestoSchema).min(1).max(exports.GRAFO_LIMITES.manifesto),
|
|
78
|
+
config_hash: sha256,
|
|
79
|
+
extractors: zod_1.z.array(exports.extratorSchema).min(1).max(exports.GRAFO_LIMITES.extratores),
|
|
80
|
+
extractors_digest: sha256,
|
|
81
|
+
}).strict();
|
|
82
|
+
/** D4: offsets em bytes UTF-8 da fonte, inicio inclusivo e fim exclusivo. */
|
|
83
|
+
exports.spanDeTextoSchema = zod_1.z.object({
|
|
84
|
+
type: zod_1.z.literal('text'), byte_start: inteiro, byte_end: inteiro, line_start: linha, line_end: linha,
|
|
85
|
+
}).strict();
|
|
86
|
+
/** D4: offsets no texto UTF-8 extraido da pagina, nunca no binario do PDF. */
|
|
87
|
+
exports.spanDePdfSchema = zod_1.z.object({
|
|
88
|
+
type: zod_1.z.literal('pdf-text'), page: zod_1.z.number().int().min(1).max(exports.GRAFO_LIMITES.paginas),
|
|
89
|
+
byte_start: inteiro, byte_end: inteiro, extracted_text_hash: sha256,
|
|
90
|
+
}).strict();
|
|
91
|
+
exports.spanSchema = zod_1.z.discriminatedUnion('type', [exports.spanDeTextoSchema, exports.spanDePdfSchema]);
|
|
92
|
+
exports.evidenciaSchema = zod_1.z.object({
|
|
93
|
+
snapshot_id: idDeSnapshot, path: caminho, source_hash: sha256, source_version: versaoDaFonte,
|
|
94
|
+
authority: autoridade, extractor_id: idDeExtrator, extractor_version: versaoDeExtrator,
|
|
95
|
+
extraction_method: zod_1.z.enum(exports.METODOS_DE_EXTRACAO), confidence_class: zod_1.z.literal(exports.CLASSE_DE_CONFIANCA),
|
|
96
|
+
span: exports.spanSchema, access: exports.acessoSchema,
|
|
97
|
+
}).strict();
|
|
98
|
+
exports.noSchema = zod_1.z.object({
|
|
99
|
+
node_id: idDeNo, kind: zod_1.z.enum(exports.TIPOS_DE_NO), snapshot_id: idDeSnapshot,
|
|
100
|
+
locator: zod_1.z.object({ path: caminho, fragment: textoCurto.nullable() }).strict(),
|
|
101
|
+
source_hash: sha256, access: exports.acessoSchema,
|
|
102
|
+
}).strict();
|
|
103
|
+
exports.arestaSchema = zod_1.z.object({
|
|
104
|
+
edge_id: idDeAresta, kind: zod_1.z.enum(exports.TIPOS_DE_ARESTA), snapshot_id: idDeSnapshot, from: idDeNo, to: idDeNo,
|
|
105
|
+
evidence: zod_1.z.array(exports.evidenciaSchema).min(1).max(exports.GRAFO_LIMITES.evidenciasPorAresta), access: exports.acessoSchema,
|
|
106
|
+
}).strict();
|
|
107
|
+
exports.diagnosticoSchema = zod_1.z.object({
|
|
108
|
+
kind: zod_1.z.enum(exports.TIPOS_DE_DIAGNOSTICO), path: caminho, reference: textoCurto.nullable(),
|
|
109
|
+
extractor_id: idDeExtrator, extractor_version: versaoDeExtrator, access: exports.acessoSchema,
|
|
110
|
+
}).strict();
|
|
111
|
+
exports.grafoSchema = zod_1.z.object({
|
|
112
|
+
schema: zod_1.z.literal(exports.GRAFO_SCHEMA), tenant_id: tenant,
|
|
113
|
+
repository_id: zod_1.z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/),
|
|
114
|
+
snapshot: exports.snapshotSchema,
|
|
115
|
+
nodes: zod_1.z.array(exports.noSchema).max(exports.GRAFO_LIMITES.nos),
|
|
116
|
+
edges: zod_1.z.array(exports.arestaSchema).max(exports.GRAFO_LIMITES.arestas),
|
|
117
|
+
diagnostics: zod_1.z.array(exports.diagnosticoSchema).max(exports.GRAFO_LIMITES.diagnosticos),
|
|
118
|
+
}).strict();
|
|
119
|
+
/** O erro traz o codigo e a posicao estrutural, nunca conteudo de fonte. */
|
|
120
|
+
function falha(codigo, onde) {
|
|
121
|
+
throw new Error(onde ? `${codigo} em ${onde}` : codigo);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Ordem de code points, que e a ordem dos bytes UTF-8: nao depende da linguagem do consumidor.
|
|
125
|
+
* Unidade UTF-16 de surrogate (D800-DFFF) representa code point acima de FFFF e vai para o fim.
|
|
126
|
+
*/
|
|
127
|
+
function compararUtf8(a, b) {
|
|
128
|
+
const ajuste = (c) => (c >= 0xe000 ? c - 0x800 : c >= 0xd800 ? c + 0x2000 : c);
|
|
129
|
+
const n = Math.min(a.length, b.length);
|
|
130
|
+
for (let i = 0; i < n; i++) {
|
|
131
|
+
const x = a.charCodeAt(i), y = b.charCodeAt(i);
|
|
132
|
+
if (x !== y)
|
|
133
|
+
return ajuste(x) - ajuste(y);
|
|
134
|
+
}
|
|
135
|
+
return a.length - b.length;
|
|
136
|
+
}
|
|
137
|
+
/** D2: JSON canonico. Chaves na ordem de bytes UTF-8, sem espaco, so numero finito. */
|
|
138
|
+
function canonico(v) {
|
|
139
|
+
if (v === null || typeof v === 'boolean' || typeof v === 'string')
|
|
140
|
+
return JSON.stringify(v);
|
|
141
|
+
if (typeof v === 'number')
|
|
142
|
+
return Number.isFinite(v) ? JSON.stringify(v) : falha('grafo.canonico.numero-invalido');
|
|
143
|
+
if (Array.isArray(v))
|
|
144
|
+
return `[${v.map(canonico).join(',')}]`;
|
|
145
|
+
if (typeof v === 'object') {
|
|
146
|
+
const o = v;
|
|
147
|
+
return `{${Object.keys(o).sort(compararUtf8).map((k) => `${JSON.stringify(k)}:${canonico(o[k])}`).join(',')}}`;
|
|
148
|
+
}
|
|
149
|
+
return falha('grafo.canonico.tipo-invalido');
|
|
150
|
+
}
|
|
151
|
+
/** SHA-256 sobre os bytes UTF-8 do conteudo canonico. */
|
|
152
|
+
const sha256DoCanonico = (v) => (0, node_crypto_1.createHash)('sha256').update(canonico(v), 'utf8').digest('hex');
|
|
153
|
+
exports.sha256DoCanonico = sha256DoCanonico;
|
|
154
|
+
const sha256DosBytes = (b) => (0, node_crypto_1.createHash)('sha256').update(b).digest('hex');
|
|
155
|
+
/** Ordena conjunto por chave estavel, calculando cada chave uma vez. */
|
|
156
|
+
function ordenado(itens, chave) {
|
|
157
|
+
return itens.map((item) => [chave(item), item]).sort((a, b) => compararUtf8(a[0], b[0])).map(([, item]) => item);
|
|
158
|
+
}
|
|
159
|
+
const digestDosExtratores = (extratores) => (0, exports.sha256DoCanonico)(ordenado(extratores, (e) => e.extractor_id));
|
|
160
|
+
exports.digestDosExtratores = digestDosExtratores;
|
|
161
|
+
/** D2: o snapshot deriva de tenant, repositorio, revisao, manifesto, configuracao e extratores. */
|
|
162
|
+
function idDoSnapshot(g) {
|
|
163
|
+
const s = g.snapshot;
|
|
164
|
+
return `snap-${(0, exports.sha256DoCanonico)({
|
|
165
|
+
schema: exports.GRAFO_SCHEMA, tenant_id: g.tenant_id, repository_id: g.repository_id,
|
|
166
|
+
revision: s.revision, revision_unavailable_reason: s.revision_unavailable_reason,
|
|
167
|
+
source_manifest: ordenado(s.source_manifest, (m) => m.path), config_hash: s.config_hash,
|
|
168
|
+
extractors: ordenado(s.extractors, (e) => e.extractor_id),
|
|
169
|
+
})}`;
|
|
170
|
+
}
|
|
171
|
+
/** D2: o no deriva de tenant, repositorio, snapshot, tipo e localizador. */
|
|
172
|
+
function idDoNo(tenantId, repositoryId, snapshotId, kind, locator) {
|
|
173
|
+
return `node-${(0, exports.sha256DoCanonico)({ tenant_id: tenantId, repository_id: repositoryId, snapshot_id: snapshotId, kind, locator })}`;
|
|
174
|
+
}
|
|
175
|
+
/** D2: a aresta deriva de snapshot, tipo, extremidades e evidencias canonicas. */
|
|
176
|
+
function idDaAresta(a) {
|
|
177
|
+
return `edge-${(0, exports.sha256DoCanonico)({ snapshot_id: a.snapshot_id, kind: a.kind, from: a.from, to: a.to, evidence: ordenado(a.evidence, canonico) })}`;
|
|
178
|
+
}
|
|
179
|
+
function canonizar(g) {
|
|
180
|
+
return {
|
|
181
|
+
...g,
|
|
182
|
+
snapshot: {
|
|
183
|
+
...g.snapshot,
|
|
184
|
+
source_manifest: ordenado(g.snapshot.source_manifest, (m) => m.path),
|
|
185
|
+
extractors: ordenado(g.snapshot.extractors, (e) => e.extractor_id),
|
|
186
|
+
},
|
|
187
|
+
nodes: ordenado(g.nodes, (n) => n.node_id),
|
|
188
|
+
edges: ordenado(g.edges.map((a) => ({ ...a, evidence: ordenado(a.evidence, canonico) })), (a) => a.edge_id),
|
|
189
|
+
diagnostics: ordenado(g.diagnostics, canonico),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Para o produtor (KG2): recalcula snapshot, nos e arestas a partir do conteudo, reescrevendo
|
|
194
|
+
* as extremidades pelo id provisorio de cada no. Nao valida; o resultado passa por validarGrafo.
|
|
195
|
+
*/
|
|
196
|
+
function derivarIds(rascunho) {
|
|
197
|
+
const g = structuredClone(rascunho), s = g.snapshot;
|
|
198
|
+
s.extractors_digest = (0, exports.digestDosExtratores)(s.extractors);
|
|
199
|
+
s.snapshot_id = idDoSnapshot(g);
|
|
200
|
+
const novos = new Map();
|
|
201
|
+
for (const n of g.nodes) {
|
|
202
|
+
// Id provisorio repetido tornaria ambigua a extremidade: recusa em vez de escolher um.
|
|
203
|
+
if (novos.has(n.node_id))
|
|
204
|
+
falha('grafo.id.provisorio-duplicado');
|
|
205
|
+
n.snapshot_id = s.snapshot_id;
|
|
206
|
+
const id = idDoNo(g.tenant_id, g.repository_id, s.snapshot_id, n.kind, n.locator);
|
|
207
|
+
novos.set(n.node_id, id);
|
|
208
|
+
n.node_id = id;
|
|
209
|
+
}
|
|
210
|
+
for (const a of g.edges) {
|
|
211
|
+
a.snapshot_id = s.snapshot_id;
|
|
212
|
+
a.from = novos.get(a.from) ?? a.from;
|
|
213
|
+
a.to = novos.get(a.to) ?? a.to;
|
|
214
|
+
for (const e of a.evidence)
|
|
215
|
+
e.snapshot_id = s.snapshot_id;
|
|
216
|
+
a.edge_id = idDaAresta(a);
|
|
217
|
+
}
|
|
218
|
+
return canonizar(g);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Controle C0 e C1, controles bidirecionais e marcas invisiveis sem uso em nome (hifen suave,
|
|
222
|
+
* espaco de largura zero, juntores de palavra, BOM), que fazem um nome parecer outro. ZWJ e ZWNJ
|
|
223
|
+
* ficam: emoji e escritas como a persa e as indicas dependem deles.
|
|
224
|
+
*/
|
|
225
|
+
const CONTROLE = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b\u200e\u200f\u2028\u2029\u202a-\u202e\u2060-\u206f\ufeff\ufff9-\ufffb]/;
|
|
226
|
+
const SURROGATE_ISOLADO = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
227
|
+
const textoValido = (t) => !CONTROLE.test(t) && !SURROGATE_ISOLADO.test(t);
|
|
228
|
+
/**
|
|
229
|
+
* D2: caminho relativo a raiz declarada, com "/" e caixa preservada. O contrato compara caminhos
|
|
230
|
+
* como texto e nao normaliza: forma ambigua e recusada, para dois caminhos distintos nunca se
|
|
231
|
+
* fundirem em silencio. Escape percentual (`%2e`, `%252e`) e recusado inteiro, porque um
|
|
232
|
+
* consumidor que decodifica uma ou duas vezes chegaria a outro caminho. Primeiro segmento com
|
|
233
|
+
* ":" parece esquema de URL ou drive e tambem e recusado.
|
|
234
|
+
*/
|
|
235
|
+
function caminhoValido(p) {
|
|
236
|
+
if (!p || !textoValido(p) || p.includes('\\') || p.startsWith('/') || /%[0-9A-Fa-f]{2}/.test(p))
|
|
237
|
+
return false;
|
|
238
|
+
const segmentos = p.split('/');
|
|
239
|
+
if (segmentos[0].includes(':'))
|
|
240
|
+
return false;
|
|
241
|
+
return segmentos.every((segmento) => segmento !== '' && segmento !== '.' && segmento !== '..');
|
|
242
|
+
}
|
|
243
|
+
const autoridadeDoIndice = (a) => exports.ESQUEMAS_DE_AUTORIDADE_RESERVADOS.includes(a.slice(0, a.indexOf(':')));
|
|
244
|
+
function contem(maior, menor) {
|
|
245
|
+
const s = new Set(maior);
|
|
246
|
+
for (const x of menor)
|
|
247
|
+
if (!s.has(x))
|
|
248
|
+
return false;
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
function conferirAcesso(a, tenantId, onde) {
|
|
252
|
+
if (a.tenant_id !== tenantId)
|
|
253
|
+
falha('grafo.acesso.tenant-divergente', onde);
|
|
254
|
+
for (let i = 1; i < a.acl_refs.length; i++) {
|
|
255
|
+
const c = compararUtf8(a.acl_refs[i - 1], a.acl_refs[i]);
|
|
256
|
+
if (c === 0)
|
|
257
|
+
falha('grafo.acesso.acl-duplicada', onde);
|
|
258
|
+
if (c > 0)
|
|
259
|
+
falha('grafo.acesso.acl-fora-de-ordem', onde);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const chaveDoExtrator = (id, versao) => `${id}@${versao}`;
|
|
263
|
+
function conferirEvidencia(e, c, onde) {
|
|
264
|
+
if (e.snapshot_id !== c.snapshotId)
|
|
265
|
+
falha('grafo.snapshot.divergente', onde);
|
|
266
|
+
if (!caminhoValido(e.path))
|
|
267
|
+
falha('grafo.caminho.invalido', onde);
|
|
268
|
+
const m = c.manifesto.get(e.path);
|
|
269
|
+
if (!m)
|
|
270
|
+
falha('grafo.proveniencia.fonte-ausente', onde);
|
|
271
|
+
if (e.source_hash !== m.source_hash || e.source_version !== m.source_version)
|
|
272
|
+
falha('grafo.proveniencia.fonte-divergente', onde);
|
|
273
|
+
if (autoridadeDoIndice(e.authority))
|
|
274
|
+
falha('grafo.proveniencia.autoridade-indice', onde);
|
|
275
|
+
if (e.authority !== m.authority)
|
|
276
|
+
falha('grafo.proveniencia.autoridade-divergente', onde);
|
|
277
|
+
if (!c.extratores.has(chaveDoExtrator(e.extractor_id, e.extractor_version)))
|
|
278
|
+
falha('grafo.proveniencia.extrator-nao-fixado', onde);
|
|
279
|
+
const s = e.span;
|
|
280
|
+
if (s.byte_start >= s.byte_end)
|
|
281
|
+
falha('grafo.span.intervalo-invalido', onde);
|
|
282
|
+
if (s.type === 'text') {
|
|
283
|
+
if (s.byte_end > m.size_bytes)
|
|
284
|
+
falha('grafo.span.fora-da-fonte', onde);
|
|
285
|
+
if ((s.line_start === null) !== (s.line_end === null) || (s.line_start !== null && s.line_end !== null && s.line_start > s.line_end)) {
|
|
286
|
+
falha('grafo.span.linhas-invalidas', onde);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
conferirAcesso(e.access, c.tenantId, onde);
|
|
290
|
+
if (!contem(e.access.acl_refs, m.access.acl_refs))
|
|
291
|
+
falha('grafo.acesso.restricao-omitida', onde);
|
|
292
|
+
}
|
|
293
|
+
/** Hierarquia `contains` sem ciclo, em profundidade iterativa para nao estourar a pilha. */
|
|
294
|
+
function semCiclo(filhos) {
|
|
295
|
+
const estado = new Map();
|
|
296
|
+
for (const raiz of filhos.keys()) {
|
|
297
|
+
if (estado.has(raiz))
|
|
298
|
+
continue;
|
|
299
|
+
estado.set(raiz, 'aberto');
|
|
300
|
+
const pilha = [[raiz, 0]];
|
|
301
|
+
while (pilha.length) {
|
|
302
|
+
const topo = pilha[pilha.length - 1], lista = filhos.get(topo[0]) ?? [];
|
|
303
|
+
if (topo[1] === lista.length) {
|
|
304
|
+
estado.set(topo[0], 'fechado');
|
|
305
|
+
pilha.pop();
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const filho = lista[topo[1]++], visto = estado.get(filho);
|
|
309
|
+
if (visto === 'aberto')
|
|
310
|
+
return false;
|
|
311
|
+
if (!visto) {
|
|
312
|
+
estado.set(filho, 'aberto');
|
|
313
|
+
pilha.push([filho, 0]);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
function semantica(g) {
|
|
320
|
+
const s = g.snapshot;
|
|
321
|
+
if ((s.revision === null) === (s.revision_unavailable_reason === null))
|
|
322
|
+
falha('grafo.snapshot.revisao-inconsistente', 'snapshot');
|
|
323
|
+
const c = { snapshotId: s.snapshot_id, tenantId: g.tenant_id, manifesto: new Map(), extratores: new Set() };
|
|
324
|
+
const normalizados = new Set();
|
|
325
|
+
s.source_manifest.forEach((m, i) => {
|
|
326
|
+
const onde = `snapshot.source_manifest.${i}`;
|
|
327
|
+
if (!caminhoValido(m.path))
|
|
328
|
+
falha('grafo.caminho.invalido', onde);
|
|
329
|
+
if (c.manifesto.has(m.path))
|
|
330
|
+
falha('grafo.manifesto.duplicado', onde);
|
|
331
|
+
// NFC e NFD sao caminhos distintos aqui, mas um sistema de arquivos que normaliza os fundiria.
|
|
332
|
+
const normalizado = m.path.normalize('NFC');
|
|
333
|
+
if (normalizados.has(normalizado))
|
|
334
|
+
falha('grafo.manifesto.caminho-ambiguo', onde);
|
|
335
|
+
normalizados.add(normalizado);
|
|
336
|
+
if (autoridadeDoIndice(m.authority))
|
|
337
|
+
falha('grafo.proveniencia.autoridade-indice', onde);
|
|
338
|
+
conferirAcesso(m.access, g.tenant_id, onde);
|
|
339
|
+
c.manifesto.set(m.path, m);
|
|
340
|
+
});
|
|
341
|
+
const idsDeExtrator = new Set();
|
|
342
|
+
s.extractors.forEach((e, i) => {
|
|
343
|
+
if (idsDeExtrator.has(e.extractor_id))
|
|
344
|
+
falha('grafo.extrator.duplicado', `snapshot.extractors.${i}`);
|
|
345
|
+
idsDeExtrator.add(e.extractor_id);
|
|
346
|
+
c.extratores.add(chaveDoExtrator(e.extractor_id, e.extractor_version));
|
|
347
|
+
});
|
|
348
|
+
const nos = new Map(), localizadores = new Set();
|
|
349
|
+
g.nodes.forEach((n, i) => {
|
|
350
|
+
const onde = `nodes.${i}`;
|
|
351
|
+
if (n.snapshot_id !== s.snapshot_id)
|
|
352
|
+
falha('grafo.snapshot.divergente', onde);
|
|
353
|
+
if (!caminhoValido(n.locator.path))
|
|
354
|
+
falha('grafo.caminho.invalido', onde);
|
|
355
|
+
if ((n.kind === 'file') !== (n.locator.fragment === null))
|
|
356
|
+
falha('grafo.no.localizador-invalido', onde);
|
|
357
|
+
if (n.locator.fragment !== null && !textoValido(n.locator.fragment))
|
|
358
|
+
falha('grafo.texto.invalido', onde);
|
|
359
|
+
const m = c.manifesto.get(n.locator.path);
|
|
360
|
+
if (!m)
|
|
361
|
+
falha('grafo.no.fonte-ausente', onde);
|
|
362
|
+
if (n.source_hash !== m.source_hash)
|
|
363
|
+
falha('grafo.no.hash-divergente', onde);
|
|
364
|
+
conferirAcesso(n.access, g.tenant_id, onde);
|
|
365
|
+
if (!contem(n.access.acl_refs, m.access.acl_refs))
|
|
366
|
+
falha('grafo.acesso.restricao-omitida', onde);
|
|
367
|
+
const localizador = canonico([n.kind, n.locator]);
|
|
368
|
+
if (nos.has(n.node_id) || localizadores.has(localizador))
|
|
369
|
+
falha('grafo.no.duplicado', onde);
|
|
370
|
+
nos.set(n.node_id, n);
|
|
371
|
+
localizadores.add(localizador);
|
|
372
|
+
});
|
|
373
|
+
const arestas = new Set(), triplas = new Set(), contidos = new Map(), pais = new Set();
|
|
374
|
+
const textosDePagina = new Map();
|
|
375
|
+
g.edges.forEach((a, i) => {
|
|
376
|
+
const onde = `edges.${i}`;
|
|
377
|
+
if (a.snapshot_id !== s.snapshot_id)
|
|
378
|
+
falha('grafo.snapshot.divergente', onde);
|
|
379
|
+
const de = nos.get(a.from), para = nos.get(a.to);
|
|
380
|
+
if (!de || !para)
|
|
381
|
+
falha('grafo.aresta.endpoint-ausente', onde);
|
|
382
|
+
if (!exports.MATRIZ_DE_ARESTAS[a.kind][de.kind].includes(para.kind))
|
|
383
|
+
falha('grafo.aresta.kind-incompativel', onde);
|
|
384
|
+
// Hierarquia e declaracao moram dentro de um arquivo; a relacao entre arquivos e imports ou references.
|
|
385
|
+
if ((a.kind === 'contains' || a.kind === 'declares') && de.locator.path !== para.locator.path)
|
|
386
|
+
falha('grafo.aresta.fora-do-arquivo', onde);
|
|
387
|
+
// D5: conjuncao das restricoes das extremidades e das evidencias, nunca uniao de permissoes.
|
|
388
|
+
const exigidas = new Set([...de.access.acl_refs, ...para.access.acl_refs]), vistas = new Set();
|
|
389
|
+
a.evidence.forEach((e, j) => {
|
|
390
|
+
const ondeDaEvidencia = `${onde}.evidence.${j}`;
|
|
391
|
+
conferirEvidencia(e, c, ondeDaEvidencia);
|
|
392
|
+
if (e.span.type === 'pdf-text') {
|
|
393
|
+
// A mesma pagina da mesma fonte tem um texto extraido so; dois hashes nao podem valer juntos.
|
|
394
|
+
const pagina = canonico([e.path, e.span.page]), anterior = textosDePagina.get(pagina);
|
|
395
|
+
if (anterior !== undefined && anterior !== e.span.extracted_text_hash)
|
|
396
|
+
falha('grafo.span.texto-da-pagina-divergente', ondeDaEvidencia);
|
|
397
|
+
textosDePagina.set(pagina, e.span.extracted_text_hash);
|
|
398
|
+
}
|
|
399
|
+
const chave = canonico(e);
|
|
400
|
+
if (vistas.has(chave))
|
|
401
|
+
falha('grafo.proveniencia.evidencia-duplicada', ondeDaEvidencia);
|
|
402
|
+
vistas.add(chave);
|
|
403
|
+
e.access.acl_refs.forEach((r) => exigidas.add(r));
|
|
404
|
+
});
|
|
405
|
+
// A relacao e observada na fonte de onde ela parte: sem evidencia la, a aresta nao e localizavel.
|
|
406
|
+
if (!a.evidence.some((e) => e.path === de.locator.path))
|
|
407
|
+
falha('grafo.proveniencia.origem-fora-do-endpoint', onde);
|
|
408
|
+
conferirAcesso(a.access, g.tenant_id, onde);
|
|
409
|
+
if (!contem(a.access.acl_refs, exigidas))
|
|
410
|
+
falha('grafo.acesso.restricao-omitida', onde);
|
|
411
|
+
const tripla = canonico([a.kind, a.from, a.to]);
|
|
412
|
+
if (arestas.has(a.edge_id) || triplas.has(tripla))
|
|
413
|
+
falha('grafo.aresta.duplicada', onde);
|
|
414
|
+
arestas.add(a.edge_id);
|
|
415
|
+
triplas.add(tripla);
|
|
416
|
+
if (a.kind === 'contains') {
|
|
417
|
+
// Depois da duplicata: a mesma aresta repetida e duplicata, nao um segundo pai.
|
|
418
|
+
if (pais.has(a.to))
|
|
419
|
+
falha('grafo.aresta.contains-com-dois-pais', onde);
|
|
420
|
+
pais.add(a.to);
|
|
421
|
+
const lista = contidos.get(a.from) ?? [];
|
|
422
|
+
lista.push(a.to);
|
|
423
|
+
contidos.set(a.from, lista);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
if (!semCiclo(contidos))
|
|
427
|
+
falha('grafo.aresta.ciclo-contains');
|
|
428
|
+
const diagnosticos = new Set();
|
|
429
|
+
g.diagnostics.forEach((d, i) => {
|
|
430
|
+
const onde = `diagnostics.${i}`;
|
|
431
|
+
if (!caminhoValido(d.path))
|
|
432
|
+
falha('grafo.caminho.invalido', onde);
|
|
433
|
+
if (d.reference !== null && !textoValido(d.reference))
|
|
434
|
+
falha('grafo.texto.invalido', onde);
|
|
435
|
+
const m = c.manifesto.get(d.path);
|
|
436
|
+
if (!m)
|
|
437
|
+
falha('grafo.diagnostico.fonte-ausente', onde);
|
|
438
|
+
if (!c.extratores.has(chaveDoExtrator(d.extractor_id, d.extractor_version)))
|
|
439
|
+
falha('grafo.proveniencia.extrator-nao-fixado', onde);
|
|
440
|
+
conferirAcesso(d.access, g.tenant_id, onde);
|
|
441
|
+
if (!contem(d.access.acl_refs, m.access.acl_refs))
|
|
442
|
+
falha('grafo.acesso.restricao-omitida', onde);
|
|
443
|
+
const chave = canonico(d);
|
|
444
|
+
if (diagnosticos.has(chave))
|
|
445
|
+
falha('grafo.diagnostico.duplicado', onde);
|
|
446
|
+
diagnosticos.add(chave);
|
|
447
|
+
});
|
|
448
|
+
// D2: id fornecido pelo produtor e recalculado; divergencia e recusa, nunca correcao silenciosa.
|
|
449
|
+
if (s.extractors_digest !== (0, exports.digestDosExtratores)(s.extractors) || s.snapshot_id !== idDoSnapshot(g))
|
|
450
|
+
falha('grafo.id.divergente', 'snapshot');
|
|
451
|
+
g.nodes.forEach((n, i) => {
|
|
452
|
+
if (n.node_id !== idDoNo(g.tenant_id, g.repository_id, s.snapshot_id, n.kind, n.locator))
|
|
453
|
+
falha('grafo.id.divergente', `nodes.${i}`);
|
|
454
|
+
});
|
|
455
|
+
g.edges.forEach((a, i) => {
|
|
456
|
+
if (a.edge_id !== idDaAresta(a))
|
|
457
|
+
falha('grafo.id.divergente', `edges.${i}`);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Validacao estrutural e semantica pura. Devolve o grafo na forma canonica (conjuntos ordenados)
|
|
462
|
+
* ou lanca erro com codigo `grafo.*`. Sem fontes, a evidencia so e conferida na estrutura.
|
|
463
|
+
*/
|
|
464
|
+
function validarGrafo(entrada) {
|
|
465
|
+
if (entrada !== null && typeof entrada === 'object' && !Array.isArray(entrada) && 'schema' in entrada &&
|
|
466
|
+
entrada.schema !== exports.GRAFO_SCHEMA)
|
|
467
|
+
falha('grafo.versao.incompativel');
|
|
468
|
+
const r = exports.grafoSchema.safeParse(entrada);
|
|
469
|
+
if (!r.success)
|
|
470
|
+
falha('grafo.estrutura.invalida', r.error.issues[0]?.path.join('.') || 'raiz');
|
|
471
|
+
semantica(r.data);
|
|
472
|
+
return canonizar(r.data);
|
|
473
|
+
}
|
|
474
|
+
/** D2: digest canonico do grafo valido; permutar nos, arestas ou evidencias nao o muda. */
|
|
475
|
+
const digestDoGrafo = (entrada) => (0, exports.sha256DoCanonico)(validarGrafo(entrada));
|
|
476
|
+
exports.digestDoGrafo = digestDoGrafo;
|
|
477
|
+
function utf8Valido(b) {
|
|
478
|
+
try {
|
|
479
|
+
new TextDecoder('utf-8', { fatal: true }).decode(b);
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const fronteiraUtf8 = (b, i) => i === b.length || (i < b.length && (b[i] & 0xc0) !== 0x80);
|
|
487
|
+
function quebrasDeLinha(b) {
|
|
488
|
+
const q = [];
|
|
489
|
+
for (let i = 0; i < b.length; i++)
|
|
490
|
+
if (b[i] === 0x0a)
|
|
491
|
+
q.push(i);
|
|
492
|
+
return q;
|
|
493
|
+
}
|
|
494
|
+
/** Linha (1-based) do byte `o`: uma a mais que as quebras antes dele. */
|
|
495
|
+
function linhaDoByte(quebras, o) {
|
|
496
|
+
let baixo = 0, alto = quebras.length;
|
|
497
|
+
while (baixo < alto) {
|
|
498
|
+
const meio = (baixo + alto) >> 1;
|
|
499
|
+
if (quebras[meio] < o)
|
|
500
|
+
baixo = meio + 1;
|
|
501
|
+
else
|
|
502
|
+
alto = meio;
|
|
503
|
+
}
|
|
504
|
+
return baixo + 1;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* D4: confere hash, tamanho e spans contra bytes fornecidos. Acerto de span nao prova que o
|
|
508
|
+
* parser interpretou a relacao certo; essa prova vem da auditoria de arestas do benchmark.
|
|
509
|
+
*/
|
|
510
|
+
function conferirFontes(entrada, fontes) {
|
|
511
|
+
const g = validarGrafo(entrada);
|
|
512
|
+
const manifesto = new Map(g.snapshot.source_manifest.map((m) => [m.path, m]));
|
|
513
|
+
for (const [p, f] of fontes) {
|
|
514
|
+
if (!manifesto.has(p))
|
|
515
|
+
falha('grafo.fonte.fora-do-manifesto');
|
|
516
|
+
const tipo = f !== null && typeof f === 'object' ? f.tipo : undefined;
|
|
517
|
+
if (!['texto', 'binario', 'pdf'].includes(tipo))
|
|
518
|
+
falha('grafo.fonte.tipo-desconhecido');
|
|
519
|
+
if (!(f.bytes instanceof Uint8Array) || (f.tipo === 'pdf' && !(Array.isArray(f.paginas) && f.paginas.every((x) => x instanceof Uint8Array)))) {
|
|
520
|
+
falha('grafo.fonte.incompleta');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const fontesVerificadas = [], fontesIndisponiveis = [];
|
|
524
|
+
g.snapshot.source_manifest.forEach((m, i) => {
|
|
525
|
+
const onde = `snapshot.source_manifest.${i}`, f = fontes.get(m.path);
|
|
526
|
+
if (!f) {
|
|
527
|
+
fontesIndisponiveis.push(m.path);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (f.bytes.length !== m.size_bytes)
|
|
531
|
+
falha('grafo.fonte.tamanho-divergente', onde);
|
|
532
|
+
if (sha256DosBytes(f.bytes) !== m.source_hash)
|
|
533
|
+
falha('grafo.fonte.hash-divergente', onde);
|
|
534
|
+
const textos = f.tipo === 'texto' ? [f.bytes] : f.tipo === 'pdf' ? f.paginas : [];
|
|
535
|
+
if (!textos.every(utf8Valido))
|
|
536
|
+
falha('grafo.fonte.utf8-invalido', onde);
|
|
537
|
+
fontesVerificadas.push(m.path);
|
|
538
|
+
});
|
|
539
|
+
const quebras = new Map();
|
|
540
|
+
let evidenciasVerificadas = 0, evidenciasIndisponiveis = 0;
|
|
541
|
+
g.edges.forEach((a, i) => a.evidence.forEach((e, j) => {
|
|
542
|
+
const onde = `edges.${i}.evidence.${j}`, f = fontes.get(e.path), s = e.span;
|
|
543
|
+
if (!f) {
|
|
544
|
+
evidenciasIndisponiveis++;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
let texto;
|
|
548
|
+
if (s.type === 'text') {
|
|
549
|
+
if (f.tipo !== 'texto')
|
|
550
|
+
falha('grafo.span.tipo-incompativel', onde);
|
|
551
|
+
texto = f.bytes;
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
if (f.tipo !== 'pdf')
|
|
555
|
+
falha('grafo.span.tipo-incompativel', onde);
|
|
556
|
+
if (s.page > f.paginas.length)
|
|
557
|
+
falha('grafo.span.pagina-inexistente', onde);
|
|
558
|
+
texto = f.paginas[s.page - 1];
|
|
559
|
+
if (sha256DosBytes(texto) !== s.extracted_text_hash)
|
|
560
|
+
falha('grafo.span.texto-da-pagina-divergente', onde);
|
|
561
|
+
}
|
|
562
|
+
if (s.byte_end > texto.length)
|
|
563
|
+
falha('grafo.span.fora-da-fonte', onde);
|
|
564
|
+
if (!fronteiraUtf8(texto, s.byte_start) || !fronteiraUtf8(texto, s.byte_end))
|
|
565
|
+
falha('grafo.span.fronteira-utf8', onde);
|
|
566
|
+
if (s.type === 'text' && s.line_start !== null) {
|
|
567
|
+
const q = quebras.get(e.path) ?? quebrasDeLinha(texto);
|
|
568
|
+
quebras.set(e.path, q);
|
|
569
|
+
if (linhaDoByte(q, s.byte_start) !== s.line_start || linhaDoByte(q, s.byte_end - 1) !== s.line_end) {
|
|
570
|
+
falha('grafo.span.linhas-divergentes', onde);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
evidenciasVerificadas++;
|
|
574
|
+
}));
|
|
575
|
+
const completa = fontesIndisponiveis.length === 0 && evidenciasIndisponiveis === 0;
|
|
576
|
+
return { estado: completa ? 'verificada' : 'parcial', fontesVerificadas, fontesIndisponiveis, evidenciasVerificadas, evidenciasIndisponiveis };
|
|
577
|
+
}
|