eyeleng 1.0.4 → 1.0.6

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.
@@ -0,0 +1,724 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { pathToFileURL } = require('node:url');
6
+ const { parseNQuads, parseN3, termToNQuads, tripleToNQuads, triplesToNQuads } = require('./rdfSyntax.js');
7
+ const { evaluateEntailmentTest } = require('./rdfEntailment.js');
8
+
9
+ // ---- W3C RDF manifest runner ----
10
+
11
+ const defaultW3cRdfManifestUrls = Object.freeze([
12
+ 'https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-triples/manifest.ttl',
13
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-triples/syntax/manifest.ttl',
14
+ 'https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-n-quads/manifest.ttl',
15
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-n-quads/syntax/manifest.ttl',
16
+ 'https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-mt/manifest.ttl',
17
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-semantics/manifest.ttl',
18
+ 'https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-turtle/manifest.ttl',
19
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/eval/manifest.ttl',
20
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-turtle/syntax/manifest.ttl',
21
+ 'https://w3c.github.io/rdf-tests/rdf/rdf11/rdf-trig/manifest.ttl',
22
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/eval/manifest.ttl',
23
+ 'https://w3c.github.io/rdf-tests/rdf/rdf12/rdf-trig/syntax/manifest.ttl',
24
+ ]);
25
+
26
+ const SUPPORTED_SYNTAX_TYPES = new Set([
27
+ 'rdft:TestNTriplesPositiveSyntax',
28
+ 'rdft:TestNTriplesNegativeSyntax',
29
+ 'rdft:TestNQuadsPositiveSyntax',
30
+ 'rdft:TestNQuadsNegativeSyntax',
31
+ 'rdft:TestTurtlePositiveSyntax',
32
+ 'rdft:TestTurtleNegativeSyntax',
33
+ 'rdft:TestTrigPositiveSyntax',
34
+ 'rdft:TestTrigNegativeSyntax',
35
+ ]);
36
+
37
+ const SUPPORTED_EVAL_TYPES = new Set([
38
+ 'rdft:TestNTriplesEval',
39
+ 'rdft:TestNQuadsEval',
40
+ 'rdft:TestTurtleEval',
41
+ 'rdft:TestTrigEval',
42
+ ]);
43
+
44
+ const NEGATIVE_TYPES = new Set([
45
+ 'rdft:TestNTriplesNegativeSyntax',
46
+ 'rdft:TestNQuadsNegativeSyntax',
47
+ 'rdft:TestTurtleNegativeSyntax',
48
+ 'rdft:TestTrigNegativeSyntax',
49
+ 'rdft:TestTurtleNegativeEval',
50
+ 'rdft:TestTrigNegativeEval',
51
+ ]);
52
+
53
+ const SEMANTICS_TYPES = new Set([
54
+ 'mf:PositiveEntailmentTest',
55
+ 'mf:NegativeEntailmentTest',
56
+ 'rdft:PositiveEntailmentTest',
57
+ 'rdft:NegativeEntailmentTest',
58
+ ]);
59
+
60
+ function isUrl(value) {
61
+ return /^https?:\/\//i.test(value || '');
62
+ }
63
+
64
+ function isDirectory(value) {
65
+ try { return fs.statSync(value).isDirectory(); } catch { return false; }
66
+ }
67
+
68
+ function normalizeResource(resource) {
69
+ if (isUrl(resource)) return resource;
70
+ const resolved = path.resolve(resource);
71
+ if (isDirectory(resolved)) return path.join(resolved, 'manifest.ttl');
72
+ return resolved;
73
+ }
74
+
75
+ function resourceDirectory(resource) {
76
+ if (isUrl(resource)) return new URL('.', resource).href;
77
+ return path.dirname(path.resolve(resource));
78
+ }
79
+
80
+ function resolveResource(ref, baseResource) {
81
+ if (!ref) return null;
82
+ if (isUrl(ref)) return ref;
83
+ if (ref.startsWith('file:')) return new URL(ref).pathname;
84
+ if (isUrl(baseResource)) return new URL(ref, baseResource).href;
85
+ return path.resolve(resourceDirectory(baseResource), ref);
86
+ }
87
+
88
+ async function readResource(resource) {
89
+ if (isUrl(resource)) {
90
+ if (typeof fetch !== 'function') throw new Error('Remote manifests require global fetch support (Node 18+)');
91
+ const response = await fetch(resource);
92
+ if (!response.ok) throw new Error(`Failed to fetch ${resource}: ${response.status} ${response.statusText}`);
93
+ return await response.text();
94
+ }
95
+ return fs.readFileSync(resource, 'utf8');
96
+ }
97
+
98
+ function stripTurtleComments(text) {
99
+ const out = [];
100
+ let inString = false;
101
+ let quote = null;
102
+ let inIri = false;
103
+ let escaped = false;
104
+ for (let i = 0; i < String(text || '').length; i += 1) {
105
+ const ch = text[i];
106
+ if (escaped) { out.push(ch); escaped = false; continue; }
107
+ if (ch === '\\') { out.push(ch); escaped = true; continue; }
108
+ if (!inIri && (ch === '"' || ch === "'")) {
109
+ if (!inString) { inString = true; quote = ch; }
110
+ else if (quote === ch) { inString = false; quote = null; }
111
+ out.push(ch);
112
+ continue;
113
+ }
114
+ if (!inString && ch === '<') { inIri = true; out.push(ch); continue; }
115
+ if (!inString && ch === '>') { inIri = false; out.push(ch); continue; }
116
+ if (ch === '#' && !inString && !inIri) {
117
+ while (i < text.length && text[i] !== '\n') i += 1;
118
+ out.push('\n');
119
+ continue;
120
+ }
121
+ out.push(ch);
122
+ }
123
+ return out.join('');
124
+ }
125
+
126
+ function splitStatements(text) {
127
+ const clean = stripTurtleComments(text);
128
+ const statements = [];
129
+ let start = 0;
130
+ let inString = false;
131
+ let quote = null;
132
+ let inIri = false;
133
+ let escaped = false;
134
+ let bracketDepth = 0;
135
+ let parenDepth = 0;
136
+ for (let i = 0; i < clean.length; i += 1) {
137
+ const ch = clean[i];
138
+ if (escaped) { escaped = false; continue; }
139
+ if (ch === '\\') { escaped = true; continue; }
140
+ if (!inIri && (ch === '"' || ch === "'")) {
141
+ if (!inString) { inString = true; quote = ch; }
142
+ else if (quote === ch) { inString = false; quote = null; }
143
+ continue;
144
+ }
145
+ if (!inString && ch === '<') { inIri = true; continue; }
146
+ if (!inString && ch === '>') { inIri = false; continue; }
147
+ if (inString || inIri) continue;
148
+ if (ch === '[') bracketDepth += 1;
149
+ else if (ch === ']') bracketDepth = Math.max(0, bracketDepth - 1);
150
+ else if (ch === '(') parenDepth += 1;
151
+ else if (ch === ')') parenDepth = Math.max(0, parenDepth - 1);
152
+ else if (ch === '.' && bracketDepth === 0 && parenDepth === 0) {
153
+ const statement = clean.slice(start, i).trim();
154
+ if (statement) statements.push(statement);
155
+ start = i + 1;
156
+ }
157
+ }
158
+ const tail = clean.slice(start).trim();
159
+ if (tail) statements.push(tail);
160
+ return statements;
161
+ }
162
+
163
+ function parseStringLiteral(value) {
164
+ if (!value) return null;
165
+ const token = value.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/)?.[0];
166
+ if (!token) return null;
167
+ if (token.startsWith('"')) return JSON.parse(token);
168
+ return token.slice(1, -1).replace(/\\([nrtbf'"\\])/g, (_, ch) => ({ n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', "'": "'", '"': '"', '\\': '\\' }[ch] ?? ch));
169
+ }
170
+
171
+ function extractFirstIriAfter(statement, predicate) {
172
+ const escaped = predicate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
173
+ const match = statement.match(new RegExp(`${escaped}\\s+<([^>]*)>`));
174
+ return match?.[1] || null;
175
+ }
176
+
177
+ function extractResultAfter(statement) {
178
+ const iri = extractFirstIriAfter(statement, 'mf:result');
179
+ if (iri) return { kind: 'resource', resource: iri };
180
+ if (/mf:result\s+false\b/.test(statement)) return { kind: 'false' };
181
+ return null;
182
+ }
183
+
184
+ function extractTypes(statement) {
185
+ const types = [];
186
+ for (const match of statement.matchAll(/(?:rdf:type|\ba\b)\s+((?:rdft|mf):[A-Za-z][A-Za-z0-9_-]*)/g)) {
187
+ types.push(match[1]);
188
+ }
189
+ return types;
190
+ }
191
+
192
+ function prefixMapFromManifest(text) {
193
+ const prefixes = {
194
+ rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
195
+ rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
196
+ xsd: 'http://www.w3.org/2001/XMLSchema#',
197
+ mf: 'http://www.w3.org/2001/sw/DataAccess/tests/test-manifest#',
198
+ rdft: 'http://www.w3.org/ns/rdftest#',
199
+ };
200
+ for (const match of String(text || '').matchAll(/(?:@prefix|PREFIX)\s+([A-Za-z][A-Za-z0-9_-]*):\s*<([^>]*)>\s*\.?/g)) {
201
+ prefixes[match[1]] = match[2];
202
+ }
203
+ return prefixes;
204
+ }
205
+
206
+ function expandManifestTerm(token, prefixes) {
207
+ if (!token) return null;
208
+ const trimmed = token.trim();
209
+ if (trimmed.startsWith('<') && trimmed.endsWith('>')) return trimmed.slice(1, -1);
210
+ const colon = trimmed.indexOf(':');
211
+ if (colon > 0) {
212
+ const prefix = trimmed.slice(0, colon);
213
+ const local = trimmed.slice(colon + 1);
214
+ if (Object.hasOwn(prefixes, prefix)) return prefixes[prefix] + local;
215
+ }
216
+ return null;
217
+ }
218
+
219
+ function extractIriListAfter(statement, predicate, prefixes) {
220
+ const escaped = predicate.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
221
+ const match = statement.match(new RegExp(`${escaped}\\s+\\(([\\s\\S]*?)\\)`));
222
+ if (!match) return [];
223
+ const out = [];
224
+ for (const token of match[1].match(/<[^>]*>|[A-Za-z][A-Za-z0-9_-]*:[^\s()]+/g) || []) {
225
+ const iriValue = expandManifestTerm(token, prefixes);
226
+ if (iriValue) out.push(iriValue);
227
+ }
228
+ return out;
229
+ }
230
+
231
+
232
+ function extractEntries(statements) {
233
+ const entries = new Set();
234
+ for (const statement of statements) {
235
+ const match = statement.match(/\bmf:entries\s*\(([\s\S]*?)\)/);
236
+ if (!match) continue;
237
+ for (const token of match[1].match(/<[^>]*>|[A-Za-z][A-Za-z0-9_-]*:[^\s()]+/g) || []) {
238
+ entries.add(token);
239
+ }
240
+ }
241
+ return entries;
242
+ }
243
+
244
+ function extractEntailmentRegime(statement) {
245
+ const literalValue = parseStringLiteral(statement.match(/mf:entailmentRegime\s+("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/)?.[1]);
246
+ if (literalValue) return literalValue;
247
+ const word = statement.match(/mf:entailmentRegime\s+([A-Za-z][A-Za-z0-9_-]*)/)?.[1];
248
+ return word || 'simple';
249
+ }
250
+
251
+ function parseManifestText(text, resource) {
252
+ const includes = [];
253
+ const tests = [];
254
+ const statements = splitStatements(text);
255
+ const prefixes = prefixMapFromManifest(text);
256
+ const manifestEntries = extractEntries(statements);
257
+
258
+ for (const statement of statements) {
259
+ const includeMatch = statement.match(/mf:include\s*\(([\s\S]*?)\)/);
260
+ if (includeMatch) {
261
+ for (const iriMatch of includeMatch[1].matchAll(/<([^>]*)>/g)) {
262
+ let ref = iriMatch[1];
263
+ if (!/\.ttl(?:#.*)?$/i.test(ref) && !ref.endsWith('/')) ref += '/';
264
+ includes.push(resolveResource(ref.endsWith('/') ? `${ref}manifest.ttl` : ref, resource));
265
+ }
266
+ }
267
+
268
+ const types = extractTypes(statement);
269
+ const type = types.find((t) => SUPPORTED_SYNTAX_TYPES.has(t) || SUPPORTED_EVAL_TYPES.has(t) || SEMANTICS_TYPES.has(t) || NEGATIVE_TYPES.has(t));
270
+ if (!type) continue;
271
+
272
+ const id = statement.match(/^([^\s;]+)/)?.[1] || null;
273
+ if (manifestEntries.size > 0 && id && !manifestEntries.has(id)) continue;
274
+ const name = parseStringLiteral(statement.match(/mf:name\s+("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/)?.[1]) || id || type;
275
+ const action = extractFirstIriAfter(statement, 'mf:action');
276
+ const resultInfo = extractResultAfter(statement);
277
+ tests.push({
278
+ id,
279
+ type,
280
+ name,
281
+ manifest: resource,
282
+ action: action ? resolveResource(action, resource) : null,
283
+ result: resultInfo?.kind === 'resource' ? resolveResource(resultInfo.resource, resource) : null,
284
+ resultKind: resultInfo?.kind || null,
285
+ entailmentRegime: SEMANTICS_TYPES.has(type) ? extractEntailmentRegime(statement) : null,
286
+ recognizedDatatypes: SEMANTICS_TYPES.has(type) ? extractIriListAfter(statement, 'mf:recognizedDatatypes', prefixes) : [],
287
+ unrecognizedDatatypes: SEMANTICS_TYPES.has(type) ? extractIriListAfter(statement, 'mf:unrecognizedDatatypes', prefixes) : [],
288
+ });
289
+ }
290
+
291
+ return { resource, includes, tests };
292
+ }
293
+
294
+ async function loadW3cRdfManifest(resource, options = {}) {
295
+ const root = normalizeResource(resource);
296
+ const seen = new Set();
297
+ const manifests = [];
298
+ const tests = [];
299
+
300
+ async function visit(current) {
301
+ if (!current || seen.has(current)) return;
302
+ seen.add(current);
303
+ const text = await readResource(current);
304
+ const manifest = parseManifestText(text, current);
305
+ manifests.push({ resource: current, includeCount: manifest.includes.length, testCount: manifest.tests.length });
306
+ tests.push(...manifest.tests);
307
+ if (options.followIncludes !== false) {
308
+ for (const include of manifest.includes) await visit(include);
309
+ }
310
+ }
311
+
312
+ await visit(root);
313
+ return { root, manifests, tests };
314
+ }
315
+
316
+ function parserForResource(resource, type) {
317
+ const lower = String(resource || '').toLowerCase();
318
+ if (type?.includes('NTriples') || lower.endsWith('.nt')) return 'ntriples';
319
+ if (type?.includes('NQuads') || lower.endsWith('.nq')) return 'nquads';
320
+ if (type?.includes('Trig') || lower.endsWith('.trig')) return 'trig';
321
+ if (type?.includes('Turtle') || lower.endsWith('.ttl')) return 'turtle';
322
+ return 'unknown';
323
+ }
324
+
325
+ function parseGraph(source, resource, type) {
326
+ const parser = parserForResource(resource, type);
327
+ if (parser === 'ntriples' || parser === 'nquads') return parseNQuads(source, { profileId: parser === 'ntriples' ? 'ntriples-graph-v0' : 'nquads-dataset-v0', format: parser });
328
+ if (parser === 'turtle' || parser === 'trig') return parseN3(source, { profile: parser, filename: resource, base: isUrl(resource) ? resource : pathToFileURL(path.resolve(resource)).href });
329
+ throw new Error(`No parser selected for ${resource || type}`);
330
+ }
331
+
332
+
333
+ function collectBlankTermsInTerm(term, out) {
334
+ if (!term) return;
335
+ if (term.kind === 'blank') out.add(term.value);
336
+ else if (term.kind === 'triple') {
337
+ collectBlankTermsInTerm(term.s, out);
338
+ collectBlankTermsInTerm(term.p, out);
339
+ collectBlankTermsInTerm(term.o, out);
340
+ }
341
+ }
342
+
343
+ function collectBlankLabels(triples) {
344
+ const out = new Set();
345
+ for (const t of triples || []) {
346
+ collectBlankTermsInTerm(t.s, out);
347
+ collectBlankTermsInTerm(t.p, out);
348
+ collectBlankTermsInTerm(t.o, out);
349
+ collectBlankTermsInTerm(t.graph, out);
350
+ }
351
+ return Array.from(out).sort();
352
+ }
353
+
354
+ function termIsoString(term, mapping = new Map()) {
355
+ if (!term) return '';
356
+ if (term.kind === 'iri') return `<${term.value}>`;
357
+ if (term.kind === 'blank') return `_:${mapping.get(term.value) || term.value}`;
358
+ if (term.kind === 'literal') return `"${term.value}"^^${term.datatype || ''}@${term.language || ''}`;
359
+ if (term.kind === 'triple') return `<<${termIsoString(term.s, mapping)} ${termIsoString(term.p, mapping)} ${termIsoString(term.o, mapping)}>>`;
360
+ return JSON.stringify(term);
361
+ }
362
+
363
+ function tripleIsoString(t, mapping = new Map()) {
364
+ return `${termIsoString(t.s, mapping)} ${termIsoString(t.p, mapping)} ${termIsoString(t.o, mapping)} ${termIsoString(t.graph, mapping)}`;
365
+ }
366
+
367
+ function* permutations(values) {
368
+ if (values.length === 0) { yield []; return; }
369
+ for (let i = 0; i < values.length; i += 1) {
370
+ const first = values[i];
371
+ const rest = values.slice(0, i).concat(values.slice(i + 1));
372
+ for (const tail of permutations(rest)) yield [first, ...tail];
373
+ }
374
+ }
375
+
376
+ function blankPositionSignature(label, triples) {
377
+ const parts = [];
378
+ function visit(term, path) {
379
+ if (!term) return;
380
+ if (term.kind === 'blank' && term.value === label) parts.push(path);
381
+ else if (term.kind === 'triple') {
382
+ visit(term.s, `${path}/ts`);
383
+ visit(term.p, `${path}/tp`);
384
+ visit(term.o, `${path}/to`);
385
+ }
386
+ }
387
+ for (const t of triples || []) {
388
+ visit(t.s, 's');
389
+ visit(t.p, 'p');
390
+ visit(t.o, 'o');
391
+ visit(t.graph, 'g');
392
+ }
393
+ return parts.sort().join('|');
394
+ }
395
+
396
+ function termIsoStringPartial(term, mapping = new Map(), requireMapped = true) {
397
+ if (!term) return '';
398
+ if (term.kind === 'iri') return `<${term.value}>`;
399
+ if (term.kind === 'blank') {
400
+ const mapped = mapping.get(term.value);
401
+ if (!mapped && requireMapped) return null;
402
+ return `_:${mapped || term.value}`;
403
+ }
404
+ if (term.kind === 'literal') return `"${term.value}"^^${term.datatype || ''}@${term.language || ''}`;
405
+ if (term.kind === 'triple') {
406
+ const s = termIsoStringPartial(term.s, mapping, requireMapped);
407
+ const p = termIsoStringPartial(term.p, mapping, requireMapped);
408
+ const o = termIsoStringPartial(term.o, mapping, requireMapped);
409
+ if (s == null || p == null || o == null) return null;
410
+ return `<<${s} ${p} ${o}>>`;
411
+ }
412
+ return JSON.stringify(term);
413
+ }
414
+
415
+ function tripleIsoStringPartial(t, mapping = new Map(), requireMapped = true) {
416
+ const s = termIsoStringPartial(t.s, mapping, requireMapped);
417
+ const p = termIsoStringPartial(t.p, mapping, requireMapped);
418
+ const o = termIsoStringPartial(t.o, mapping, requireMapped);
419
+ const g = termIsoStringPartial(t.graph, mapping, requireMapped);
420
+ if (s == null || p == null || o == null || g == null) return null;
421
+ return `${s} ${p} ${o} ${g}`;
422
+ }
423
+
424
+ function uniqueTriplesForIso(triples) {
425
+ // RDF graphs/datasets are sets. Some eval inputs repeat an asserted triple while
426
+ // adding distinct RDF 1.2 annotation blocks; keep only exact duplicate triples
427
+ // before doing graph-isomorphism. This preserves distinct blank-node structures
428
+ // while avoiding false mismatches from duplicate asserted triples.
429
+ const seen = new Set();
430
+ const out = [];
431
+ for (const t of triples || []) {
432
+ const key = tripleIsoString(t);
433
+ if (seen.has(key)) continue;
434
+ seen.add(key);
435
+ out.push(t);
436
+ }
437
+ return out;
438
+ }
439
+
440
+ function graphsIsomorphic(actualTriples0, expectedTriples0) {
441
+ const actualTriples = uniqueTriplesForIso(actualTriples0);
442
+ const expectedTriples = uniqueTriplesForIso(expectedTriples0);
443
+ if ((actualTriples || []).length !== (expectedTriples || []).length) return false;
444
+ const actualBlanks = collectBlankLabels(actualTriples);
445
+ const expectedBlanks = collectBlankLabels(expectedTriples);
446
+ if (actualBlanks.length !== expectedBlanks.length) return false;
447
+ const expectedSet = new Set((expectedTriples || []).map((t) => tripleIsoString(t)));
448
+ if (actualBlanks.length === 0) return (actualTriples || []).every((t) => expectedSet.has(tripleIsoString(t)));
449
+
450
+ const expectedSig = new Map(expectedBlanks.map((label) => [label, blankPositionSignature(label, expectedTriples)]));
451
+ const actualSig = new Map(actualBlanks.map((label) => [label, blankPositionSignature(label, actualTriples)]));
452
+ const order = [...actualBlanks].sort((a, b) => {
453
+ const ca = (actualTriples || []).filter((t) => tripleIsoString(t).includes(`_:${a}`)).length;
454
+ const cb = (actualTriples || []).filter((t) => tripleIsoString(t).includes(`_:${b}`)).length;
455
+ return cb - ca;
456
+ });
457
+ const candidates = new Map(order.map((a) => {
458
+ // Do not over-constrain by local position signatures: RDF 1.2 annotation
459
+ // tests often contain several structurally similar fresh reifiers whose
460
+ // identities are intentionally arbitrary. Exhaustive search is fine for
461
+ // the small W3C eval graphs used here.
462
+ return [a, expectedBlanks];
463
+ }));
464
+
465
+ function partialConsistent(mapping) {
466
+ for (const t of actualTriples || []) {
467
+ const str = tripleIsoStringPartial(t, mapping, true);
468
+ if (str && !expectedSet.has(str)) return false;
469
+ }
470
+ return true;
471
+ }
472
+
473
+ function search(index, mapping, used) {
474
+ if (index >= order.length) return (actualTriples || []).every((t) => expectedSet.has(tripleIsoString(t, mapping)));
475
+ const a = order[index];
476
+ for (const b of candidates.get(a) || expectedBlanks) {
477
+ if (used.has(b)) continue;
478
+ mapping.set(a, b);
479
+ used.add(b);
480
+ // The W3C eval graphs are small. Avoid over-pruning: RDF 1.2 annotation tests
481
+ // can contain several fresh reifiers with identical rdf:reifies edges but
482
+ // distinct annotation payloads, and a premature partial check can reject
483
+ // the mapping before the other fresh nodes are assigned.
484
+ if (search(index + 1, mapping, used)) return true;
485
+ used.delete(b);
486
+ mapping.delete(a);
487
+ }
488
+ return false;
489
+ }
490
+ return search(0, new Map(), new Set());
491
+ }
492
+ function datasetSet(program) {
493
+ return new Set(triplesToNQuads(program.facts || []).split('\n').filter(Boolean));
494
+ }
495
+
496
+ function setEquals(a, b) {
497
+ if (a.size !== b.size) return false;
498
+ for (const value of a) if (!b.has(value)) return false;
499
+ return true;
500
+ }
501
+
502
+ function setDiff(a, b, limit = 5) {
503
+ const out = [];
504
+ for (const value of a) {
505
+ if (!b.has(value)) out.push(value);
506
+ if (out.length >= limit) break;
507
+ }
508
+ return out;
509
+ }
510
+
511
+ async function runSyntaxTest(test) {
512
+ if (!test.action) return { status: 'fail', message: 'missing mf:action' };
513
+ const expectAccept = !NEGATIVE_TYPES.has(test.type);
514
+ try {
515
+ const source = await readResource(test.action);
516
+ parseGraph(source, test.action, test.type);
517
+ return expectAccept
518
+ ? { status: 'pass', message: 'accepted as expected' }
519
+ : { status: 'fail', message: 'negative syntax test was accepted' };
520
+ } catch (error) {
521
+ return expectAccept
522
+ ? { status: 'fail', message: `positive syntax/eval test was rejected: ${error.message}` }
523
+ : { status: 'pass', message: `rejected as expected: ${error.message}` };
524
+ }
525
+ }
526
+
527
+ async function runEvalTest(test) {
528
+ if (!test.action || !test.result) return { status: 'fail', message: 'missing mf:action or mf:result' };
529
+ try {
530
+ const [actionText, resultText] = await Promise.all([readResource(test.action), readResource(test.result)]);
531
+ const actualProgram = parseGraph(actionText, test.action, test.type);
532
+ const expectedProgram = parseGraph(resultText, test.result, test.type);
533
+ if (graphsIsomorphic(actualProgram.facts || [], expectedProgram.facts || [])) return { status: 'pass', message: 'parsed graph matches expected result graph' };
534
+ const actual = datasetSet(actualProgram);
535
+ const expected = datasetSet(expectedProgram);
536
+ const missing = setDiff(expected, actual);
537
+ const extra = setDiff(actual, expected);
538
+ return { status: 'fail', message: `graph mismatch: missing ${missing.length ? missing.join(' | ') : 'none'}; extra ${extra.length ? extra.join(' | ') : 'none'}` };
539
+ } catch (error) {
540
+ return { status: 'fail', message: error.message };
541
+ }
542
+ }
543
+
544
+ async function runEntailmentTest(test) {
545
+ if (!test.action) return { status: 'fail', message: 'missing mf:action' };
546
+ try {
547
+ const actionText = await readResource(test.action);
548
+ const actionProgram = parseGraph(actionText, test.action, test.type);
549
+ let resultProgram = null;
550
+ if (test.resultKind !== 'false') {
551
+ if (!test.result) return { status: 'fail', message: 'missing mf:result' };
552
+ const resultText = await readResource(test.result);
553
+ resultProgram = parseGraph(resultText, test.result, test.type);
554
+ }
555
+ const positive = /PositiveEntailmentTest$/.test(test.type);
556
+ const evaluated = evaluateEntailmentTest(actionProgram.facts || [], resultProgram ? resultProgram.facts || [] : [], {
557
+ positive,
558
+ resultKind: test.resultKind,
559
+ regime: test.entailmentRegime || 'simple',
560
+ recognizedDatatypes: test.recognizedDatatypes || [],
561
+ unrecognizedDatatypes: test.unrecognizedDatatypes || [],
562
+ });
563
+ return evaluated.passed
564
+ ? { status: 'pass', message: `${test.entailmentRegime || 'simple'} entailment: ${evaluated.message}` }
565
+ : { status: 'fail', message: `${test.entailmentRegime || 'simple'} entailment failed: ${evaluated.message}` };
566
+ } catch (error) {
567
+ return { status: 'fail', message: error.message };
568
+ }
569
+ }
570
+
571
+ async function runRdfTest(test) {
572
+ const t0 = Date.now();
573
+ let outcome;
574
+ if (SEMANTICS_TYPES.has(test.type)) outcome = await runEntailmentTest(test);
575
+ else if (SUPPORTED_EVAL_TYPES.has(test.type)) outcome = await runEvalTest(test);
576
+ else if (SUPPORTED_SYNTAX_TYPES.has(test.type) || NEGATIVE_TYPES.has(test.type)) outcome = await runSyntaxTest(test);
577
+ else outcome = { status: 'skip', message: `unsupported RDF test type ${test.type}` };
578
+ return { ...test, ...outcome, durationMs: Date.now() - t0 };
579
+ }
580
+
581
+ async function runW3cRdfManifest(resource, options = {}) {
582
+ const t0 = Date.now();
583
+ const manifest = await loadW3cRdfManifest(resource, options);
584
+ const results = [];
585
+ for (let index = 0; index < manifest.tests.length; index += 1) {
586
+ const item = await runRdfTest(manifest.tests[index]);
587
+ results.push(item);
588
+ if (typeof options.onProgress === 'function') options.onProgress(item, index, manifest.tests.length, manifest.root);
589
+ }
590
+ const counts = {
591
+ total: results.length,
592
+ pass: results.filter((r) => r.status === 'pass').length,
593
+ fail: results.filter((r) => r.status === 'fail').length,
594
+ skip: results.filter((r) => r.status === 'skip').length,
595
+ };
596
+ return { ok: counts.fail === 0 && counts.total > 0, source: manifest.root, manifests: manifest.manifests, counts, durationMs: Date.now() - t0, results };
597
+ }
598
+
599
+ async function runW3cRdfManifests(resources = defaultW3cRdfManifestUrls, options = {}) {
600
+ const t0 = Date.now();
601
+ const inputs = Array.isArray(resources) && resources.length ? resources : defaultW3cRdfManifestUrls;
602
+ const manifests = [];
603
+ for (const resource of inputs) {
604
+ if (typeof options.onManifestStart === 'function') options.onManifestStart(resource, manifests.length, inputs.length);
605
+ const result = await runW3cRdfManifest(resource, options);
606
+ manifests.push(result);
607
+ if (typeof options.onManifestDone === 'function') options.onManifestDone(result, manifests.length - 1, inputs.length);
608
+ }
609
+ const counts = manifests.reduce((acc, result) => {
610
+ acc.total += result.counts.total;
611
+ acc.pass += result.counts.pass;
612
+ acc.fail += result.counts.fail;
613
+ acc.skip += result.counts.skip;
614
+ return acc;
615
+ }, { total: 0, pass: 0, fail: 0, skip: 0 });
616
+ return { ok: counts.fail === 0 && counts.total > 0, manifestCount: manifests.length, counts, durationMs: Date.now() - t0, manifests };
617
+ }
618
+
619
+ function nullColors() {
620
+ return { g: '', r: '', y: '', dim: '', n: '' };
621
+ }
622
+
623
+ function colorizeStatus(status, colors = nullColors()) {
624
+ if (status === 'pass') return `${colors.g}OK${colors.n}`;
625
+ if (status === 'skip') return `${colors.y}SKIP${colors.n}`;
626
+ return `${colors.r}FAIL${colors.n}`;
627
+ }
628
+
629
+ function formatMs(ms, colors = nullColors()) {
630
+ return `${colors.dim}(${ms} ms)${colors.n}`;
631
+ }
632
+
633
+ function colorizeTextForStatus(status, text, colors = nullColors()) {
634
+ if (status === 'pass') return `${colors.g}${text}${colors.n}`;
635
+ if (status === 'skip') return `${colors.y}${text}${colors.n}`;
636
+ return `${colors.r}${text}${colors.n}`;
637
+ }
638
+
639
+ function formatW3cRdfProgressLine(item, index, options = {}) {
640
+ const C = options.colors || nullColors();
641
+ const tag = colorizeStatus(item.status, C);
642
+ const idx = `${C.dim}${String(index + 1).padStart(3, '0')}${C.n}`;
643
+ const description = colorizeTextForStatus(item.status, `${item.type} ${item.name}`, C);
644
+ let line = `${idx} ${tag} ${description} ${formatMs(item.durationMs, C)}`;
645
+ if (item.status !== 'pass') line += `\n ${colorizeTextForStatus(item.status, item.message, C)}`;
646
+ return line;
647
+ }
648
+
649
+ function formatW3cRdfManifestSummaryLine(result, options = {}) {
650
+ const C = options.colors || nullColors();
651
+ const skipPart = result.counts.skip ? `, ${result.counts.skip} skipped` : '';
652
+ const status = colorizeStatus(result.counts.fail === 0 ? 'pass' : 'fail', C);
653
+ return `${status} ${result.counts.pass}/${result.counts.total} tests passed${skipPart} ${formatMs(result.durationMs, C)}`;
654
+ }
655
+
656
+ function formatW3cRdfManifestResult(result, options = {}) {
657
+ const C = options.colors || nullColors();
658
+ const lines = [];
659
+ lines.push(`${C.y}==${C.n} W3C RDF manifest`);
660
+ lines.push(`Source: ${result.source}`);
661
+ lines.push(`Manifests: ${result.manifests.length}`);
662
+ result.results.forEach((item, index) => lines.push(formatW3cRdfProgressLine(item, index, options)));
663
+ lines.push(formatW3cRdfManifestSummaryLine(result, options));
664
+ return lines.join('\n');
665
+ }
666
+
667
+ function formatW3cRdfManifestsResult(result, options = {}) {
668
+ const C = options.colors || nullColors();
669
+ const lines = [`${C.y}==${C.n} W3C RDF manifests`];
670
+ for (const manifest of result.manifests) lines.push(formatW3cRdfManifestResult(manifest, options));
671
+ const skipPart = result.counts.skip ? `, ${result.counts.skip} skipped` : '';
672
+ const status = colorizeStatus(result.counts.fail === 0 ? 'pass' : 'fail', C);
673
+ lines.push(`${C.y}==${C.n} Total`);
674
+ lines.push(`${status} ${result.counts.pass}/${result.counts.total} tests passed${skipPart} across ${result.manifestCount} manifest(s) ${formatMs(result.durationMs, C)}`);
675
+ return lines.join('\n');
676
+ }
677
+
678
+ function rdfManifestsToEarl(result, options = {}) {
679
+ const assertedBy = options.assertedBy || '<https://github.com/eyereasoner/eyeleng>';
680
+ const lines = [
681
+ '@prefix earl: <http://www.w3.org/ns/earl#> .',
682
+ '@prefix doap: <http://usefulinc.com/ns/doap#> .',
683
+ '@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .',
684
+ '',
685
+ `${assertedBy} a earl:Software, doap:Project ;`,
686
+ ' doap:name "Eyeleng" .',
687
+ '',
688
+ ];
689
+ for (const manifest of result.manifests || []) {
690
+ for (const item of manifest.results || []) {
691
+ const outcome = item.status === 'pass' ? 'earl:passed' : item.status === 'skip' ? 'earl:untested' : 'earl:failed';
692
+ const rawTestUri = item.action || item.id || `urn:eyeleng:w3c-rdf:${item.name}`;
693
+ const testUri = isUrl(rawTestUri) || String(rawTestUri).startsWith('urn:') ? rawTestUri : pathToFileURL(rawTestUri).href;
694
+ lines.push('[] a earl:Assertion ;');
695
+ lines.push(` earl:assertedBy ${assertedBy} ;`);
696
+ lines.push(` earl:subject ${assertedBy} ;`);
697
+ lines.push(` earl:test <${String(testUri).replace(/[<>]/g, '')}> ;`);
698
+ lines.push(' earl:result [');
699
+ lines.push(' a earl:TestResult ;');
700
+ lines.push(` earl:outcome ${outcome} ;`);
701
+ lines.push(` earl:info ${JSON.stringify(item.message || item.status)} ;`);
702
+ lines.push(' ] .');
703
+ lines.push('');
704
+ }
705
+ }
706
+ return lines.join('\n');
707
+ }
708
+
709
+ module.exports = {
710
+ defaultW3cRdfManifestUrls,
711
+ parseNQuads,
712
+ termToNQuads,
713
+ tripleToNQuads,
714
+ triplesToNQuads,
715
+ parseN3,
716
+ loadW3cRdfManifest,
717
+ runW3cRdfManifest,
718
+ runW3cRdfManifests,
719
+ formatW3cRdfProgressLine,
720
+ formatW3cRdfManifestSummaryLine,
721
+ formatW3cRdfManifestResult,
722
+ formatW3cRdfManifestsResult,
723
+ rdfManifestsToEarl,
724
+ };