eyeling 1.26.6 → 1.27.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.
Files changed (32) hide show
  1. package/HANDBOOK.md +100 -3
  2. package/dist/browser/eyeling.browser.js +396 -3
  3. package/examples/deck/act-barley-seed-lineage.md +1 -1
  4. package/examples/deck/faltings-genus2-finiteness.md +1 -1
  5. package/examples/deck/high-trust-rdf-bloom-envelope.md +1 -1
  6. package/examples/deck/odrl-dpv-risk-ranked.md +1 -1
  7. package/examples/deck/rdf-message-cold-chain-recall.md +71 -0
  8. package/examples/deck/rdf-message-flow.md +1 -1
  9. package/examples/deck/rdf-message-ldes-incremental.md +94 -0
  10. package/examples/deck/rdf-message-window-repair.md +175 -0
  11. package/examples/deck/schema-foaf-mapping.md +2 -0
  12. package/examples/input/rdf-message-cold-chain-recall.trig +668 -0
  13. package/examples/input/rdf-message-flow.trig +3 -0
  14. package/examples/input/rdf-message-ldes-incremental.trig +564 -0
  15. package/examples/input/rdf-message-microgrid.trig +3 -0
  16. package/examples/input/rdf-message-window-repair.trig +82 -0
  17. package/examples/input/rdf-messages.trig +3 -0
  18. package/examples/output/rdf-message-cold-chain-recall.md +13 -0
  19. package/examples/output/rdf-message-ldes-incremental.md +13 -0
  20. package/examples/output/rdf-message-window-repair.md +13 -0
  21. package/examples/rdf-message-cold-chain-recall.n3 +229 -0
  22. package/examples/rdf-message-flow.n3 +3 -0
  23. package/examples/rdf-message-ldes-incremental.n3 +231 -0
  24. package/examples/rdf-message-microgrid.n3 +3 -0
  25. package/examples/rdf-message-window-repair.n3 +166 -0
  26. package/examples/rdf-messages.n3 +3 -0
  27. package/eyeling.js +396 -3
  28. package/lib/cli.js +396 -3
  29. package/package.json +5 -3
  30. package/test/examples.test.js +25 -14
  31. package/test/fixtures/marc-rules-stream-messages.n3 +192 -0
  32. package/test/stream_messages.test.js +243 -0
package/HANDBOOK.md CHANGED
@@ -236,7 +236,15 @@ The lexer turns the input into tokens like:
236
236
 
237
237
  Parsing becomes dramatically simpler because tokenization already decided where strings end, where numbers are, and so on.
238
238
 
239
- By default, Eyeling parses ordinary N3. Selected RDF/TriG surface syntax is accepted only when RDF compatibility is explicitly enabled with `eyeling -r file.trig`, `eyeling --rdf file.trig`, or API option `{ rdf: true }`. In that mode, the lexer normalizes RDF/TriG input syntax to ordinary N3 graph terms before normal parsing, and the printer emits RDF/TriG-compatible output where feasible. Eyeling remains an N3 reasoner; this is syntax compatibility, not a separate RDF dataset reasoning model.
239
+ By default, Eyeling parses ordinary N3. Selected RDF/TriG surface syntax is accepted only when RDF compatibility is explicitly enabled with `eyeling -r file.trig`, `eyeling --rdf file.trig`, or API option `{ rdf: true }`.
240
+
241
+ The `-r` flag does **not** switch Eyeling to a different RDF dataset reasoner. It adds a parser/printer compatibility layer around the same N3 engine:
242
+
243
+ 1. before normal parsing, RDF/TriG surface syntax is normalized into ordinary Eyeling N3 terms;
244
+ 2. the usual N3 parser, rule compiler, forward chainer, backward prover, builtins, and output rendering then run on that normalized program;
245
+ 3. when possible, final output is printed back in RDF/TriG-compatible syntax.
246
+
247
+ This matters because rules still reason over Eyeling's N3 term model. Named graphs become quoted formulas, RDF 1.2 triple terms become singleton quoted formulas, and RDF Message Logs become an explicit replay graph that ordinary N3 rules can consume.
240
248
 
241
249
  In RDF compatibility mode, RDF 1.2 triple terms written as `<<( s p o )>>`, plus the reified triple form `<<s p o ~ r>>`, are normalized to Eyeling's existing singleton quoted-formula term `{ s p o }`. A reifier `r` is preserved as `r rdf:reifies { s p o }`. A leading `VERSION "1.2"` or `@version "1.2"` directive is ignored for the same reason. On output, `--rdf` converts a singleton graph term back to `<<( ... )>>` only when its inner triple is valid as an RDF triple term; otherwise it stays in N3 graph-term form. It also prints `log:nameOf` graph-term triples back as TriG named graph blocks. For example:
242
250
 
@@ -266,7 +274,94 @@ is treated internally like:
266
274
  } .
267
275
  ```
268
276
 
269
- This keeps Eyeling's N3 model stable while allowing small RDF 1.1/RDF 1.2 dataset-shaped inputs to run through the existing `GraphTerm` machinery when the caller opts in. More exotic future RDF forms should be added only if they can be mapped cleanly onto Eyeling's quoted-formula term model.
277
+ A top-level default graph block is unwrapped into ordinary top-level triples. A named graph block is not merged into the default graph; it remains a quoted formula reachable through `log:nameOf`.
278
+
279
+ #### RDF Message Log replay under `-r`
280
+
281
+ If RDF compatibility mode sees a top-level message version directive such as:
282
+
283
+ ```trig
284
+ VERSION "1.2-messages"
285
+ ```
286
+
287
+ or the corresponding `@version` spelling, Eyeling treats the input as an RDF Message Log before ordinary N3 parsing starts. The accepted message-version strings are `"1.1-messages"`, `"1.2-messages"`, and `"1.2-basic-messages"`.
288
+
289
+ At top-level statement boundaries, `MESSAGE` and `@message` delimiters split the document into message chunks. The text before the first delimiter is the first message. An empty chunk is still a message: it is replayed as an empty heartbeat rather than being dropped. Directives and comments alone do not make a message non-empty.
290
+
291
+ Each chunk is then normalized separately using the same RDF/TriG compatibility rules. Eyeling also scopes blank-node labels per message, so reusing `_:x` in two different messages does not accidentally identify the same blank node across message boundaries.
292
+
293
+ The result of replay is not a hidden side channel. Eyeling materializes ordinary facts using the `eymsg:` vocabulary:
294
+
295
+ ```n3
296
+ @prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.
297
+
298
+ ?stream a eymsg:RDFMessageStream;
299
+ eymsg:messageCount ?count;
300
+ eymsg:orderedEnvelopes ?envelopes;
301
+ eymsg:firstEnvelope ?first;
302
+ eymsg:lastEnvelope ?last;
303
+ eymsg:envelope ?envelope.
304
+
305
+ ?envelope a eymsg:MessageEnvelope;
306
+ eymsg:offset ?n;
307
+ eymsg:payloadKind eymsg:nonEmpty;
308
+ eymsg:payloadGraph ?payload;
309
+ eymsg:nextEnvelope ?next.
310
+ ```
311
+
312
+ For an empty message, the envelope has `eymsg:payloadKind eymsg:empty` and no `eymsg:payloadGraph`. For a non-empty message, the payload is exposed as a named graph:
313
+
314
+ ```n3
315
+ ?payload log:nameOf {
316
+ ...the normalized message body...
317
+ } .
318
+ ```
319
+
320
+ That design is deliberate: message payload triples are **not silently merged** into the global fact store. Rules inspect a payload explicitly, usually with `log:nameOf` plus `log:includes`:
321
+
322
+ ```n3
323
+ {
324
+ ?envelope eymsg:payloadGraph ?payload.
325
+ ?payload log:nameOf ?payloadGraph.
326
+ ?payloadGraph log:includes { :doorA :state :closed }.
327
+ } => {
328
+ ?envelope :reportsClosed :doorA.
329
+ }.
330
+ ```
331
+
332
+ This is the important mental model for `-r` with RDF Messages: the parser preserves message order and message boundaries as data. Reasoning can then implement stream-like behavior—flow stages, sliding windows, heartbeats, conflict repair, replay audits—without the TriG sidecar having to invent envelope facts by hand.
333
+
334
+ This keeps Eyeling's N3 model stable while allowing small RDF 1.1/RDF 1.2 dataset-shaped and message-log-shaped inputs to run through the existing `GraphTerm` machinery when the caller opts in. More exotic future RDF forms should be added only if they can be mapped cleanly onto Eyeling's quoted-formula term model.
335
+
336
+
337
+ #### Streaming large RDF Message Logs with `--stream-messages`
338
+
339
+ The ordinary `-r` replay described above is intentionally a faithful whole-log replay: it materializes one stream resource, the complete ordered-envelope list, all envelope links, and all payload graphs before reasoning. That is useful for examples, audits, proofs, and rules that need to see the whole log at once, but it is not the right execution model for very large append-only logs.
340
+
341
+ For large files where each RDF Message can be processed independently, use:
342
+
343
+ ```sh
344
+ eyeling -r --stream-messages rules.n3 large-message-log.trig
345
+ ```
346
+
347
+ `--stream-messages` keeps the rule files loaded once, then reads each RDF Message chunk from the log and runs the rules against a one-message replay view:
348
+
349
+ ```n3
350
+ ?stream a eymsg:RDFMessageStream;
351
+ eymsg:envelope ?envelope;
352
+ eymsg:firstEnvelope ?envelope;
353
+ eymsg:lastEnvelope ?envelope;
354
+ eymsg:orderedEnvelopes (?envelope).
355
+
356
+ ?envelope a eymsg:MessageEnvelope;
357
+ eymsg:offset ?n;
358
+ eymsg:payloadKind eymsg:nonEmpty;
359
+ eymsg:payloadGraph ?payload.
360
+ ```
361
+
362
+ The payload is still scoped behind `?payload log:nameOf { ... }`, so rules use the same `log:nameOf`/`log:includes` pattern as in whole-log replay. The important difference is lifetime: after one message has been parsed, reasoned over, and printed, its facts are discarded before the next message is read. Local files are scanned incrementally instead of first being normalized into one giant N3 document.
363
+
364
+ This mode is meant for production-style message feeds such as MARC-record streams, telemetry streams, or LDES member logs where a consumer checkpoint already tells the application which messages are new. It deliberately does not expose `eymsg:nextEnvelope` links or a complete `eymsg:messageCount`, because those require holding global stream state. Use ordinary `eyeling -r` when your rules need global ordering, sliding windows across several messages, or proof output for the entire replay.
270
365
 
271
366
  ### 4.2 Parsing triples, with Turtle-style convenience
272
367
 
@@ -2557,6 +2652,8 @@ Options:
2557
2652
  -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
2558
2653
  -h, --help Show this help and exit.
2559
2654
  -p, --proof Enable proof explanations.
2655
+ -r, --rdf Enable RDF/TriG input/output compatibility.
2656
+ --stream-messages Process RDF Message Logs one message at a time under -r.
2560
2657
  -s, --super-restricted Disable all builtins except => and <=.
2561
2658
  -t, --stream Stream derived triples as soon as they are derived.
2562
2659
  -v, --version Print version and exit.
@@ -2597,7 +2694,7 @@ Quoted graphs/formulas use `{ ... }`. Inside a quoted formula, directive scope m
2597
2694
 
2598
2695
  - `@prefix/@base` and `PREFIX/BASE` directives may appear at top level **or inside `{ ... }`**, and apply to the formula they occur in (formula-local scoping).
2599
2696
 
2600
- With `-r, --rdf` / `{ rdf: true }`, Eyeling also accepts RDF 1.2 triple-term surface forms such as `<<( s p o )>>` and `<<s p o ~ r>>` as compatibility spellings for a singleton quoted formula `{ s p o }`. In the same mode, feasible singleton graph terms are printed back as RDF 1.2 triple terms, while invalid cases such as a literal subject remain ordinary N3 graph terms. This is useful for inputs that use `rdf:reifies` or other predicates whose objects are RDF 1.2 triple terms, while keeping the default language and the rest of Eyeling on its N3 formula-term model.
2697
+ With `-r, --rdf` / `{ rdf: true }`, Eyeling accepts selected RDF/TriG surface syntax before normal N3 parsing. RDF 1.2 triple-term forms such as `<<( s p o )>>` and `<<s p o ~ r>>` are compatibility spellings for singleton quoted formulas such as `{ s p o }`; feasible singleton graph terms are printed back as RDF 1.2 triple terms. TriG named graph blocks become `log:nameOf` quoted formulas. If a `VERSION "1.2-messages"`-style directive is present, top-level `MESSAGE` delimiters are replayed as `eymsg:` stream/envelope facts and per-message payload graphs. For large independent message logs, `--stream-messages` runs the same payload-inspection style one message at a time. See [Chapter 4.1](#41-lexing-tokens-not-magic) for the full `-r` model and RDF Message handling.
2601
2698
 
2602
2699
  For the formal grammar, see the N3 spec grammar:
2603
2700
 
@@ -4622,12 +4622,15 @@ module.exports = {
4622
4622
 
4623
4623
  'use strict';
4624
4624
 
4625
+ const fs = require('node:fs');
4625
4626
  const path = require('node:path');
4626
- const { pathToFileURL } = require('node:url');
4627
+ const { pathToFileURL, fileURLToPath } = require('node:url');
4628
+ const { TextDecoder } = require('node:util');
4627
4629
 
4628
4630
  const engine = require('./engine');
4629
4631
  const deref = require('./deref');
4630
4632
  const { PrefixEnv } = require('./prelude');
4633
+ const { normalizeRdfCompatibility } = require('./lexer');
4631
4634
  const { parseN3Text, mergeParsedDocuments } = require('./multisource');
4632
4635
 
4633
4636
  function offsetToLineCol(text, offset) {
@@ -4666,7 +4669,6 @@ function formatN3SyntaxError(err, text, path) {
4666
4669
 
4667
4670
  // CLI entry point (invoked when eyeling.js is run directly)
4668
4671
  function readTextFromStdinSync() {
4669
- const fs = require('node:fs');
4670
4672
  return fs.readFileSync(0, { encoding: 'utf8' });
4671
4673
  }
4672
4674
 
@@ -4689,10 +4691,374 @@ function __readInputSourceSync(sourceLabel) {
4689
4691
  return txt;
4690
4692
  }
4691
4693
 
4692
- const fs = require('node:fs');
4693
4694
  return fs.readFileSync(sourceLabel, { encoding: 'utf8' });
4694
4695
  }
4695
4696
 
4697
+
4698
+ const RDF_MESSAGE_VERSION_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/im;
4699
+ const RDF_MESSAGE_VERSION_LINE_RE = /^\s*(?:@version|VERSION)\s+(["'])(?:1\.1|1\.2|1\.2-basic)-messages\1\s*\.?\s*(?:#.*)?$/i;
4700
+ const RDF_DIRECTIVE_LINE_RE = /^\s*(?:@?(?:prefix|base)\b|PREFIX\b|BASE\b)/i;
4701
+ const RDF_MESSAGE_DELIMITER_LINE_RE = /^\s*(?:MESSAGE\b|@message\s*\.?)\s*(?:#.*)?$/i;
4702
+ const LOG_NAME_OF_IRI = '<http://www.w3.org/2000/10/swap/log#nameOf>';
4703
+ const RDF_TYPE_IRI = '<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>';
4704
+ const XSD_INTEGER_IRI = '<http://www.w3.org/2001/XMLSchema#integer>';
4705
+ const EYMSG_NS = 'https://eyereasoner.github.io/eyeling/vocab/message#';
4706
+ const EYMSG_IRIS = Object.freeze({
4707
+ RDFMessageStream: `<${EYMSG_NS}RDFMessageStream>`,
4708
+ MessageEnvelope: `<${EYMSG_NS}MessageEnvelope>`,
4709
+ envelope: `<${EYMSG_NS}envelope>`,
4710
+ firstEnvelope: `<${EYMSG_NS}firstEnvelope>`,
4711
+ lastEnvelope: `<${EYMSG_NS}lastEnvelope>`,
4712
+ orderedEnvelopes: `<${EYMSG_NS}orderedEnvelopes>`,
4713
+ offset: `<${EYMSG_NS}offset>`,
4714
+ payloadGraph: `<${EYMSG_NS}payloadGraph>`,
4715
+ payloadKind: `<${EYMSG_NS}payloadKind>`,
4716
+ empty: `<${EYMSG_NS}empty>`,
4717
+ nonEmpty: `<${EYMSG_NS}nonEmpty>`,
4718
+ });
4719
+
4720
+ function simpleHashText(s) {
4721
+ let h = 0x811c9dc5;
4722
+ const text = String(s || '');
4723
+ for (let i = 0; i < text.length; i += 1) {
4724
+ h ^= text.charCodeAt(i);
4725
+ h = Math.imul(h, 0x01000193) >>> 0;
4726
+ }
4727
+ return h.toString(16).padStart(8, '0');
4728
+ }
4729
+
4730
+ function __isLocalPathSource(sourceLabel) {
4731
+ return typeof sourceLabel === 'string' && sourceLabel !== '<stdin>' && !/^(https?:|file:\/\/)/i.test(sourceLabel);
4732
+ }
4733
+
4734
+ function __localPathForSource(sourceLabel) {
4735
+ if (__isLocalPathSource(sourceLabel)) return sourceLabel;
4736
+ if (typeof sourceLabel === 'string' && /^file:\/\//i.test(sourceLabel)) return fileURLToPath(sourceLabel);
4737
+ return null;
4738
+ }
4739
+
4740
+ function __sourceLooksLikeRdfMessageLogSync(sourceLabel) {
4741
+ const filePath = __localPathForSource(sourceLabel);
4742
+ if (filePath) {
4743
+ const fd = fs.openSync(filePath, 'r');
4744
+ try {
4745
+ const buf = Buffer.allocUnsafe(64 * 1024);
4746
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
4747
+ return RDF_MESSAGE_VERSION_RE.test(buf.toString('utf8', 0, n));
4748
+ } finally {
4749
+ fs.closeSync(fd);
4750
+ }
4751
+ }
4752
+ return RDF_MESSAGE_VERSION_RE.test(__readInputSourceSync(sourceLabel));
4753
+ }
4754
+
4755
+ function stripRdfDirectiveLines(text) {
4756
+ return String(text || '')
4757
+ .split(/(?<=\r\n|\n|\r)/)
4758
+ .filter((line) => !RDF_DIRECTIVE_LINE_RE.test(line) && !RDF_MESSAGE_VERSION_LINE_RE.test(line))
4759
+ .join('');
4760
+ }
4761
+
4762
+ function hasRdfPayload(text) {
4763
+ return String(text || '')
4764
+ .split(/\r\n|\n|\r/)
4765
+ .some((line) => {
4766
+ const trimmed = line.replace(/#.*$/g, '').trim();
4767
+ return trimmed && !RDF_DIRECTIVE_LINE_RE.test(trimmed) && !RDF_MESSAGE_VERSION_LINE_RE.test(trimmed);
4768
+ });
4769
+ }
4770
+
4771
+ function addRdfDirective(directives, seen, line) {
4772
+ if (!RDF_DIRECTIVE_LINE_RE.test(line)) return;
4773
+ const key = line.trim();
4774
+ if (!key || seen.has(key)) return;
4775
+ seen.add(key);
4776
+ directives.push(line.endsWith('\n') || line.endsWith('\r') ? line : line + '\n');
4777
+ }
4778
+
4779
+ function normalizeStreamingPayloadChunk(chunk, directives) {
4780
+ const prelude = directives.join('');
4781
+ const normalized = normalizeRdfCompatibility(prelude + String(chunk || ''));
4782
+ return stripRdfDirectiveLines(normalized).trim();
4783
+ }
4784
+
4785
+ function buildSingleMessageReplayDocument({ sourceLabel, messageIndex, chunk, directives }) {
4786
+ const hash = simpleHashText(sourceLabel || '<stream>');
4787
+ const base = `urn:eyeling:message-stream:${hash}`;
4788
+ const padded = String(messageIndex).padStart(6, '0');
4789
+ const stream = `<${base}#stream>`;
4790
+ const envelope = `<${base}#m${padded}>`;
4791
+ const payload = `<${base}#m${padded}/payload>`;
4792
+ const body = normalizeStreamingPayloadChunk(chunk, directives);
4793
+ const hasBody = hasRdfPayload(body);
4794
+ const out = [];
4795
+
4796
+ out.push(...directives.map((line) => line.trim()).filter(Boolean));
4797
+ out.push(`${stream} ${RDF_TYPE_IRI} ${EYMSG_IRIS.RDFMessageStream} .`);
4798
+ out.push(`${stream} ${EYMSG_IRIS.envelope} ${envelope} .`);
4799
+ out.push(`${stream} ${EYMSG_IRIS.orderedEnvelopes} (${envelope}) .`);
4800
+ out.push(`${stream} ${EYMSG_IRIS.firstEnvelope} ${envelope} .`);
4801
+ out.push(`${stream} ${EYMSG_IRIS.lastEnvelope} ${envelope} .`);
4802
+ out.push(`${envelope} ${RDF_TYPE_IRI} ${EYMSG_IRIS.MessageEnvelope} .`);
4803
+ out.push(`${envelope} ${EYMSG_IRIS.offset} "${messageIndex}"^^${XSD_INTEGER_IRI} .`);
4804
+ out.push(`${envelope} ${EYMSG_IRIS.payloadKind} ${hasBody ? EYMSG_IRIS.nonEmpty : EYMSG_IRIS.empty} .`);
4805
+ if (hasBody) {
4806
+ out.push(`${envelope} ${EYMSG_IRIS.payloadGraph} ${payload} .`);
4807
+ out.push(`${payload} ${LOG_NAME_OF_IRI} {`);
4808
+ out.push(body);
4809
+ out.push(`} .`);
4810
+ }
4811
+ return out.join('\n') + '\n';
4812
+ }
4813
+
4814
+ function forEachRdfMessageChunkInText(text, onMessage) {
4815
+ const directives = [];
4816
+ const seenDirectives = new Set();
4817
+ let chunk = '';
4818
+ let messageIndex = 1;
4819
+ let sawVersion = false;
4820
+ let sawDelimiter = false;
4821
+
4822
+ function emit() {
4823
+ onMessage({ messageIndex, chunk, directives: directives.slice() });
4824
+ messageIndex += 1;
4825
+ chunk = '';
4826
+ }
4827
+
4828
+ const lines = String(text || '').match(/.*(?:\r\n|\n|\r)|.+$/g) || [];
4829
+ for (const line of lines) {
4830
+ if (RDF_MESSAGE_VERSION_LINE_RE.test(line)) {
4831
+ sawVersion = true;
4832
+ continue;
4833
+ }
4834
+ if (RDF_MESSAGE_DELIMITER_LINE_RE.test(line)) {
4835
+ emit();
4836
+ sawDelimiter = true;
4837
+ continue;
4838
+ }
4839
+ addRdfDirective(directives, seenDirectives, line);
4840
+ chunk += line;
4841
+ }
4842
+ if (!sawVersion) throw new Error('not an RDF Message Log: missing VERSION "*-messages" directive');
4843
+ if (sawDelimiter || hasRdfPayload(chunk)) emit();
4844
+ }
4845
+
4846
+ function forEachLineInFileSync(filePath, onLine) {
4847
+ const fd = fs.openSync(filePath, 'r');
4848
+ const decoder = new TextDecoder('utf8');
4849
+ const buf = Buffer.allocUnsafe(64 * 1024);
4850
+ let carry = '';
4851
+ try {
4852
+ for (;;) {
4853
+ const n = fs.readSync(fd, buf, 0, buf.length, null);
4854
+ if (n === 0) break;
4855
+ carry += decoder.decode(buf.subarray(0, n), { stream: true });
4856
+ for (;;) {
4857
+ const m = /\r\n|\n|\r/.exec(carry);
4858
+ if (!m) break;
4859
+ const end = m.index + m[0].length;
4860
+ onLine(carry.slice(0, end));
4861
+ carry = carry.slice(end);
4862
+ }
4863
+ }
4864
+ carry += decoder.decode();
4865
+ if (carry) onLine(carry);
4866
+ } finally {
4867
+ fs.closeSync(fd);
4868
+ }
4869
+ }
4870
+
4871
+ function forEachRdfMessageChunkInFileSync(filePath, onMessage) {
4872
+ const directives = [];
4873
+ const seenDirectives = new Set();
4874
+ let chunk = '';
4875
+ let messageIndex = 1;
4876
+ let sawVersion = false;
4877
+ let sawDelimiter = false;
4878
+
4879
+ function emit() {
4880
+ onMessage({ messageIndex, chunk, directives: directives.slice() });
4881
+ messageIndex += 1;
4882
+ chunk = '';
4883
+ }
4884
+
4885
+ forEachLineInFileSync(filePath, (line) => {
4886
+ if (RDF_MESSAGE_VERSION_LINE_RE.test(line)) {
4887
+ sawVersion = true;
4888
+ return;
4889
+ }
4890
+ if (RDF_MESSAGE_DELIMITER_LINE_RE.test(line)) {
4891
+ emit();
4892
+ sawDelimiter = true;
4893
+ return;
4894
+ }
4895
+ addRdfDirective(directives, seenDirectives, line);
4896
+ chunk += line;
4897
+ });
4898
+
4899
+ if (!sawVersion) throw new Error('not an RDF Message Log: missing VERSION "*-messages" directive');
4900
+ if (sawDelimiter || hasRdfPayload(chunk)) emit();
4901
+ }
4902
+
4903
+
4904
+ function __forEachRdfMessageChunkSync(sourceLabel, onMessage) {
4905
+ const filePath = __localPathForSource(sourceLabel);
4906
+ if (filePath) {
4907
+ forEachRdfMessageChunkInFileSync(filePath, onMessage);
4908
+ return;
4909
+ }
4910
+ forEachRdfMessageChunkInText(__readInputSourceSync(sourceLabel), onMessage);
4911
+ }
4912
+
4913
+ function factsContainOutputStrings(triplesForOutput) {
4914
+ const LOG_OUTPUT_STRING = 'http://www.w3.org/2000/10/swap/log#outputString';
4915
+ return (
4916
+ Array.isArray(triplesForOutput) &&
4917
+ triplesForOutput.some(
4918
+ (tr) => tr && tr.p && tr.p.constructor && tr.p.constructor.name === 'Iri' && tr.p.value === LOG_OUTPUT_STRING,
4919
+ )
4920
+ );
4921
+ }
4922
+
4923
+ function programMayProduceOutputStrings(topLevelTriples, forwardRules, logQueryRules) {
4924
+ const hasOutputStringPredicate = (trs) => factsContainOutputStrings(trs);
4925
+ if (hasOutputStringPredicate(topLevelTriples)) return true;
4926
+ if (Array.isArray(forwardRules) && forwardRules.some((r) => hasOutputStringPredicate(r && r.conclusion))) return true;
4927
+ if (Array.isArray(logQueryRules) && logQueryRules.some((r) => hasOutputStringPredicate(r && r.conclusion))) return true;
4928
+ return false;
4929
+ }
4930
+
4931
+ function runParsedDocumentOnce(mergedDocument, { rdfMode = false, outputPrefixes = null } = {}) {
4932
+ const prefixes = mergedDocument.prefixes;
4933
+ const triples = mergedDocument.triples;
4934
+ const frules = mergedDocument.frules;
4935
+ const brules = mergedDocument.brules;
4936
+ const qrules = mergedDocument.logQueryRules;
4937
+
4938
+ engine.materializeRdfLists(triples, frules.concat(qrules || []), brules);
4939
+ const facts = triples.slice();
4940
+ const hasQueries = Array.isArray(qrules) && qrules.length;
4941
+ const mayAutoRenderOutputStrings = programMayProduceOutputStrings(triples, frules, qrules);
4942
+
4943
+ let derived = [];
4944
+ let outTriples = [];
4945
+ if (hasQueries) {
4946
+ const res = engine.forwardChainAndCollectLogQueryConclusions(facts, frules, brules, qrules, { prefixes });
4947
+ derived = res.derived;
4948
+ outTriples = res.queryTriples;
4949
+ } else {
4950
+ const skipDerivedCollection = mayAutoRenderOutputStrings;
4951
+ derived = engine.forwardChain(facts, frules, brules, null, {
4952
+ captureExplanations: false,
4953
+ collectDerived: !skipDerivedCollection,
4954
+ prefixes,
4955
+ });
4956
+ outTriples = skipDerivedCollection ? [] : derived.map((df) => df.fact);
4957
+ }
4958
+
4959
+ const renderedOutputTriples = hasQueries ? outTriples : facts;
4960
+ if (factsContainOutputStrings(renderedOutputTriples)) {
4961
+ process.stdout.write(engine.collectOutputStringsFromFacts(renderedOutputTriples, prefixes));
4962
+ return;
4963
+ }
4964
+
4965
+ const outPrefixEnv = outputPrefixes || prefixes;
4966
+ if (rdfMode) {
4967
+ for (const tr of outTriples) console.log(engine.tripleToRdfCompatible(tr, outPrefixEnv));
4968
+ } else if (hasQueries) {
4969
+ const bodyText = engine.prettyPrintQueryTriples(outTriples, outPrefixEnv);
4970
+ if (bodyText) process.stdout.write(String(bodyText).replace(/\s*$/g, '') + '\n');
4971
+ } else {
4972
+ for (const df of derived) console.log(engine.tripleToN3(df.fact, outPrefixEnv));
4973
+ }
4974
+ }
4975
+
4976
+ function runStreamMessagesMode(sourceLabels, { rdfMode }) {
4977
+ const ordinarySourceLabels = [];
4978
+ const messageSourceLabels = [];
4979
+
4980
+ for (const sourceLabel of sourceLabels) {
4981
+ try {
4982
+ if (__sourceLooksLikeRdfMessageLogSync(sourceLabel)) messageSourceLabels.push(sourceLabel);
4983
+ else ordinarySourceLabels.push(sourceLabel);
4984
+ } catch (e) {
4985
+ console.error(`Error reading source ${JSON.stringify(sourceLabel)}: ${e && e.message ? e.message : String(e)}`);
4986
+ process.exit(1);
4987
+ }
4988
+ }
4989
+
4990
+ if (!messageSourceLabels.length) {
4991
+ console.error('Error: --stream-messages did not find any RDF Message Log input.');
4992
+ process.exit(1);
4993
+ }
4994
+
4995
+ const programSources = [];
4996
+ for (const sourceLabel of ordinarySourceLabels) {
4997
+ let text;
4998
+ try {
4999
+ text = __readInputSourceSync(sourceLabel);
5000
+ } catch (e) {
5001
+ if (sourceLabel === '<stdin>') console.error(`Error reading stdin: ${e.message}`);
5002
+ else console.error(`Error reading source ${JSON.stringify(sourceLabel)}: ${e.message}`);
5003
+ process.exit(1);
5004
+ }
5005
+
5006
+ try {
5007
+ programSources.push(
5008
+ parseN3Text(text, {
5009
+ baseIri: __sourceLabelToBaseIri(sourceLabel),
5010
+ label: sourceLabel,
5011
+ keepSourceArtifacts: false,
5012
+ sourceLocations: false,
5013
+ rdf: rdfMode,
5014
+ }),
5015
+ );
5016
+ } catch (e) {
5017
+ if (e && e.name === 'N3SyntaxError') {
5018
+ console.error(formatN3SyntaxError(e, text, sourceLabel));
5019
+ process.exit(1);
5020
+ }
5021
+ throw e;
5022
+ }
5023
+ }
5024
+
5025
+ const fullIriPrefixes = new PrefixEnv({});
5026
+ for (const messageSourceLabel of messageSourceLabels) {
5027
+ try {
5028
+ __forEachRdfMessageChunkSync(messageSourceLabel, ({ messageIndex, chunk, directives }) => {
5029
+ const messageText = buildSingleMessageReplayDocument({
5030
+ sourceLabel: messageSourceLabel,
5031
+ messageIndex,
5032
+ chunk,
5033
+ directives,
5034
+ });
5035
+ let messageDoc;
5036
+ try {
5037
+ messageDoc = parseN3Text(messageText, {
5038
+ baseIri: `${__sourceLabelToBaseIri(messageSourceLabel)}#message-${messageIndex}`,
5039
+ label: `${messageSourceLabel}#message-${messageIndex}`,
5040
+ keepSourceArtifacts: false,
5041
+ sourceLocations: false,
5042
+ rdf: false,
5043
+ });
5044
+ } catch (e) {
5045
+ if (e && e.name === 'N3SyntaxError') {
5046
+ console.error(formatN3SyntaxError(e, messageText, `${messageSourceLabel}#message-${messageIndex}`));
5047
+ process.exit(1);
5048
+ }
5049
+ throw e;
5050
+ }
5051
+
5052
+ const merged = mergeParsedDocuments(programSources.concat([messageDoc]));
5053
+ runParsedDocumentOnce(merged, { rdfMode, outputPrefixes: fullIriPrefixes });
5054
+ });
5055
+ } catch (e) {
5056
+ console.error(`Error streaming RDF Message Log ${JSON.stringify(messageSourceLabel)}: ${e && e.message ? e.message : String(e)}`);
5057
+ process.exit(1);
5058
+ }
5059
+ }
5060
+ }
5061
+
4696
5062
  function main() {
4697
5063
  // Drop "node" and script name; keep only user-provided args
4698
5064
  // Expand combined short options: -pt == -p -t
@@ -4723,6 +5089,7 @@ function main() {
4723
5089
  ` -h, --help Show this help and exit.\n` +
4724
5090
  ` -p, --proof Enable proof explanations.\n` +
4725
5091
  ` -r, --rdf Enable RDF/TriG input/output compatibility.\n` +
5092
+ ` --stream-messages Process RDF Message Logs one message at a time under -r.\n` +
4726
5093
  ` -s, --super-restricted Disable all builtins except => and <=.\n` +
4727
5094
  ` -t, --stream Stream derived triples as soon as they are derived.\n` +
4728
5095
  ` -v, --version Print version and exit.\n`;
@@ -4764,6 +5131,7 @@ function main() {
4764
5131
 
4765
5132
  const showAst = argv.includes('--ast') || argv.includes('-a');
4766
5133
  const streamMode = argv.includes('--stream') || argv.includes('-t');
5134
+ const streamMessagesMode = argv.includes('--stream-messages');
4767
5135
  const rdfMode = argv.includes('--rdf') || argv.includes('-r');
4768
5136
 
4769
5137
  // --enforce-https: rewrite http:// -> https:// for log dereferencing builtins
@@ -4786,6 +5154,26 @@ function main() {
4786
5154
  if (typeof engine.setSuperRestrictedMode === 'function') engine.setSuperRestrictedMode(true);
4787
5155
  }
4788
5156
 
5157
+
5158
+ if (streamMessagesMode) {
5159
+ if (!rdfMode) {
5160
+ console.error('Error: --stream-messages requires -r/--rdf.');
5161
+ process.exit(1);
5162
+ }
5163
+ if (showAst) {
5164
+ console.error('Error: --stream-messages cannot be combined with --ast.');
5165
+ process.exit(1);
5166
+ }
5167
+ if (streamMode) {
5168
+ console.error('Error: --stream-messages cannot be combined with --stream.');
5169
+ process.exit(1);
5170
+ }
5171
+ if (engine.getProofCommentsEnabled()) {
5172
+ console.error('Error: --stream-messages currently does not support proof output.');
5173
+ process.exit(1);
5174
+ }
5175
+ }
5176
+
4789
5177
  // Positional args (one or more N3 sources).
4790
5178
  const useImplicitStdin = positional.length === 0 && !process.stdin.isTTY;
4791
5179
  if (positional.length === 0 && !useImplicitStdin) {
@@ -4809,6 +5197,11 @@ function main() {
4809
5197
  process.exit(1);
4810
5198
  }
4811
5199
 
5200
+ if (streamMessagesMode) {
5201
+ runStreamMessagesMode(sourceLabels, { rdfMode });
5202
+ return;
5203
+ }
5204
+
4812
5205
  const parsedSources = [];
4813
5206
  for (const sourceLabel of sourceLabels) {
4814
5207
  let text;
@@ -1,6 +1,6 @@
1
1
  # ACT barley seed lineage — can and can't
2
2
 
3
- This deck explains the example `act-barley-seed-lineage-can-and-cant.n3`.
3
+ [Start with the source](../act-barley-seed-lineage.n3) to see the ACT model itself; the [golden output](../output/act-barley-seed-lineage.md) shows the lineage conclusions that Eyeling derives from it.
4
4
 
5
5
  The aim is to show how **Applied Constructor Theory** can describe a concrete biological case in Notation3 while doing both sides of Chiara Marletto’s formula:
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Faltings’ theorem (emulated) in Notation3: a genus‑2 curve over Q
2
2
 
3
- This deck explains the example `faltings-genus2-finiteness.n3` ([Playground][1]).
3
+ [Start with the source](../faltings-genus2-finiteness.n3) for the emulated theorem statement and inference rules; the [golden output](../output/faltings-genus2-finiteness.n3) shows the finite-rational-points conclusion produced by the example.
4
4
 
5
5
  The goal is to show—at a friendly, “wide audience” level—how an N3 file can _model_ a famous mathematical implication:
6
6
 
@@ -1,6 +1,6 @@
1
1
  # High-trust RDF graph lookup with a decimal certificate
2
2
 
3
- This deck explains the example `high-trust-rdf-bloom-envelope.n3` ([Playground][1]).
3
+ [Start with the source](../high-trust-rdf-bloom-envelope.n3) for the Bloom-filter certificate and arithmetic checks; the [golden output](../output/high-trust-rdf-bloom-envelope.n3) shows the trust decision that Eyeling derives.
4
4
 
5
5
  The goal is to show that **advanced engineering claims can be expressed and checked in N3**, even when the claim involves a **transcendental quantity** such as `exp(-k*n/m)`.
6
6
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Ranked, explainable output from machine-readable “Terms of Service”
4
4
 
5
- This deck explains how an agreement is modeled in **ODRL**, how risks are expressed in **DPV**, and how **N3 rules** connect the two into a ranked report. ([Playground][1])
5
+ [Start with the source](../odrl-dpv-risk-ranked.n3) to inspect the machine-readable policy and risk rules; the [golden output](../output/odrl-dpv-risk-ranked.md) shows the ranked explanation produced from them.
6
6
 
7
7
  ---
8
8