eyeling 1.7.20 → 1.8.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 +1 -0
- package/eyeling.js +243 -91
- package/index.js +5 -1
- package/package.json +1 -1
- package/test/api.test.js +103 -2
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ Options:
|
|
|
122
122
|
-a, --ast Print parsed AST as JSON and exit.
|
|
123
123
|
--strings Print log:outputString strings (ordered by key) instead of N3 output.
|
|
124
124
|
--enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
|
|
125
|
+
--stream Stream derived triples as soon as they are derived.
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
By default, `eyeling`:
|
package/eyeling.js
CHANGED
|
@@ -111,9 +111,7 @@ let enforceHttpsEnabled = false;
|
|
|
111
111
|
|
|
112
112
|
function __maybeEnforceHttps(iri) {
|
|
113
113
|
if (!enforceHttpsEnabled) return iri;
|
|
114
|
-
return typeof iri === 'string' && iri.startsWith('http://')
|
|
115
|
-
? 'https://' + iri.slice('http://'.length)
|
|
116
|
-
: iri;
|
|
114
|
+
return typeof iri === 'string' && iri.startsWith('http://') ? 'https://' + iri.slice('http://'.length) : iri;
|
|
117
115
|
}
|
|
118
116
|
|
|
119
117
|
// Environment detection (Node vs Browser/Worker).
|
|
@@ -427,12 +425,7 @@ let __tracePrefixes = null;
|
|
|
427
425
|
function __traceWriteLine(line) {
|
|
428
426
|
// Prefer stderr in Node, fall back to console.error elsewhere.
|
|
429
427
|
try {
|
|
430
|
-
if (
|
|
431
|
-
__IS_NODE &&
|
|
432
|
-
typeof process !== 'undefined' &&
|
|
433
|
-
process.stderr &&
|
|
434
|
-
typeof process.stderr.write === 'function'
|
|
435
|
-
) {
|
|
428
|
+
if (__IS_NODE && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
|
|
436
429
|
process.stderr.write(String(line) + '\n');
|
|
437
430
|
return;
|
|
438
431
|
}
|
|
@@ -3109,7 +3102,6 @@ function termToJsString(t) {
|
|
|
3109
3102
|
return typeof lex === 'string' ? lex : String(lex);
|
|
3110
3103
|
}
|
|
3111
3104
|
|
|
3112
|
-
|
|
3113
3105
|
function makeStringLiteral(str) {
|
|
3114
3106
|
// JSON.stringify gives us a valid N3/Turtle-style quoted string
|
|
3115
3107
|
// (with proper escaping for quotes, backslashes, newlines, …).
|
|
@@ -3757,19 +3749,37 @@ function __tfnFormatYear(y) {
|
|
|
3757
3749
|
|
|
3758
3750
|
function __tfnAdd1ms(c) {
|
|
3759
3751
|
// Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
|
|
3760
|
-
if (c.millis < 999) {
|
|
3752
|
+
if (c.millis < 999) {
|
|
3753
|
+
c.millis++;
|
|
3754
|
+
return c;
|
|
3755
|
+
}
|
|
3761
3756
|
c.millis = 0;
|
|
3762
|
-
if (c.second < 59) {
|
|
3757
|
+
if (c.second < 59) {
|
|
3758
|
+
c.second++;
|
|
3759
|
+
return c;
|
|
3760
|
+
}
|
|
3763
3761
|
c.second = 0;
|
|
3764
|
-
if (c.minute < 59) {
|
|
3762
|
+
if (c.minute < 59) {
|
|
3763
|
+
c.minute++;
|
|
3764
|
+
return c;
|
|
3765
|
+
}
|
|
3765
3766
|
c.minute = 0;
|
|
3766
|
-
if (c.hour < 23) {
|
|
3767
|
+
if (c.hour < 23) {
|
|
3768
|
+
c.hour++;
|
|
3769
|
+
return c;
|
|
3770
|
+
}
|
|
3767
3771
|
c.hour = 0;
|
|
3768
3772
|
|
|
3769
3773
|
const dim = __tfnDaysInMonth(c.year, c.month);
|
|
3770
|
-
if (c.day < dim) {
|
|
3774
|
+
if (c.day < dim) {
|
|
3775
|
+
c.day++;
|
|
3776
|
+
return c;
|
|
3777
|
+
}
|
|
3771
3778
|
c.day = 1;
|
|
3772
|
-
if (c.month < 12) {
|
|
3779
|
+
if (c.month < 12) {
|
|
3780
|
+
c.month++;
|
|
3781
|
+
return c;
|
|
3782
|
+
}
|
|
3773
3783
|
c.month = 1;
|
|
3774
3784
|
c.year = c.year + 1n;
|
|
3775
3785
|
return c;
|
|
@@ -3777,16 +3787,31 @@ function __tfnAdd1ms(c) {
|
|
|
3777
3787
|
|
|
3778
3788
|
function __tfnSub1ms(c) {
|
|
3779
3789
|
// Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
|
|
3780
|
-
if (c.millis > 0) {
|
|
3790
|
+
if (c.millis > 0) {
|
|
3791
|
+
c.millis--;
|
|
3792
|
+
return c;
|
|
3793
|
+
}
|
|
3781
3794
|
c.millis = 999;
|
|
3782
|
-
if (c.second > 0) {
|
|
3795
|
+
if (c.second > 0) {
|
|
3796
|
+
c.second--;
|
|
3797
|
+
return c;
|
|
3798
|
+
}
|
|
3783
3799
|
c.second = 59;
|
|
3784
|
-
if (c.minute > 0) {
|
|
3800
|
+
if (c.minute > 0) {
|
|
3801
|
+
c.minute--;
|
|
3802
|
+
return c;
|
|
3803
|
+
}
|
|
3785
3804
|
c.minute = 59;
|
|
3786
|
-
if (c.hour > 0) {
|
|
3805
|
+
if (c.hour > 0) {
|
|
3806
|
+
c.hour--;
|
|
3807
|
+
return c;
|
|
3808
|
+
}
|
|
3787
3809
|
c.hour = 23;
|
|
3788
3810
|
|
|
3789
|
-
if (c.day > 1) {
|
|
3811
|
+
if (c.day > 1) {
|
|
3812
|
+
c.day--;
|
|
3813
|
+
return c;
|
|
3814
|
+
}
|
|
3790
3815
|
// move to previous month
|
|
3791
3816
|
if (c.month > 1) {
|
|
3792
3817
|
c.month--;
|
|
@@ -3824,7 +3849,15 @@ function __tfnComputePeriodBounds(parts) {
|
|
|
3824
3849
|
|
|
3825
3850
|
if (parts.kind === 'date') {
|
|
3826
3851
|
const startC = { year: parts.year, month: parts.month, day: parts.day, hour: 0, minute: 0, second: 0, millis: 0 };
|
|
3827
|
-
const endC = {
|
|
3852
|
+
const endC = {
|
|
3853
|
+
year: parts.year,
|
|
3854
|
+
month: parts.month,
|
|
3855
|
+
day: parts.day,
|
|
3856
|
+
hour: 23,
|
|
3857
|
+
minute: 59,
|
|
3858
|
+
second: 59,
|
|
3859
|
+
millis: 999,
|
|
3860
|
+
};
|
|
3828
3861
|
return { tzMin, tzMax, startC, endC };
|
|
3829
3862
|
}
|
|
3830
3863
|
|
|
@@ -3857,12 +3890,8 @@ function __tfnBindDefaultTimezone(timeLit, tzLit) {
|
|
|
3857
3890
|
if (__tfnHasTimezoneSuffix(v)) return timeLit;
|
|
3858
3891
|
|
|
3859
3892
|
// Only support the temporal types we parse.
|
|
3860
|
-
if (
|
|
3861
|
-
|
|
3862
|
-
dt !== XSD_NS + 'date' &&
|
|
3863
|
-
dt !== XSD_NS + 'gYearMonth' &&
|
|
3864
|
-
dt !== XSD_NS + 'gYear'
|
|
3865
|
-
) return null;
|
|
3893
|
+
if (dt !== XSD_NS + 'dateTime' && dt !== XSD_NS + 'date' && dt !== XSD_NS + 'gYearMonth' && dt !== XSD_NS + 'gYear')
|
|
3894
|
+
return null;
|
|
3866
3895
|
|
|
3867
3896
|
const outLex = `"${v}${tz}"^^<${dt}>`;
|
|
3868
3897
|
return internLiteral(outLex);
|
|
@@ -5163,72 +5192,73 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
5163
5192
|
return [];
|
|
5164
5193
|
}
|
|
5165
5194
|
|
|
5166
|
-
// -----------------------------------------------------------------
|
|
5167
|
-
// 4.3.1 tfn: Time Functions builtins
|
|
5168
|
-
// -----------------------------------------------------------------
|
|
5169
|
-
|
|
5170
|
-
// tfn:periodMinInclusive / periodMaxInclusive / periodMinExclusive / periodMaxExclusive
|
|
5171
|
-
// Schema: ( $s.1+ )+ tfn:* $o-
|
|
5172
|
-
const tfnPeriodKind =
|
|
5173
|
-
pv === TFN_NS + 'periodMinInclusive'
|
|
5174
|
-
? 'minInc'
|
|
5175
|
-
: pv === TFN_NS + 'periodMaxInclusive'
|
|
5176
|
-
? 'maxInc'
|
|
5177
|
-
: pv === TFN_NS + 'periodMinExclusive'
|
|
5178
|
-
? 'minEx'
|
|
5179
|
-
: pv === TFN_NS + 'periodMaxExclusive'
|
|
5180
|
-
? 'maxEx'
|
|
5181
|
-
: null;
|
|
5182
|
-
if (tfnPeriodKind) {
|
|
5183
|
-
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 1) return [];
|
|
5184
|
-
const arg = g.s.elems[0];
|
|
5185
|
-
const parts = __tfnParseTemporalLiteralParts(arg);
|
|
5186
|
-
if (!parts) return [];
|
|
5187
|
-
const bounds = __tfnComputePeriodBounds(parts);
|
|
5188
|
-
if (!bounds) return [];
|
|
5189
|
-
|
|
5190
|
-
let out;
|
|
5191
|
-
if (tfnPeriodKind === 'minInc') {
|
|
5192
|
-
out = __tfnMakeDateTimeLiteral({ ...bounds.startC }, bounds.tzMin);
|
|
5193
|
-
} else if (tfnPeriodKind === 'maxInc') {
|
|
5194
|
-
out = __tfnMakeDateTimeLiteral({ ...bounds.endC }, bounds.tzMax);
|
|
5195
|
-
} else if (tfnPeriodKind === 'minEx') {
|
|
5196
|
-
const c = __tfnSub1ms({ ...bounds.startC });
|
|
5197
|
-
out = __tfnMakeDateTimeLiteral(c, bounds.tzMin);
|
|
5198
|
-
} else { // maxEx
|
|
5199
|
-
const c = __tfnAdd1ms({ ...bounds.endC });
|
|
5200
|
-
out = __tfnMakeDateTimeLiteral(c, bounds.tzMax);
|
|
5201
|
-
}
|
|
5195
|
+
// -----------------------------------------------------------------
|
|
5196
|
+
// 4.3.1 tfn: Time Functions builtins
|
|
5197
|
+
// -----------------------------------------------------------------
|
|
5202
5198
|
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5199
|
+
// tfn:periodMinInclusive / periodMaxInclusive / periodMinExclusive / periodMaxExclusive
|
|
5200
|
+
// Schema: ( $s.1+ )+ tfn:* $o-
|
|
5201
|
+
const tfnPeriodKind =
|
|
5202
|
+
pv === TFN_NS + 'periodMinInclusive'
|
|
5203
|
+
? 'minInc'
|
|
5204
|
+
: pv === TFN_NS + 'periodMaxInclusive'
|
|
5205
|
+
? 'maxInc'
|
|
5206
|
+
: pv === TFN_NS + 'periodMinExclusive'
|
|
5207
|
+
? 'minEx'
|
|
5208
|
+
: pv === TFN_NS + 'periodMaxExclusive'
|
|
5209
|
+
? 'maxEx'
|
|
5210
|
+
: null;
|
|
5211
|
+
if (tfnPeriodKind) {
|
|
5212
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 1) return [];
|
|
5213
|
+
const arg = g.s.elems[0];
|
|
5214
|
+
const parts = __tfnParseTemporalLiteralParts(arg);
|
|
5215
|
+
if (!parts) return [];
|
|
5216
|
+
const bounds = __tfnComputePeriodBounds(parts);
|
|
5217
|
+
if (!bounds) return [];
|
|
5218
|
+
|
|
5219
|
+
let out;
|
|
5220
|
+
if (tfnPeriodKind === 'minInc') {
|
|
5221
|
+
out = __tfnMakeDateTimeLiteral({ ...bounds.startC }, bounds.tzMin);
|
|
5222
|
+
} else if (tfnPeriodKind === 'maxInc') {
|
|
5223
|
+
out = __tfnMakeDateTimeLiteral({ ...bounds.endC }, bounds.tzMax);
|
|
5224
|
+
} else if (tfnPeriodKind === 'minEx') {
|
|
5225
|
+
const c = __tfnSub1ms({ ...bounds.startC });
|
|
5226
|
+
out = __tfnMakeDateTimeLiteral(c, bounds.tzMin);
|
|
5227
|
+
} else {
|
|
5228
|
+
// maxEx
|
|
5229
|
+
const c = __tfnAdd1ms({ ...bounds.endC });
|
|
5230
|
+
out = __tfnMakeDateTimeLiteral(c, bounds.tzMax);
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
if (g.o instanceof Var) {
|
|
5234
|
+
const s2 = { ...subst };
|
|
5235
|
+
s2[g.o.name] = out;
|
|
5236
|
+
return [s2];
|
|
5237
|
+
}
|
|
5238
|
+
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5239
|
+
|
|
5240
|
+
const s2 = unifyTerm(g.o, out, subst);
|
|
5241
|
+
return s2 !== null ? [s2] : [];
|
|
5207
5242
|
}
|
|
5208
|
-
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5209
5243
|
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5244
|
+
// tfn:bindDefaultTimezone
|
|
5245
|
+
// Schema: ( $s.1+ $s.2+ )+ tfn:bindDefaultTimezone $o-
|
|
5246
|
+
if (pv === TFN_NS + 'bindDefaultTimezone') {
|
|
5247
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
5248
|
+
const [timeLit, tzLit] = g.s.elems;
|
|
5249
|
+
const out = __tfnBindDefaultTimezone(timeLit, tzLit);
|
|
5250
|
+
if (!out) return [];
|
|
5213
5251
|
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
if (!out) return [];
|
|
5252
|
+
if (g.o instanceof Var) {
|
|
5253
|
+
const s2 = { ...subst };
|
|
5254
|
+
s2[g.o.name] = out;
|
|
5255
|
+
return [s2];
|
|
5256
|
+
}
|
|
5257
|
+
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5221
5258
|
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
s2[g.o.name] = out;
|
|
5225
|
-
return [s2];
|
|
5259
|
+
const s2 = unifyTerm(g.o, out, subst);
|
|
5260
|
+
return s2 !== null ? [s2] : [];
|
|
5226
5261
|
}
|
|
5227
|
-
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5228
|
-
|
|
5229
|
-
const s2 = unifyTerm(g.o, out, subst);
|
|
5230
|
-
return s2 !== null ? [s2] : [];
|
|
5231
|
-
}
|
|
5232
5262
|
|
|
5233
5263
|
// -----------------------------------------------------------------
|
|
5234
5264
|
// 4.4 list: builtins
|
|
@@ -6879,7 +6909,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
6879
6909
|
if (allKnown) continue;
|
|
6880
6910
|
}
|
|
6881
6911
|
|
|
6882
|
-
const maxSols =
|
|
6912
|
+
const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
|
|
6883
6913
|
const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen, maxSols);
|
|
6884
6914
|
|
|
6885
6915
|
// Inference fuse
|
|
@@ -7413,7 +7443,8 @@ function main() {
|
|
|
7413
7443
|
` -s, --super-restricted Disable all builtins except => and <=.\n` +
|
|
7414
7444
|
` -a, --ast Print parsed AST as JSON and exit.\n` +
|
|
7415
7445
|
` --strings Print log:outputString strings (ordered by key) instead of N3 output.\n` +
|
|
7416
|
-
` --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n
|
|
7446
|
+
` --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
|
|
7447
|
+
` --stream Stream derived triples as soon as they are derived.\n`;
|
|
7417
7448
|
(toStderr ? console.error : console.log)(msg);
|
|
7418
7449
|
}
|
|
7419
7450
|
|
|
@@ -7435,6 +7466,7 @@ function main() {
|
|
|
7435
7466
|
const showAst = argv.includes('--ast') || argv.includes('-a');
|
|
7436
7467
|
|
|
7437
7468
|
const outputStringsMode = argv.includes('--strings');
|
|
7469
|
+
const streamMode = argv.includes('--stream');
|
|
7438
7470
|
|
|
7439
7471
|
// --enforce-https: rewrite http:// -> https:// for log dereferencing builtins
|
|
7440
7472
|
if (argv.includes('--enforce-https')) {
|
|
@@ -7517,13 +7549,133 @@ function main() {
|
|
|
7517
7549
|
materializeRdfLists(triples, frules, brules);
|
|
7518
7550
|
|
|
7519
7551
|
const facts = triples.filter((tr) => isGroundTriple(tr));
|
|
7520
|
-
|
|
7552
|
+
|
|
7521
7553
|
// If requested, print log:outputString values (ordered by subject key) and exit.
|
|
7554
|
+
// Note: log:outputString values may depend on derived facts, so we must saturate first.
|
|
7522
7555
|
if (outputStringsMode) {
|
|
7556
|
+
forwardChain(facts, frules, brules);
|
|
7523
7557
|
const out = __collectOutputStringsFromFacts(facts, prefixes);
|
|
7524
7558
|
if (out) process.stdout.write(out);
|
|
7525
7559
|
process.exit(0);
|
|
7526
7560
|
}
|
|
7561
|
+
|
|
7562
|
+
// In --stream mode we print prefixes *before* any derivations happen.
|
|
7563
|
+
// To keep the header small and stable, emit only prefixes that are actually
|
|
7564
|
+
// used (as QNames) in the *input* N3 program.
|
|
7565
|
+
function prefixesUsedInInputTokens(toks2, prefEnv) {
|
|
7566
|
+
const used = new Set();
|
|
7567
|
+
|
|
7568
|
+
function maybeAddFromQName(name) {
|
|
7569
|
+
if (typeof name !== 'string') return;
|
|
7570
|
+
if (!name.includes(':')) return;
|
|
7571
|
+
if (name.startsWith('_:')) return; // blank node
|
|
7572
|
+
|
|
7573
|
+
// Split only on the first ':'
|
|
7574
|
+
const idx = name.indexOf(':');
|
|
7575
|
+
const p = name.slice(0, idx); // may be '' for ":foo"
|
|
7576
|
+
|
|
7577
|
+
// Ignore things like "http://..." unless that prefix is actually defined.
|
|
7578
|
+
if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
|
|
7579
|
+
|
|
7580
|
+
used.add(p);
|
|
7581
|
+
}
|
|
7582
|
+
|
|
7583
|
+
for (let i = 0; i < toks2.length; i++) {
|
|
7584
|
+
const t = toks2[i];
|
|
7585
|
+
|
|
7586
|
+
// Skip @prefix ... .
|
|
7587
|
+
if (t.typ === 'AtPrefix') {
|
|
7588
|
+
// @prefix <pfx:> <iri> .
|
|
7589
|
+
// We skip the directive itself so declared-but-unused prefixes don't count.
|
|
7590
|
+
// Advance until we pass the terminating dot (if present).
|
|
7591
|
+
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
7592
|
+
continue;
|
|
7593
|
+
}
|
|
7594
|
+
// Skip @base ... .
|
|
7595
|
+
if (t.typ === 'AtBase') {
|
|
7596
|
+
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
7597
|
+
continue;
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
// Skip SPARQL/Turtle PREFIX pfx: <iri>
|
|
7601
|
+
if (
|
|
7602
|
+
t.typ === 'Ident' &&
|
|
7603
|
+
typeof t.value === 'string' &&
|
|
7604
|
+
t.value.toLowerCase() === 'prefix' &&
|
|
7605
|
+
toks2[i + 1] &&
|
|
7606
|
+
toks2[i + 1].typ === 'Ident' &&
|
|
7607
|
+
typeof toks2[i + 1].value === 'string' &&
|
|
7608
|
+
toks2[i + 1].value.endsWith(':') &&
|
|
7609
|
+
toks2[i + 2] &&
|
|
7610
|
+
(toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
|
|
7611
|
+
) {
|
|
7612
|
+
// Consume PREFIX <pfx:> <iri>
|
|
7613
|
+
i += 2;
|
|
7614
|
+
continue;
|
|
7615
|
+
}
|
|
7616
|
+
// Skip SPARQL BASE <iri>
|
|
7617
|
+
if (
|
|
7618
|
+
t.typ === 'Ident' &&
|
|
7619
|
+
typeof t.value === 'string' &&
|
|
7620
|
+
t.value.toLowerCase() === 'base' &&
|
|
7621
|
+
toks2[i + 1] &&
|
|
7622
|
+
toks2[i + 1].typ === 'IriRef'
|
|
7623
|
+
) {
|
|
7624
|
+
i += 1;
|
|
7625
|
+
continue;
|
|
7626
|
+
}
|
|
7627
|
+
|
|
7628
|
+
// Count QNames in identifiers (including datatypes like xsd:integer).
|
|
7629
|
+
if (t.typ === 'Ident') {
|
|
7630
|
+
maybeAddFromQName(t.value);
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
return used;
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
function restrictPrefixEnv(prefEnv, usedSet) {
|
|
7638
|
+
const m = {};
|
|
7639
|
+
for (const p of usedSet) {
|
|
7640
|
+
if (Object.prototype.hasOwnProperty.call(prefEnv.map, p)) {
|
|
7641
|
+
m[p] = prefEnv.map[p];
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
return new PrefixEnv(m, prefEnv.baseIri || '');
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7647
|
+
// Streaming mode: print (input) prefixes first, then print derived triples as soon as they are found.
|
|
7648
|
+
if (streamMode) {
|
|
7649
|
+
const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
|
|
7650
|
+
const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
|
|
7651
|
+
|
|
7652
|
+
// Ensure log:trace uses the same compact prefix set as the output.
|
|
7653
|
+
__tracePrefixes = outPrefixes;
|
|
7654
|
+
|
|
7655
|
+
const entries = Object.entries(outPrefixes.map)
|
|
7656
|
+
.filter(([_p, base]) => !!base)
|
|
7657
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
7658
|
+
|
|
7659
|
+
for (const [pfx, base] of entries) {
|
|
7660
|
+
if (pfx === '') console.log(`@prefix : <${base}> .`);
|
|
7661
|
+
else console.log(`@prefix ${pfx}: <${base}> .`);
|
|
7662
|
+
}
|
|
7663
|
+
if (entries.length) console.log();
|
|
7664
|
+
|
|
7665
|
+
forwardChain(facts, frules, brules, (df) => {
|
|
7666
|
+
if (proofCommentsEnabled) {
|
|
7667
|
+
printExplanation(df, outPrefixes);
|
|
7668
|
+
console.log(tripleToN3(df.fact, outPrefixes));
|
|
7669
|
+
console.log();
|
|
7670
|
+
} else {
|
|
7671
|
+
console.log(tripleToN3(df.fact, outPrefixes));
|
|
7672
|
+
}
|
|
7673
|
+
});
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
// Default (non-streaming): derive everything first, then print only the newly derived facts.
|
|
7678
|
+
const derived = forwardChain(facts, frules, brules);
|
|
7527
7679
|
const derivedTriples = derived.map((df) => df.fact);
|
|
7528
7680
|
const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
|
|
7529
7681
|
|
package/index.js
CHANGED
|
@@ -22,7 +22,11 @@ function reason(opt = {}, n3_input = '') {
|
|
|
22
22
|
const proofCommentsSpecified = typeof opt.proofComments === 'boolean' || typeof opt.noProofComments === 'boolean';
|
|
23
23
|
|
|
24
24
|
const proofComments =
|
|
25
|
-
typeof opt.proofComments === 'boolean'
|
|
25
|
+
typeof opt.proofComments === 'boolean'
|
|
26
|
+
? opt.proofComments
|
|
27
|
+
: typeof opt.noProofComments === 'boolean'
|
|
28
|
+
? !opt.noProofComments
|
|
29
|
+
: false;
|
|
26
30
|
|
|
27
31
|
// Only pass a flag when the caller explicitly asked.
|
|
28
32
|
// (CLI default is now: no proof comments.)
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
4
|
const { reason } = require('..');
|
|
5
|
+
// Direct eyeling.js API (in-process) for testing reasonStream/onDerived.
|
|
6
|
+
// This is the "latest eyeling.js" surface and is used by the browser demo.
|
|
7
|
+
const { reasonStream } = require('../eyeling.js');
|
|
5
8
|
|
|
6
9
|
const TTY = process.stdout.isTTY;
|
|
7
10
|
const C = TTY ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' } : { g: '', r: '', y: '', dim: '', n: '' };
|
|
@@ -687,6 +690,104 @@ _:l2 rdf:rest rdf:nil.
|
|
|
687
690
|
new RegExp(`${EX}s>\\s+<${EX}q3>\\s+<${EX}b>\\s*\\.`),
|
|
688
691
|
],
|
|
689
692
|
},
|
|
693
|
+
|
|
694
|
+
// -------------------------
|
|
695
|
+
// Newer eyeling.js features
|
|
696
|
+
// -------------------------
|
|
697
|
+
|
|
698
|
+
{
|
|
699
|
+
name: '51 --strings: prints log:outputString values ordered by key (subject)',
|
|
700
|
+
opt: ['--strings', '-n'],
|
|
701
|
+
input: `@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
702
|
+
|
|
703
|
+
<http://example.org/2> log:outputString "B".
|
|
704
|
+
<http://example.org/1> log:outputString "A".
|
|
705
|
+
`,
|
|
706
|
+
// CLI prints concatenated strings and exits.
|
|
707
|
+
check(out) {
|
|
708
|
+
assert.equal(String(out).trimEnd(), 'AB');
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
{
|
|
713
|
+
name: '52 --ast: prints parse result as JSON array [prefixes, triples, frules, brules]',
|
|
714
|
+
opt: ['--ast'],
|
|
715
|
+
input: `@prefix ex: <http://example.org/>.
|
|
716
|
+
ex:s ex:p ex:o.
|
|
717
|
+
`,
|
|
718
|
+
expect: [/^\s*\[/m],
|
|
719
|
+
check(out) {
|
|
720
|
+
const v = JSON.parse(String(out));
|
|
721
|
+
assert.ok(Array.isArray(v), 'AST output should be a JSON array');
|
|
722
|
+
assert.equal(v.length, 4, 'AST output should have 4 top-level elements');
|
|
723
|
+
// The second element is the parsed triples array.
|
|
724
|
+
assert.ok(Array.isArray(v[1]), 'AST[1] (triples) should be an array');
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
{
|
|
729
|
+
name: '53 --stream: prints prefixes used in input (not just derived output) before streaming triples',
|
|
730
|
+
opt: ['--stream', '-n'],
|
|
731
|
+
input: `@prefix ex: <http://example.org/>.
|
|
732
|
+
@prefix p: <http://premise.example/>.
|
|
733
|
+
@prefix unused: <http://unused.example/>.
|
|
734
|
+
|
|
735
|
+
ex:a p:trig ex:b.
|
|
736
|
+
{ ?s p:trig ?o. } => { ?s ex:q ?o. }.
|
|
737
|
+
`,
|
|
738
|
+
expect: [
|
|
739
|
+
/@prefix\s+ex:\s+<http:\/\/example\.org\/>\s*\./m,
|
|
740
|
+
/@prefix\s+p:\s+<http:\/\/premise\.example\/>\s*\./m,
|
|
741
|
+
/(?:ex:a|<http:\/\/example\.org\/a>)\s+(?:ex:q|<http:\/\/example\.org\/q>)\s+(?:ex:b|<http:\/\/example\.org\/b>)\s*\./m,
|
|
742
|
+
],
|
|
743
|
+
notExpect: [/@prefix\s+unused:/m, /^#/m],
|
|
744
|
+
check(out) {
|
|
745
|
+
const lines = String(out).split(/\r?\n/);
|
|
746
|
+
const firstNonPrefix = lines.findIndex((l) => {
|
|
747
|
+
const t = l.trim();
|
|
748
|
+
return t && !t.startsWith('@prefix');
|
|
749
|
+
});
|
|
750
|
+
assert.ok(firstNonPrefix > 0, 'Expected at least one @prefix line before the first triple');
|
|
751
|
+
for (let i = 0; i < firstNonPrefix; i++) {
|
|
752
|
+
const t = lines[i].trim();
|
|
753
|
+
if (!t) continue;
|
|
754
|
+
assert.ok(t.startsWith('@prefix'), `Non-prefix line found before first triple: ${lines[i]}`);
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
{
|
|
760
|
+
name: '54 reasonStream: onDerived callback fires and includeInputFactsInClosure=false excludes input facts',
|
|
761
|
+
run() {
|
|
762
|
+
const input = `
|
|
763
|
+
{ <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
|
|
764
|
+
=> { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
|
|
765
|
+
|
|
766
|
+
<http://example.org/s> <http://example.org/p> <http://example.org/o>.
|
|
767
|
+
`;
|
|
768
|
+
|
|
769
|
+
const seen = [];
|
|
770
|
+
const r = reasonStream(input, {
|
|
771
|
+
proof: false,
|
|
772
|
+
includeInputFactsInClosure: false,
|
|
773
|
+
onDerived: ({ triple }) => seen.push(triple),
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// stash for check()
|
|
777
|
+
this._seen = seen;
|
|
778
|
+
this._result = r;
|
|
779
|
+
return r.closureN3;
|
|
780
|
+
},
|
|
781
|
+
expect: [/http:\/\/example\.org\/q/m],
|
|
782
|
+
notExpect: [/http:\/\/example\.org\/p/m],
|
|
783
|
+
check(out, tc) {
|
|
784
|
+
assert.equal(tc._seen.length, 1, 'Expected onDerived to be called once');
|
|
785
|
+
assert.match(tc._seen[0], /http:\/\/example\.org\/q/, 'Expected streamed triple to be the derived one');
|
|
786
|
+
// closureN3 should be exactly the derived triple (no input facts).
|
|
787
|
+
assert.ok(String(out).trim().includes('http://example.org/q'));
|
|
788
|
+
assert.ok(!String(out).includes('http://example.org/p'));
|
|
789
|
+
},
|
|
790
|
+
},
|
|
690
791
|
];
|
|
691
792
|
|
|
692
793
|
let passed = 0;
|
|
@@ -699,7 +800,7 @@ let failed = 0;
|
|
|
699
800
|
for (const tc of cases) {
|
|
700
801
|
const start = msNow();
|
|
701
802
|
try {
|
|
702
|
-
const out = reason(tc.opt, tc.input);
|
|
803
|
+
const out = typeof tc.run === 'function' ? await tc.run() : reason(tc.opt, tc.input);
|
|
703
804
|
|
|
704
805
|
if (tc.expectErrorCode != null || tc.expectError) {
|
|
705
806
|
throw new Error(`Expected an error, but reason() returned output:\n${out}`);
|
|
@@ -708,7 +809,7 @@ let failed = 0;
|
|
|
708
809
|
for (const re of tc.expect || []) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
|
|
709
810
|
for (const re of tc.notExpect || []) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
|
|
710
811
|
|
|
711
|
-
if (typeof tc.check === 'function') tc.check(out);
|
|
812
|
+
if (typeof tc.check === 'function') tc.check(out, tc);
|
|
712
813
|
|
|
713
814
|
const dur = msNow() - start;
|
|
714
815
|
ok(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
|