eyeling 1.33.11 → 1.34.1
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 +2 -2
- package/docs/eyelang-guide.md +4 -1
- 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/test/eyelang/run-regression.mjs +33 -0
package/README.md
CHANGED
|
@@ -818,13 +818,13 @@ Formula-aware built-ins make Eyeling useful for meta-reasoning. `log:includes`,
|
|
|
818
818
|
|
|
819
819
|
### eyelang built-ins
|
|
820
820
|
|
|
821
|
-
The eyelang engine has its own built-in registry under `lib/eyelang/builtins/`. These are separate from the N3 namespaces above and are called as ordinary eyelang predicates. See the [eyelang language reference](docs/eyelang-language-reference.md#9-standard-built-in-predicates) for the portable profile. The bundled implementation currently registers:
|
|
821
|
+
The eyelang engine has its own built-in registry under `lib/eyelang/builtins/`. These are separate from the N3 namespaces above and are called as ordinary eyelang predicates. See the [eyelang language reference](docs/eyelang-language-reference.md#9-standard-built-in-predicates) for the portable profile. The bundled implementation currently registers 68 name/arity entries across 66 predicate names:
|
|
822
822
|
|
|
823
823
|
| Family | Built-ins |
|
|
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
|
@@ -261,7 +261,7 @@ best(Cycle, Cost) :-
|
|
|
261
261
|
weighted_hamiltonian_cycle(edge, Cities, Cycle, Cost).
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
The reusable search and numeric helpers include `n_queens/2`, Hamiltonian path/cycle helpers, `bounded_path/5`, `cnf_model/3`, Quine-McCluskey helpers, number-theory helpers such as `extended_gcd/5`, and matrix helpers such as `matrix_multiply/2`. These helpers are extension builtins of this implementation; [the eyelang language reference](eyelang-language-reference.md) defines the portable core and standard builtin profile.
|
|
264
|
+
The reusable search and numeric helpers include `n_queens/2`, Hamiltonian path/cycle helpers, `bounded_path/5`, `cnf_model/3`, Quine-McCluskey helpers, number-theory helpers such as `extended_gcd/5`, and matrix helpers such as `matrix_multiply/2`. These helpers are extension builtins of this implementation; [the eyelang language reference](eyelang-language-reference.md) defines the portable core and standard builtin profile. The complete bundled implementation list is kept in the top-level [README built-ins section](../README.md#built-ins-1), and the regression suite checks that table against the actual runtime registry.
|
|
265
265
|
|
|
266
266
|
To add a builtin, create or extend a module with `register(registry)` and call `registry.add(name, arity, handler, options)`. The default registry is assembled in [`lib/eyelang/builtins/registry.js`](../lib/eyelang/builtins/registry.js). Builtins that are only safe for specific argument modes should provide a `ready` predicate and `fallbackWhenNotReady: true`, so user-defined clauses remain visible until the builtin is applicable.
|
|
267
267
|
|
|
@@ -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").
|
|
@@ -196,6 +196,18 @@ why(
|
|
|
196
196
|
assertEqual(readmeExamples.join('\n'), examples.join('\n'), 'README example catalog');
|
|
197
197
|
},
|
|
198
198
|
},
|
|
199
|
+
{
|
|
200
|
+
name: 'README mirrors the eyelang builtin registry',
|
|
201
|
+
run: () => {
|
|
202
|
+
const actual = registeredBuiltinNames();
|
|
203
|
+
const documented = readmeBuiltinNames();
|
|
204
|
+
assertEqual(documented.join('\n'), actual.join('\n'), 'README builtin catalog');
|
|
205
|
+
|
|
206
|
+
const { entries, names } = readmeBuiltinSummary();
|
|
207
|
+
assertEqual(entries, actual.length, 'README builtin entry count');
|
|
208
|
+
assertEqual(names, new Set(actual.map((item) => item.split('/')[0])).size, 'README builtin name count');
|
|
209
|
+
},
|
|
210
|
+
},
|
|
199
211
|
{
|
|
200
212
|
name: 'stdin input is accepted',
|
|
201
213
|
run: () => {
|
|
@@ -499,6 +511,27 @@ function readmeCatalogExampleNames() {
|
|
|
499
511
|
.sort();
|
|
500
512
|
}
|
|
501
513
|
|
|
514
|
+
function registeredBuiltinNames() {
|
|
515
|
+
return [...createDefaultRegistry().defs.keys()].sort();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function readmeBuiltinNames() {
|
|
519
|
+
const readme = fs.readFileSync(path.join(packageRoot, 'README.md'), 'utf8');
|
|
520
|
+
const section = between(readme, '### eyelang built-ins', '## Custom built-ins');
|
|
521
|
+
return [...section.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)\/(\d+)`/g)]
|
|
522
|
+
.map((match) => `${match[1]}/${match[2]}`)
|
|
523
|
+
.filter((name, index, names) => names.indexOf(name) === index)
|
|
524
|
+
.sort();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function readmeBuiltinSummary() {
|
|
528
|
+
const readme = fs.readFileSync(path.join(packageRoot, 'README.md'), 'utf8');
|
|
529
|
+
const section = between(readme, '### eyelang built-ins', '## Custom built-ins');
|
|
530
|
+
const match = section.match(/currently registers (\d+) name\/arity entries across (\d+) predicate names/);
|
|
531
|
+
if (match == null) throw new Error('README builtin summary not found');
|
|
532
|
+
return { entries: Number(match[1]), names: Number(match[2]) };
|
|
533
|
+
}
|
|
534
|
+
|
|
502
535
|
function playgroundExampleNames() {
|
|
503
536
|
const html = fs.readFileSync(path.join(root, 'playground.html'), 'utf8');
|
|
504
537
|
const match = html.match(/const EXAMPLES = \[(.*?)\];/s);
|