eyeling 1.33.11 → 1.34.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 CHANGED
@@ -824,7 +824,7 @@ The eyelang engine has its own built-in registry under `lib/eyelang/builtins/`.
824
824
  |---|---|
825
825
  | Core and host | `eq/2`, `neq/2`, `local_time/1`, `difference/3` |
826
826
  | Arithmetic and comparison | `neg/2`, `abs/2`, `sin/2`, `cos/2`, `asin/2`, `acos/2`, `rounded/2`, `log/2`, `add/3`, `sub/3`, `mul/3`, `div/3`, `mod/3`, `min/3`, `pow/3`, `lt/2`, `gt/2`, `le/2`, `ge/2`, `between/3`, `smallest_divisor_from/3` |
827
- | Strings | `str_concat/3`, `contains/2`, `matches/2`, `not_matches/2` |
827
+ | Strings | `str_concat/3`, `contains/2`, `matches/2`, `matches/3`, `not_matches/2` |
828
828
  | Lists | `append/3`, `nth0/3`, `set_nth0/4`, `rest/2`, `member/2`, `select/3`, `not_member/2`, `reverse/2`, `length/2`, `sort/2` |
829
829
  | Aggregation | `findall/3`, `countall/2`, `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
830
830
  | Control | `not/1`, `once/1` |
@@ -298,6 +298,8 @@ holds((ready, name(alice, "Alice"), route(alice, bob, 7)), Name, Args).
298
298
 
299
299
  Use `holds/2` when you want to match the member term directly, for example `name(S, O)`, `route(A, B, Cost)`, or `edge(A, arc(B, Cost))`. Use `holds/3` when you need the predicate name and argument list as data: it exposes any-arity member as atom constant `Name` plus a proper list `Args`, so zero-, binary-, and ternary members appear as `ready/0`, `name/2`, and `route/3` shapes without a special binary predicate. These utilities are useful for quoted context data, but they do not make those context members true in the ambient program. The [`context-schema-audit.pl`](../examples/eyelang/context-schema-audit.pl) example shows a case that really needs `holds/3`: it audits heterogeneous message contexts by extracting every member as `Name + Args`, computing each arity, and checking the resulting shape against a schema without knowing the predicate names in advance.
300
300
 
301
+ `matches/3` can create context data from named regular-expression captures, which is useful when text logs or messages need to become facts before later rules inspect them with `holds/2` or `holds/3`. See [`observability-log-correlation.pl`](../examples/eyelang/observability-log-correlation.pl) for a complete log-correlation example.
302
+
301
303
 
302
304
  ## Example catalog
303
305
 
@@ -409,6 +411,7 @@ The repository includes examples for recursion, graph reachability, finite searc
409
411
  | [`network-sla.pl`](../examples/eyelang/network-sla.pl) | Checks network path SLA compliance. | [`output/network-sla.pl`](../examples/eyelang/output/network-sla.pl) |
410
412
  | [`newton-raphson.pl`](../examples/eyelang/newton-raphson.pl) | Finds roots by Newton-Raphson iteration. | [`output/newton-raphson.pl`](../examples/eyelang/output/newton-raphson.pl) |
411
413
  | [`nixon-diamond.pl`](../examples/eyelang/nixon-diamond.pl) | Reports the classic Nixon-diamond conflict. | [`output/nixon-diamond.pl`](../examples/eyelang/output/nixon-diamond.pl) |
414
+ | [`observability-log-correlation.pl`](../examples/eyelang/observability-log-correlation.pl) | Extracts named regex captures from observability logs and correlates events by trace id. | [`output/observability-log-correlation.pl`](../examples/eyelang/output/observability-log-correlation.pl) |
412
415
  | [`odrl-dpv-fpv-trust-flow.pl`](../examples/eyelang/odrl-dpv-fpv-trust-flow.pl) | Decides ODRL/DPV data flows with local FPV trust gates. | [`output/odrl-dpv-fpv-trust-flow.pl`](../examples/eyelang/output/odrl-dpv-fpv-trust-flow.pl) |
413
416
  | [`odrl-dpv-healthcare-risk-ranked.pl`](../examples/eyelang/odrl-dpv-healthcare-risk-ranked.pl) | Ranks healthcare policy risks and mitigations. | [`output/odrl-dpv-healthcare-risk-ranked.pl`](../examples/eyelang/output/odrl-dpv-healthcare-risk-ranked.pl) |
414
417
  | [`odrl-dpv-risk-ranked.pl`](../examples/eyelang/odrl-dpv-risk-ranked.pl) | Ranks data-policy risks and mitigations. | [`output/odrl-dpv-risk-ranked.pl`](../examples/eyelang/output/odrl-dpv-risk-ranked.pl) |
@@ -402,6 +402,7 @@ Comparisons interpret numeric-looking terms numerically. Other scalar terms are
402
402
  | `str_concat(A, B, C)` | String concatenation. |
403
403
  | `contains(Text, Needle)` | Text contains `Needle`. |
404
404
  | `matches(Text, Pattern)` | Text matches a simple implementation regex/search pattern. |
405
+ | `matches(Text, Pattern, Context)` | Text matches a JavaScript regular expression with named capture groups; `Context` is a comma context containing one unary term per matched capture group. |
405
406
  | `not_matches(Text, Pattern)` | Negation of `matches/2`. |
406
407
 
407
408
  ### 9.7 Lists
@@ -0,0 +1,34 @@
1
+ % Parse unstructured service logs with named regex captures, then reason over
2
+ % the extracted context to correlate events that share a trace id.
3
+ materialize(parsed_event, 5).
4
+ materialize(captured_field, 3).
5
+ materialize(trace_alert, 3).
6
+
7
+ log_pattern("^ts=(?<ts>\\S+) level=(?<level>\\w+) event=(?<event>\\w+) user=(?<user>\\w+) ip=(?<ip>\\S+) traceparent=00-(?<trace_id>[0-9a-f]{32})-(?<span_id>[0-9a-f]{16})-(?<flags>[0-9a-f]{2})$").
8
+
9
+ raw_log(l1, "ts=2026-06-18T10:00:00Z level=warn event=login_failed user=alice ip=203.0.113.9 traceparent=00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").
10
+ raw_log(l2, "ts=2026-06-18T10:00:03Z level=error event=payment_denied user=alice ip=203.0.113.9 traceparent=00-4bf92f3577b34da6a3ce929d0e0e4736-aaf067aa0ba90000-01").
11
+ raw_log(l3, "ts=2026-06-18T10:01:12Z level=info event=login_success user=bob ip=198.51.100.4 traceparent=00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01").
12
+ raw_log(noise, "healthcheck ok").
13
+
14
+ parsed(Log, Context) :-
15
+ raw_log(Log, Text),
16
+ log_pattern(Pattern),
17
+ matches(Text, Pattern, Context).
18
+
19
+ % holds/3 lets one generic rule project every named capture as field data.
20
+ captured_field(Log, Name, Value) :-
21
+ parsed(Log, Context),
22
+ holds(Context, Name, [Value]).
23
+
24
+ parsed_event(Log, Event, User, Ip, TraceId) :-
25
+ parsed(Log, Context),
26
+ holds(Context, event(Event)),
27
+ holds(Context, user(User)),
28
+ holds(Context, ip(Ip)),
29
+ holds(Context, trace_id(TraceId)).
30
+
31
+ trace_alert(User, TraceId, Ip) :-
32
+ parsed_event(LoginLog, "login_failed", User, Ip, TraceId),
33
+ parsed_event(PaymentLog, "payment_denied", User, Ip, TraceId),
34
+ neq(LoginLog, PaymentLog).
@@ -0,0 +1,28 @@
1
+ captured_field(l1, ts, "2026-06-18T10:00:00Z").
2
+ captured_field(l1, level, "warn").
3
+ captured_field(l1, event, "login_failed").
4
+ captured_field(l1, user, "alice").
5
+ captured_field(l1, ip, "203.0.113.9").
6
+ captured_field(l1, trace_id, "4bf92f3577b34da6a3ce929d0e0e4736").
7
+ captured_field(l1, span_id, "00f067aa0ba902b7").
8
+ captured_field(l1, flags, "01").
9
+ captured_field(l2, ts, "2026-06-18T10:00:03Z").
10
+ captured_field(l2, level, "error").
11
+ captured_field(l2, event, "payment_denied").
12
+ captured_field(l2, user, "alice").
13
+ captured_field(l2, ip, "203.0.113.9").
14
+ captured_field(l2, trace_id, "4bf92f3577b34da6a3ce929d0e0e4736").
15
+ captured_field(l2, span_id, "aaf067aa0ba90000").
16
+ captured_field(l2, flags, "01").
17
+ captured_field(l3, ts, "2026-06-18T10:01:12Z").
18
+ captured_field(l3, level, "info").
19
+ captured_field(l3, event, "login_success").
20
+ captured_field(l3, user, "bob").
21
+ captured_field(l3, ip, "198.51.100.4").
22
+ captured_field(l3, trace_id, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
23
+ captured_field(l3, span_id, "bbbbbbbbbbbbbbbb").
24
+ captured_field(l3, flags, "01").
25
+ parsed_event(l1, "login_failed", "alice", "203.0.113.9", "4bf92f3577b34da6a3ce929d0e0e4736").
26
+ parsed_event(l2, "payment_denied", "alice", "203.0.113.9", "4bf92f3577b34da6a3ce929d0e0e4736").
27
+ parsed_event(l3, "login_success", "bob", "198.51.100.4", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
28
+ trace_alert("alice", "4bf92f3577b34da6a3ce929d0e0e4736", "203.0.113.9").
@@ -1,11 +1,12 @@
1
1
  // String builtins.
2
2
  // They mostly project from already-ground terms to avoid guessing string domains.
3
- import { lexicalValue, stringTerm, unify } from '../term.js';
3
+ import { compound, lexicalValue, stringTerm, unify } from '../term.js';
4
4
 
5
5
  export const stringBuiltins = {
6
6
  register(registry) {
7
7
  registry.add('str_concat', 3, concat, { deterministic: true });
8
8
  for (const name of ['contains', 'matches', 'not_matches']) registry.add(name, 2, contains(name), { deterministic: true });
9
+ registry.add('matches', 3, matchCaptures, { deterministic: true });
9
10
  }
10
11
  };
11
12
 
@@ -32,6 +33,38 @@ function contains(name) {
32
33
  };
33
34
  }
34
35
 
36
+ function* matchCaptures({ goal, env }) {
37
+ const text = lexicalValue(goal.args[0], env);
38
+ const pattern = lexicalValue(goal.args[1], env);
39
+ if (text == null || pattern == null) return;
40
+
41
+ let match;
42
+ try {
43
+ match = new RegExp(pattern).exec(text);
44
+ } catch (_) {
45
+ return;
46
+ }
47
+ if (!match?.groups) return;
48
+
49
+ const context = contextFromGroups(match.groups);
50
+ if (context == null) return;
51
+
52
+ const next = env.clone();
53
+ if (unify(goal.args[2], context, next)) yield next;
54
+ }
55
+
56
+ function contextFromGroups(groups) {
57
+ const terms = [];
58
+ for (const [name, value] of Object.entries(groups)) {
59
+ if (value !== undefined) terms.push(compound(name, [stringTerm(value)]));
60
+ }
61
+ if (terms.length === 0) return null;
62
+
63
+ let context = terms[terms.length - 1];
64
+ for (let i = terms.length - 2; i >= 0; i--) context = compound(',', [terms[i], context]);
65
+ return context;
66
+ }
67
+
35
68
  function simpleAlternationMatch(haystack, pattern) {
36
69
  return pattern.split('|').some((part) => part === '' || haystack.includes(part));
37
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.33.11",
3
+ "version": "1.34.0",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -0,0 +1,13 @@
1
+ % Reference 9.6: matches/3 extracts named regular-expression captures into a context.
2
+ materialize(answer, 3).
3
+
4
+ line("event=login_failed user=alice trace=4bf92f3577b34da6a3ce929d0e0e4736").
5
+ pattern("^event=(?<event>\\w+) user=(?<user>\\w+) trace=(?<trace_id>[0-9a-f]{32})$").
6
+
7
+ answer(User, Event, TraceId) :-
8
+ line(Text),
9
+ pattern(Pattern),
10
+ matches(Text, Pattern, Context),
11
+ holds(Context, event(Event)),
12
+ holds(Context, user(User)),
13
+ holds(Context, trace_id(TraceId)).
@@ -0,0 +1 @@
1
+ answer("alice", "login_failed", "4bf92f3577b34da6a3ce929d0e0e4736").