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 +1 -1
- package/docs/eyelang-guide.md +3 -0
- package/docs/eyelang-language-reference.md +1 -0
- package/examples/eyelang/observability-log-correlation.pl +34 -0
- package/examples/eyelang/output/observability-log-correlation.pl +28 -0
- package/lib/eyelang/builtins/strings.js +34 -1
- package/package.json +1 -1
- package/test/eyelang/conformance/cases/extension/044_matches_named_captures.pl +13 -0
- package/test/eyelang/conformance/expected/extension/044_matches_named_captures.out +1 -0
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` |
|
package/docs/eyelang-guide.md
CHANGED
|
@@ -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
|
@@ -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").
|