eyeling 1.33.10 → 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 +5 -1
- package/docs/eyelang-language-reference.md +3 -0
- package/examples/eyelang/context-schema-audit.pl +46 -0
- package/examples/eyelang/observability-log-correlation.pl +34 -0
- package/examples/eyelang/output/context-schema-audit.pl +12 -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
|
@@ -296,7 +296,9 @@ holds((name(alice, "Alice"), knows(alice, bob)), name(S, O)).
|
|
|
296
296
|
holds((ready, name(alice, "Alice"), route(alice, bob, 7)), Name, Args).
|
|
297
297
|
```
|
|
298
298
|
|
|
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.
|
|
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
|
+
|
|
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.
|
|
300
302
|
|
|
301
303
|
|
|
302
304
|
## Example catalog
|
|
@@ -335,6 +337,7 @@ The repository includes examples for recursion, graph reachability, finite searc
|
|
|
335
337
|
| [`complex.pl`](../examples/eyelang/complex.pl) | Performs arithmetic on complex pairs. | [`output/complex.pl`](../examples/eyelang/output/complex.pl) |
|
|
336
338
|
| [`composition-of-injective-functions-is-injective.pl`](../examples/eyelang/composition-of-injective-functions-is-injective.pl) | Encodes composition and injectivity of finite functions. | [`output/composition-of-injective-functions-is-injective.pl`](../examples/eyelang/output/composition-of-injective-functions-is-injective.pl) |
|
|
337
339
|
| [`context-association.pl`](../examples/eyelang/context-association.pl) | Associates named contexts with their contents. | [`output/context-association.pl`](../examples/eyelang/output/context-association.pl) |
|
|
340
|
+
| [`context-schema-audit.pl`](../examples/eyelang/context-schema-audit.pl) | Audits mixed-arity context members with `holds/3`. | [`output/context-schema-audit.pl`](../examples/eyelang/output/context-schema-audit.pl) |
|
|
338
341
|
| [`control-system.pl`](../examples/eyelang/control-system.pl) | Evaluates control-system measurements and targets. | [`output/control-system.pl`](../examples/eyelang/output/control-system.pl) |
|
|
339
342
|
| [`cyclic-path.pl`](../examples/eyelang/cyclic-path.pl) | Computes paths in a cyclic graph. | [`output/cyclic-path.pl`](../examples/eyelang/output/cyclic-path.pl) |
|
|
340
343
|
| [`d3-group.pl`](../examples/eyelang/d3-group.pl) | Enumerates subgroups of the D3 group. | [`output/d3-group.pl`](../examples/eyelang/output/d3-group.pl) |
|
|
@@ -408,6 +411,7 @@ The repository includes examples for recursion, graph reachability, finite searc
|
|
|
408
411
|
| [`network-sla.pl`](../examples/eyelang/network-sla.pl) | Checks network path SLA compliance. | [`output/network-sla.pl`](../examples/eyelang/output/network-sla.pl) |
|
|
409
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) |
|
|
410
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) |
|
|
411
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) |
|
|
412
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) |
|
|
413
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
|
|
@@ -447,6 +448,8 @@ holds((ready, name(alice, "Alice"), route(alice, bob, 7)), Name, Args).
|
|
|
447
448
|
|
|
448
449
|
The first goal can yield `holds((name(alice, "Alice"), knows(alice, bob)), name(alice, "Alice")).` The second can yield `holds((ready, name(alice, "Alice"), route(alice, bob, 7)), ready, []).`, `holds((ready, name(alice, "Alice"), route(alice, bob, 7)), name, [alice, "Alice"]).`, and `holds((ready, name(alice, "Alice"), route(alice, bob, 7)), route, [alice, bob, 7]).`
|
|
449
450
|
|
|
451
|
+
`holds/3` is the appropriate form for schema-style introspection because it exposes the predicate name and all arguments without assuming a fixed arity. For example, a single rule can inspect `heartbeat`, `source(sensor17)`, `temperature(sensor17, 38)`, and `signature(sensor17, sha256, Hash, Time)` as `heartbeat/0`, `source/1`, `temperature/2`, and `signature/4`; see [`context-schema-audit.pl`](../examples/eyelang/context-schema-audit.pl).
|
|
452
|
+
|
|
450
453
|
### 9.10 Search control
|
|
451
454
|
|
|
452
455
|
| Built-in | Meaning |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
% Context schema audit.
|
|
2
|
+
%
|
|
3
|
+
% This example needs holds/3 rather than holds/2 because the audit rule does
|
|
4
|
+
% not know the predicate names or arities in advance. It inspects a whole
|
|
5
|
+
% context as data, extracts each member as Name + Args, computes the arity, and
|
|
6
|
+
% checks that shape against an allowed schema.
|
|
7
|
+
|
|
8
|
+
% Output declarations: materialize/2 selects the relations written to this example's golden output.
|
|
9
|
+
materialize(context_shape, 3).
|
|
10
|
+
materialize(schema_violation, 3).
|
|
11
|
+
|
|
12
|
+
% Program structure: each message carries heterogeneous context data. The
|
|
13
|
+
% members deliberately use different arities: heartbeat/0, source/1,
|
|
14
|
+
% temperature/2, gps/3, and signature/4.
|
|
15
|
+
message_context(msg_ok, (
|
|
16
|
+
heartbeat,
|
|
17
|
+
source(sensor17),
|
|
18
|
+
temperature(sensor17, 38),
|
|
19
|
+
gps(sensor17, 51, 4),
|
|
20
|
+
signature(sensor17, sha256, "9f86d081", "2026-06-18T09:30:00Z")
|
|
21
|
+
)).
|
|
22
|
+
|
|
23
|
+
message_context(msg_bad, (
|
|
24
|
+
heartbeat,
|
|
25
|
+
source(sensor18),
|
|
26
|
+
temperature(sensor18, 99),
|
|
27
|
+
gps(sensor18, 51),
|
|
28
|
+
tampered(sensor18)
|
|
29
|
+
)).
|
|
30
|
+
|
|
31
|
+
allowed_shape(heartbeat, 0).
|
|
32
|
+
allowed_shape(source, 1).
|
|
33
|
+
allowed_shape(temperature, 2).
|
|
34
|
+
allowed_shape(gps, 3).
|
|
35
|
+
allowed_shape(signature, 4).
|
|
36
|
+
|
|
37
|
+
% Derivation rules: holds/3 exposes arbitrary context members as predicate name
|
|
38
|
+
% plus argument list, so one generic rule can audit mixed-arity data.
|
|
39
|
+
context_shape(Message, Name, Arity) :-
|
|
40
|
+
message_context(Message, Context),
|
|
41
|
+
holds(Context, Name, Args),
|
|
42
|
+
length(Args, Arity).
|
|
43
|
+
|
|
44
|
+
schema_violation(Message, Name, Arity) :-
|
|
45
|
+
context_shape(Message, Name, Arity),
|
|
46
|
+
not(allowed_shape(Name, Arity)).
|
|
@@ -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,12 @@
|
|
|
1
|
+
context_shape(msg_ok, heartbeat, 0).
|
|
2
|
+
context_shape(msg_ok, source, 1).
|
|
3
|
+
context_shape(msg_ok, temperature, 2).
|
|
4
|
+
context_shape(msg_ok, gps, 3).
|
|
5
|
+
context_shape(msg_ok, signature, 4).
|
|
6
|
+
context_shape(msg_bad, heartbeat, 0).
|
|
7
|
+
context_shape(msg_bad, source, 1).
|
|
8
|
+
context_shape(msg_bad, temperature, 2).
|
|
9
|
+
context_shape(msg_bad, gps, 2).
|
|
10
|
+
context_shape(msg_bad, tampered, 1).
|
|
11
|
+
schema_violation(msg_bad, gps, 2).
|
|
12
|
+
schema_violation(msg_bad, tampered, 1).
|
|
@@ -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").
|