eyeling 1.24.31 → 1.25.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/eyeling.js CHANGED
@@ -1207,25 +1207,36 @@ function parseXsdDateTerm(t) {
1207
1207
  return d;
1208
1208
  }
1209
1209
 
1210
+ function isXsdDateTimeDatatype(dt) {
1211
+ return dt === XSD_NS + 'dateTime' || dt === XSD_NS + 'dateTimeStamp';
1212
+ }
1213
+
1210
1214
  function parseXsdDatetimeTerm(t) {
1211
1215
  if (!(t instanceof Literal)) return null;
1212
1216
  const [lex, dt] = literalParts(t.value);
1213
- if (dt !== XSD_NS + 'dateTime') return null;
1217
+ if (!isXsdDateTimeDatatype(dt)) return null;
1214
1218
  const val = stripQuotes(lex);
1219
+
1220
+ // xsd:dateTimeStamp is a subtype of xsd:dateTime with a required timezone.
1221
+ // Keep xsd:dateTime's existing permissive behaviour, but reject stamp
1222
+ // lexicals that do not actually carry the required timezone.
1223
+ if (dt === XSD_NS + 'dateTimeStamp' && !/(Z|[+-]\d{2}:\d{2})$/.test(val)) return null;
1224
+
1215
1225
  const d = new Date(val);
1216
1226
  if (Number.isNaN(d.getTime())) return null;
1217
1227
  return d; // Date in local/UTC, we only use timestamp
1218
1228
  }
1219
1229
 
1220
1230
  function parseXsdDateTimeLexParts(t) {
1221
- // Parse *lexical* components of an xsd:dateTime literal without timezone normalization.
1231
+ // Parse *lexical* components of an xsd:dateTime/dateTimeStamp literal without timezone normalization.
1222
1232
  // Returns { yearStr, month, day, hour, minute, second, tz } or null.
1223
1233
  if (!(t instanceof Literal)) return null;
1224
1234
  const [lex, dt] = literalParts(t.value);
1225
- if (dt !== XSD_NS + 'dateTime') return null;
1235
+ if (!isXsdDateTimeDatatype(dt)) return null;
1226
1236
  const val = stripQuotes(lex);
1227
1237
 
1228
1238
  // xsd:dateTime lexical: YYYY-MM-DDThh:mm:ss(.s+)?(Z|(+|-)hh:mm)?
1239
+ // xsd:dateTimeStamp has the same lexical form, but with the timezone required.
1229
1240
  const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
1230
1241
  if (!m) return null;
1231
1242
 
@@ -1236,6 +1247,7 @@ function parseXsdDateTimeLexParts(t) {
1236
1247
  const minute = parseInt(m[5], 10);
1237
1248
  const second = parseInt(m[6], 10);
1238
1249
  const tz = m[7] || null;
1250
+ if (dt === XSD_NS + 'dateTimeStamp' && !tz) return null;
1239
1251
 
1240
1252
  if (!(month >= 1 && month <= 12)) return null;
1241
1253
  if (!(day >= 1 && day <= 31)) return null;
@@ -1302,7 +1314,8 @@ function parseIso8601DurationToSeconds(s) {
1302
1314
  }
1303
1315
 
1304
1316
  function parseNumericForCompareTerm(t) {
1305
- // Strict: only accept xsd numeric literals, xsd:duration, xsd:date, xsd:dateTime
1317
+ // Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
1318
+ // xsd:dateTime, and xsd:dateTimeStamp.
1306
1319
  // (or untyped numeric tokens).
1307
1320
  const bi = parseIntLiteral(t);
1308
1321
  if (bi !== null) return { kind: 'bigint', value: bi };
@@ -1369,7 +1382,7 @@ function parseNumOrDuration(t) {
1369
1382
  }
1370
1383
  }
1371
1384
 
1372
- // xsd:date / xsd:dateTime
1385
+ // xsd:date / xsd:dateTime / xsd:dateTimeStamp
1373
1386
  const dtval = parseDatetimeLike(t);
1374
1387
  if (dtval !== null) {
1375
1388
  return dtval.getTime() / 1000.0;
@@ -5504,6 +5517,10 @@ const {
5504
5517
  copyQuotedGraphMetadata,
5505
5518
  } = require('./prelude');
5506
5519
 
5520
+ // Inference fuses use sysexits.h EX_DATAERR (65): input/rules made a
5521
+ // forbidden condition provable, rather than a generic usage/runtime error.
5522
+ const INFERENCE_FUSE_EXIT_CODE = 65;
5523
+
5507
5524
  // In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
5508
5525
  // Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
5509
5526
  const RDF_NIL_IRI = RDF_NS + 'nil';
@@ -8432,7 +8449,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8432
8449
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
8433
8450
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
8434
8451
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
8435
- __exitReasoning(2, 'Inference fuse triggered.');
8452
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
8436
8453
  }
8437
8454
 
8438
8455
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -8593,7 +8610,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
8593
8610
  // Inference fuse
8594
8611
  if (r.isFuse && sols.length) {
8595
8612
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
8596
- __exitReasoning(2, 'Inference fuse triggered.');
8613
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
8597
8614
  }
8598
8615
 
8599
8616
  for (const s of sols) {
@@ -9130,6 +9147,7 @@ module.exports = {
9130
9147
  registerBuiltinModule,
9131
9148
  loadBuiltinModule,
9132
9149
  listBuiltinIris,
9150
+ INFERENCE_FUSE_EXIT_CODE,
9133
9151
  };
9134
9152
 
9135
9153
  };
@@ -9157,6 +9175,7 @@ module.exports = {
9157
9175
  rdfjs: dataFactory,
9158
9176
  main: engine.main,
9159
9177
  version: engine.version,
9178
+ INFERENCE_FUSE_EXIT_CODE: engine.INFERENCE_FUSE_EXIT_CODE,
9160
9179
 
9161
9180
  // internals for playground.html
9162
9181
  lex: engine.lex,
package/index.d.ts CHANGED
@@ -209,6 +209,7 @@ declare module 'eyeling' {
209
209
  opts?: Omit<ReasonStreamOptions, 'rdfjs' | 'onDerived'>,
210
210
  ): AsyncIterable<RdfJsQuad>;
211
211
 
212
+ export const INFERENCE_FUSE_EXIT_CODE: 65;
212
213
  export const rdfjs: RdfJsDataFactory;
213
214
  export function registerBuiltin(iri: string, handler: BuiltinHandler): BuiltinHandler;
214
215
  export function unregisterBuiltin(iri: string): boolean;
@@ -237,6 +238,7 @@ declare module 'eyeling/browser' {
237
238
  opts?: Omit<ReasonStreamOptions, 'rdfjs' | 'onDerived'>,
238
239
  ): AsyncIterable<RdfJsQuad>;
239
240
 
241
+ export const INFERENCE_FUSE_EXIT_CODE: 65;
240
242
  export const rdfjs: RdfJsDataFactory;
241
243
  export function registerBuiltin(iri: string, handler: BuiltinHandler): BuiltinHandler;
242
244
  export function unregisterBuiltin(iri: string): boolean;
@@ -247,6 +249,7 @@ declare module 'eyeling/browser' {
247
249
  readonly version: string;
248
250
  reasonStream: typeof reasonStream;
249
251
  reasonRdfJs: typeof reasonRdfJs;
252
+ readonly INFERENCE_FUSE_EXIT_CODE: typeof INFERENCE_FUSE_EXIT_CODE;
250
253
  rdfjs: typeof rdfjs;
251
254
  registerBuiltin: typeof registerBuiltin;
252
255
  unregisterBuiltin: typeof unregisterBuiltin;
package/lib/builtins.js CHANGED
@@ -1196,25 +1196,36 @@ function parseXsdDateTerm(t) {
1196
1196
  return d;
1197
1197
  }
1198
1198
 
1199
+ function isXsdDateTimeDatatype(dt) {
1200
+ return dt === XSD_NS + 'dateTime' || dt === XSD_NS + 'dateTimeStamp';
1201
+ }
1202
+
1199
1203
  function parseXsdDatetimeTerm(t) {
1200
1204
  if (!(t instanceof Literal)) return null;
1201
1205
  const [lex, dt] = literalParts(t.value);
1202
- if (dt !== XSD_NS + 'dateTime') return null;
1206
+ if (!isXsdDateTimeDatatype(dt)) return null;
1203
1207
  const val = stripQuotes(lex);
1208
+
1209
+ // xsd:dateTimeStamp is a subtype of xsd:dateTime with a required timezone.
1210
+ // Keep xsd:dateTime's existing permissive behaviour, but reject stamp
1211
+ // lexicals that do not actually carry the required timezone.
1212
+ if (dt === XSD_NS + 'dateTimeStamp' && !/(Z|[+-]\d{2}:\d{2})$/.test(val)) return null;
1213
+
1204
1214
  const d = new Date(val);
1205
1215
  if (Number.isNaN(d.getTime())) return null;
1206
1216
  return d; // Date in local/UTC, we only use timestamp
1207
1217
  }
1208
1218
 
1209
1219
  function parseXsdDateTimeLexParts(t) {
1210
- // Parse *lexical* components of an xsd:dateTime literal without timezone normalization.
1220
+ // Parse *lexical* components of an xsd:dateTime/dateTimeStamp literal without timezone normalization.
1211
1221
  // Returns { yearStr, month, day, hour, minute, second, tz } or null.
1212
1222
  if (!(t instanceof Literal)) return null;
1213
1223
  const [lex, dt] = literalParts(t.value);
1214
- if (dt !== XSD_NS + 'dateTime') return null;
1224
+ if (!isXsdDateTimeDatatype(dt)) return null;
1215
1225
  const val = stripQuotes(lex);
1216
1226
 
1217
1227
  // xsd:dateTime lexical: YYYY-MM-DDThh:mm:ss(.s+)?(Z|(+|-)hh:mm)?
1228
+ // xsd:dateTimeStamp has the same lexical form, but with the timezone required.
1218
1229
  const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
1219
1230
  if (!m) return null;
1220
1231
 
@@ -1225,6 +1236,7 @@ function parseXsdDateTimeLexParts(t) {
1225
1236
  const minute = parseInt(m[5], 10);
1226
1237
  const second = parseInt(m[6], 10);
1227
1238
  const tz = m[7] || null;
1239
+ if (dt === XSD_NS + 'dateTimeStamp' && !tz) return null;
1228
1240
 
1229
1241
  if (!(month >= 1 && month <= 12)) return null;
1230
1242
  if (!(day >= 1 && day <= 31)) return null;
@@ -1291,7 +1303,8 @@ function parseIso8601DurationToSeconds(s) {
1291
1303
  }
1292
1304
 
1293
1305
  function parseNumericForCompareTerm(t) {
1294
- // Strict: only accept xsd numeric literals, xsd:duration, xsd:date, xsd:dateTime
1306
+ // Strict: only accept xsd numeric literals, xsd:duration, xsd:date,
1307
+ // xsd:dateTime, and xsd:dateTimeStamp.
1295
1308
  // (or untyped numeric tokens).
1296
1309
  const bi = parseIntLiteral(t);
1297
1310
  if (bi !== null) return { kind: 'bigint', value: bi };
@@ -1358,7 +1371,7 @@ function parseNumOrDuration(t) {
1358
1371
  }
1359
1372
  }
1360
1373
 
1361
- // xsd:date / xsd:dateTime
1374
+ // xsd:date / xsd:dateTime / xsd:dateTimeStamp
1362
1375
  const dtval = parseDatetimeLike(t);
1363
1376
  if (dtval !== null) {
1364
1377
  return dtval.getTime() / 1000.0;
package/lib/engine.js CHANGED
@@ -30,6 +30,10 @@ const {
30
30
  copyQuotedGraphMetadata,
31
31
  } = require('./prelude');
32
32
 
33
+ // Inference fuses use sysexits.h EX_DATAERR (65): input/rules made a
34
+ // forbidden condition provable, rather than a generic usage/runtime error.
35
+ const INFERENCE_FUSE_EXIT_CODE = 65;
36
+
33
37
  // In N3/Turtle, rdf:nil is the canonical IRI for the empty RDF list.
34
38
  // Eyeling represents list literals with ListTerm; ensure rdf:nil unifies with ().
35
39
  const RDF_NIL_IRI = RDF_NS + 'nil';
@@ -2958,7 +2962,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
2958
2962
  // Allow dynamic fuses: ... => ?X. where ?X becomes false
2959
2963
  if (dynTerm instanceof Literal && dynTerm.value === 'false') {
2960
2964
  __printTriggeredFuse(r, opts && opts.prefixes, s, 'Dynamic head resolved to false.');
2961
- __exitReasoning(2, 'Inference fuse triggered.');
2965
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
2962
2966
  }
2963
2967
 
2964
2968
  const dynTriples = __graphTriplesOrTrue(dynTerm);
@@ -3119,7 +3123,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */,
3119
3123
  // Inference fuse
3120
3124
  if (r.isFuse && sols.length) {
3121
3125
  __printTriggeredFuse(r, opts && opts.prefixes, sols[0]);
3122
- __exitReasoning(2, 'Inference fuse triggered.');
3126
+ __exitReasoning(INFERENCE_FUSE_EXIT_CODE, 'Inference fuse triggered.');
3123
3127
  }
3124
3128
 
3125
3129
  for (const s of sols) {
@@ -3656,4 +3660,5 @@ module.exports = {
3656
3660
  registerBuiltinModule,
3657
3661
  loadBuiltinModule,
3658
3662
  listBuiltinIris,
3663
+ INFERENCE_FUSE_EXIT_CODE,
3659
3664
  };
package/lib/entry.js CHANGED
@@ -21,6 +21,7 @@ module.exports = {
21
21
  rdfjs: dataFactory,
22
22
  main: engine.main,
23
23
  version: engine.version,
24
+ INFERENCE_FUSE_EXIT_CODE: engine.INFERENCE_FUSE_EXIT_CODE,
24
25
 
25
26
  // internals for playground.html
26
27
  lex: engine.lex,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.31",
3
+ "version": "1.25.0",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -323,6 +323,35 @@ const cases = [
323
323
  assert.match(out, /\\"2023-04-01T18:06:04Z\\"\^\^xsd:dateTime \./);
324
324
  },
325
325
  },
326
+ {
327
+ name: '00d math comparison accepts xsd:dateTimeStamp with xsd:dateTime',
328
+ opt: { proofComments: false },
329
+ input: `
330
+ @prefix : <http://example.org/> .
331
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
332
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
333
+
334
+ {
335
+ "2026-05-30T00:00:00Z"^^xsd:dateTimeStamp math:notGreaterThan "2027-02-12T00:00:00Z"^^xsd:dateTime .
336
+ }
337
+ =>
338
+ {
339
+ :mixedDateTimeStampCompare :is true .
340
+ }.
341
+
342
+ {
343
+ "2026-05-30T00:00:00Z"^^xsd:dateTimeStamp math:notGreaterThan "2026-05-30T00:00:00Z"^^xsd:dateTimeStamp .
344
+ }
345
+ =>
346
+ {
347
+ :sameDateTimeStampCompare :is true .
348
+ }.
349
+ `,
350
+ expect: [
351
+ /:mixedDateTimeStampCompare\s+:is\s+true\s*\./,
352
+ /:sameDateTimeStampCompare\s+:is\s+true\s*\./,
353
+ ],
354
+ },
326
355
  {
327
356
  name: '01 forward rule: p -> q',
328
357
  opt: { proofComments: false },
@@ -428,13 +457,13 @@ ${U('s')} ${U('p')} ${U('o')}.
428
457
  notExpect: [/^#/m],
429
458
  },
430
459
  {
431
- name: '11 negative entailment: rule derives false (expect exit 2 => throws)',
460
+ name: '11 negative entailment: rule derives false (expect exit 65 => throws)',
432
461
  opt: { proofComments: false },
433
462
  input: `
434
463
  { ${U('a')} ${U('p')} ${U('b')}. } => false.
435
464
  ${U('a')} ${U('p')} ${U('b')}.
436
465
  `,
437
- expectErrorCode: 2,
466
+ expectErrorCode: 65,
438
467
  },
439
468
  {
440
469
  name: '12 invalid syntax should throw (non-zero exit)',
@@ -958,10 +987,10 @@ ${transitiveClosureN3('sub')}
958
987
  ],
959
988
  },
960
989
  {
961
- name: '25 heavier negative entailment: batch + forbidden => false (expect exit 2)',
990
+ name: '25 heavier negative entailment: batch + forbidden => false (expect exit 65)',
962
991
  opt: { proofComments: false, maxBuffer: 200 * 1024 * 1024 },
963
992
  input: negativeEntailmentBatchN3(200),
964
- expectErrorCode: 2,
993
+ expectErrorCode: 65,
965
994
  },
966
995
  {
967
996
  name: '26 sanity: no rules => no newly derived facts',
@@ -1079,7 +1108,7 @@ ${U('a')} ${U('p')} ${U('b')}.
1079
1108
  { ${U('a')} ${U('p')} ${U('b')}. } => { ${U('a')} ${U('q')} ${U('b')}. }.
1080
1109
  { ${U('a')} ${U('q')} ${U('b')}. } => false.
1081
1110
  `,
1082
- expectErrorCode: 2,
1111
+ expectErrorCode: 65,
1083
1112
  },
1084
1113
 
1085
1114
  {
@@ -79,12 +79,12 @@ function normalizeMarkdownForCompare(text) {
79
79
 
80
80
  // Expectation logic (shared with test/examples.test.js):
81
81
  // 1) If file contains: # expect-exit: N -> use N
82
- // 2) Else, if it contains "=> false" -> expect exit 2
82
+ // 2) Else, if it contains "=> false" -> expect exit 65
83
83
  // 3) Else -> expect exit 0
84
84
  function expectedExitCode(n3Text) {
85
85
  const m = n3Text.match(/^[ \t]*#[: ]*expect-exit:[ \t]*([0-9]+)\b/m);
86
86
  if (m) return parseInt(m[1], 10);
87
- if (/=>\s*false\b/.test(n3Text)) return 2;
87
+ if (/=>\s*false\b/.test(n3Text)) return 65;
88
88
  return 0;
89
89
  }
90
90
 
package/tools/bundle.js CHANGED
@@ -229,6 +229,8 @@ function getBrowserApi() {
229
229
  return api;
230
230
  }
231
231
 
232
+ export const INFERENCE_FUSE_EXIT_CODE = 65;
233
+
232
234
  export function reasonStream(input, opts) {
233
235
  return getBrowserApi().reasonStream(input, opts);
234
236
  }
@@ -283,6 +285,7 @@ const eyeling = {
283
285
  get version() {
284
286
  return getBrowserApi().version;
285
287
  },
288
+ INFERENCE_FUSE_EXIT_CODE,
286
289
  reasonStream,
287
290
  reasonRdfJs,
288
291
  rdfjs,