Skip to content

Nasdanika/nxpath

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 

Repository files navigation

nxpath

A path language over Nasdanika graphs with a pluggable, multi-language predicate kernel - reactive and telemetric by construction.

Status: Design draft. This document describes intent and open decisions, not a shipped API. Everything below is subject to revision as the kernel is implemented and integrated with NSML and waypoint.


1. Motivation

NSML and other Nasdanika tools need a way to navigate and select elements of a graph inside mapping rules and conditions. The graph is usually - but not always - an EMF/Ecore model; it can equally be a Drawio diagram graph, or anything else exposed through the Nasdanika graph API. The obvious off-the-shelf candidates fall short:

  • JXPath (Apache Commons) is frozen at XPath 1.0 semantics. The core received a maintenance release (1.4.0, April 2025) but no feature evolution, and the Ecore extension it depended on has been dormant since ~2010–2012.
  • OGNL has projection/selection/lambdas, but its expression language is fixed, and it carries a long history of remote-code-execution CVEs - a poor fit when expressions may be authored by AI agents.
  • OCL / Epsilon (EOL/EVL) / QVT are mature and standards-based, but each is a complete language. Adopting one means asking authors to learn an entire sublanguage - the exact cost nxpath is designed to remove.
  • None of the above is graph-agnostic, lazy/cancellable, or telemetric - properties that matter when predicates are expensive (AI or external calls) and the graph is large.

The insight behind nxpath: the structural part of a query (axes, steps) is small, stable, and easy to learn; the condition part is where the power lives. Rather than invent a condition language, nxpath keeps a minimal XPath-style navigation grammar and delegates predicates to a pluggable expression engine - SpEL by default, swappable per expression or per context.

This matters most for agentic systems, NSML's primary target. A small, fixed navigation grammar is far more reliably generated and validated than free-form expressions, while the pluggable predicate gives agents (and humans) full power where they need it. nxpath optimizes for expressive power and predictability over raw evaluation speed.

2. Landscape

Option Substrate Navigation Condition language Lazy / cancellable Telemetric Fit for agents
JXPath + Ecore ext. Beans / EMF XPath 1.0 Fixed (XPath 1.0) No No Weak
OGNL Beans Property/index Fixed (OGNL) No No Weak (RCE history)
OCL / Epsilon / QVT MOF/Ecore Full Whole language No No Heavy onboarding
Groovy GPath Object graph XPath-like Groovy only No No Couples to Groovy
SpEL alone Object graph Selection/projection SpEL only No No No descendant axes
nxpath Any Nasdanika graph Minimal, fixed Pluggable (SpEL default) Yes (Flux) Yes (waypoint) Designed for it

The closest conceptual match - "JXPath with a pluggable predicate language" - does not exist as a maintained artifact, and nothing in the landscape is lazy, cancellable, or telemetric.

3. Core idea: a thin traversal kernel over graph + waypoint

nxpath separates three concerns that XPath, OGNL, and OCL conflate (or omit):

  1. Navigation - moving through a graph along axes (children, descendants, parent, all references). A small, fixed grammar, expressed over the Nasdanika graph API rather than any one model technology.
  2. Conditioning - deciding which nodes to keep. Delegated to a predicate evaluator resolved through a dispatcher (SpEL default).
  3. Execution - threading the traversal through a waypoint, which supplies lineage/provenance, optional state, telemetry, and a reactive (Flux) modality with cancellation.

nxpath itself is therefore thin: a grammar, an axis-to-edge mapping, a predicate dispatcher, and stream assembly. The node/edge model lives in the graph layer; lineage/state/telemetry/async live in waypoint.

nxpath expression
        │
        ▼
   Path engine (axes, steps)  ──nodes/edges──▶  Nasdanika graph API
        │                                        (EMF graph, diagram graph, …)
        │  for each step → emits Waypoints ────▶ waypoint
        │                                        (lineage, state, telemetry, Flux)
        ▼   for each predicate
   Predicate dispatcher (overridable kernel method)
        │
   ┌────┼─────────────┐
   ▼    ▼             ▼
 SpEL  Groovy   <your engine>   ── may call LLMs / external systems
(def)  (groovy@…)               (each evaluation can be its own span)

4. Syntax (proposed)

4.1 Steps and axes

Familiar XPath-style structure, deliberately minimal:

personas                     # child step (follow the named feature edge)
personas/concerns            # nested child step
//capability                 # descendant over CONTAINMENT only (pre-order / document order)
///capability                # descendant over ALL references, breadth-first, each object once
..                           # parent
.                            # self
@name                        # attribute / scalar feature (sigil TBD - see Open Decisions)
*                            # all containment children (single hop)

The two transitive axes are deliberately distinct rather than one operator with a flag. Their meaning is defined in terms of the graph adapter's edge classification, not any one model technology:

  • // - primary closure. Follows the adapter's primary / containment-like edge subset transitively. The adapter declares which edges are primary: EMF containment references, diagram element nesting, etc. This subset is expected to be acyclic (a tree/forest), so traversal is ordered pre-order (document order), matching XPath intuition. If an adapter's primary subset is not acyclic, waypoint's visit-tracking still guarantees termination, but ordering degrades to breadth-first.
  • /// - reachability closure. Follows all edges the adapter exposes (primary and cross-edges), breadth-first, yielding each node at most once. This is general graph traversal; cycles and diamonds are expected. Termination and visit-tracking are guaranteed by waypoint (see §6.3).

So for an EMF graph, // ≈ containment and /// ≈ all references; for a diagram graph, // ≈ nesting and /// ≈ all connections. The grammar is identical across substrates; only the adapter's edge classification differs.

Note the intentional order asymmetry: // is pre-order, /// is breadth-first (nearest-first). Authors and agents generating paths must account for this.

4.2 Predicates

A predicate is a bracketed condition. The condition is an expression in a pluggable language, reusing NSML's expression convention:

personas[<language>:<inline-expression>]
personas[<language>@<URI-to-external-expression>]

When no language prefix is given, the default dispatcher (SpEL) is used:

personas[active]                          # SpEL: node.active == true
concerns[priority > 3]                    # SpEL
personas["groovy@persona-selector.groovy"]  # external Groovy script
capabilities[spel:owner.team == 'core']   # explicit SpEL

This is intentionally identical to NSML's <language>:<expression> (inline) and <language>@<URI> (external reference) forms, so authors learn one expression convention across NSML and nxpath. Default dispatch languages align with NSML: spel, groovy, and others as registered.

4.3 Composition

Predicates and steps compose left to right:

//persona[spel:role == 'decision-maker']/concerns[severity >= 'HIGH']/addressedBy

5. Examples

# Personas with at least one unaddressed high-severity concern
//persona[concerns[severity == 'HIGH' and addressedBy.isEmpty()]]

# Capabilities selected by an external decision script
capabilities["groovy@mcda-select.groovy"]

# All proposed-lifecycle features under the current node (containment)
.//feature[lifecycle == 'PROPOSED']

# Every capability reachable from a persona through any reference, deduplicated
//persona[role == 'sponsor']///capability

6. Architecture

6.1 The kernel

The kernel is the path engine plus a single overridable predicate-evaluation method, conceptually:

// Illustrative - not a committed signature.
public interface NxpathKernel<E> {

    /** Synchronous evaluation: eager stream of result nodes. */
    Stream<Waypoint<E>> evaluate(String path, NodeContext<E> context);

    /** Asynchronous evaluation: cold, lazy, cancellable Flux (see §6.4). */
    Flux<Waypoint<E>> evaluateAsync(String path, NodeContext<E> context);

    /**
     * Override to change how predicates are evaluated.
     * Default delegates to the SpEL dispatcher. The async form returns
     * Mono<Boolean> so expensive predicates (LLM / external calls) can be
     * non-blocking and cancellable.
     */
    boolean evaluatePredicate(
        String language,           // null/empty => default
        String expression,         // inline expression or resolved external source
        NodeContext<E> nodeContext // current node, position, size, root, variables
    );

    Mono<Boolean> evaluatePredicateAsync(
        String language,
        String expression,
        NodeContext<E> nodeContext);
}

6.2 The dispatcher

The default evaluatePredicate resolves language to a registered dispatcher:

  • null / empty → default dispatcher (SpEL).
  • spel, groovy, … → the corresponding registered engine.
  • A custom kernel may install its own dispatcher (e.g. a single fixed language, an allow-listed subset, or a remote evaluator).

Dispatchers are registered, not hard-wired, so the set of condition languages is an integration concern, not a compile-time one. This mirrors NSML's existing language registry.

6.3 Implementation substrate: Nasdanika graph + waypoint

nxpath is a traversal engine layered on two existing Nasdanika components:

  • The Nasdanika graph API supplies the node/edge model. nxpath navigates generic graph Elements; it does not bind to EMF. The EMF/Ecore graph is one adapter; a Drawio diagram graph is another; others are possible. Each adapter decides how its structure maps to nodes and edges, which edges are primary (the // subset) vs cross-edges, and how proxies/derived features/inverse edges are handled. All the model-technology-specific semantics live here, not in nxpath.
  • waypoint threads the traversal. As the traversal advances, nxpath emits waypoints whose position E is the current graph Element. This is exactly the use case waypoint's own §13 anticipates: "Stream-of-waypoints APIs (walkers, traversal engines) sit on top of this module and may use Flux." nxpath is that traversal engine. It may be built on, or motivate, waypoint's proposed generic WaypointWalker (waypoint open question #4).

Building on waypoint, nxpath inherits four things rather than reimplementing them:

  1. Visit-tracking / termination. For ///, every node is traversed at most once even with cycles and diamonds - the safety property that makes reachability traversal usable at all.
  2. Lineage / provenance. Each result waypoint's parent chain records the path by which it was reached. "Why was this node selected?" is structurally answerable, not reconstructed - consistent with the ecosystem's accountability-through-provenance principle and directly useful for agent reasoning.
  3. Telemetry. Via TelemetryWaypoint, traversal can emit OpenTelemetry spans; "the waypoint tree is the trace tree." See §10.
  4. Reactive modality with cancellation. Via waypoint's Mono/Flux design and cancellation hooks (waypoint open question #6). See §6.4.

To pin down: waypoint's dedup identity key - object identity (==) vs logical URI. With proxies and cross-resource references these diverge; the choice is a semantic commitment of ///, not an implementation detail. (For non-EMF graphs the adapter defines node identity, which makes this an adapter concern.)

6.4 Reactive evaluation - the differentiator

Every path can be evaluated in two modalities, mirroring waypoint's sync/async-parity principle (its §5.1):

  • Synchronous - returns a Stream / iterator of result nodes. Correct for cheap, deterministic predicates (concerns[priority > 3]), where reactive machinery is pure overhead.
  • Asynchronous - returns a cold Flux<Waypoint<E>>. This is what no other path library offers, and it earns its keep precisely when predicates are expensive - calling an LLM, an external service, or a heavy computation.

The async flavor gives three properties for free:

  1. Pay only for what you consume. The Flux is cold and lazy: nothing is traversed until subscription, and take(n) / takeUntil(...) / unsubscribe stops the traversal. On a large ///, predicates for nodes past the cutoff are never evaluated. This also defuses the cost concern of reachability traversal - /// is only as expensive as the prefix you actually drain.
  2. Cancellation of in-flight work. When the downstream cancels (e.g. take(3) is satisfied), in-flight asynchronous predicate evaluations are cancelled, not merely abandoned - built on waypoint's cancellation hooks. Caveat: a Reactor-native predicate cancels cleanly; a blocking SpEL/Groovy predicate evaluated on a worker thread honours cancellation only best-effort. Such predicates should run on a bounded-elastic scheduler, and the limit should be documented.
  3. Free short-circuiting and bounded fan-out. "First match" (next()), "exists" (hasElements()), and "limit" fall out of Reactor operators. Concurrent predicate evaluation is governed by standard backpressure.

Ordering vs concurrency - a decision. Evaluating predicates concurrently (flatMap) maximises throughput but breaks strict BFS emission order, which collides with the determinism agents want. Serial evaluation (concatMap) preserves order. Recommended: ordered by default, with an explicit opt-in to a concurrent/unordered mode for throughput-bound workloads.

7. Context-binding contract (the hard part)

The grammar is the easy part; the binding contract is where correctness lives. Each predicate evaluates against a NodeContext that must be bound consistently into every engine. The proposed bindings:

Binding Meaning SpEL Groovy
current node the node under test root object / #this delegate / it
position 1-based index within the current node-set variable binding var
last / size size of the current node-set variable binding var
root the evaluation root variable binding var
named variables caller-supplied variables binding vars

Decision required: whether the current node is the implicit root of the expression (so active means currentNode.active) or must be referenced explicitly (#this.active). The implicit-root form is more ergonomic and more XPath-like; it requires each dispatcher to set the engine's root/delegate correctly. Recommended: implicit root, explicit handle (#this / it) also available.

8. Graph model and adapters

nxpath is defined over the Nasdanika graph API; model-technology semantics are delegated to the graph adapter (§6.3), not decided by nxpath. The contract nxpath relies on:

  • Node-set vs scalar. A step over a one-to-many edge yields a node-set; over a one-to-one edge, a singleton or empty.
  • Primary vs reachability - resolved. // follows the adapter's primary edge subset (expected acyclic, pre-order). /// follows all edges, breadth-first, visit-once. Cycles are handled by waypoint, not avoided by restriction.
  • Edge directionality of /// - to pin down. Whether /// follows outgoing edges only, or also materialized inverse/opposite/container edges the adapter exposes. Cleanest definition: /// = transitive closure over the adapter's declared edge set, whatever it is configured to include.
  • Filter vs prune - to decide. XPath-style, ///[p] filters output but still traverses through non-matching nodes. A pruning variant (stop descending past a failing predicate) is cheaper on large graphs but changes semantics. Recommended default: filter-only, with pruning as a possible future opt-in. (Note that with the lazy Flux modality, §6.4, unbounded traversal is far less costly than it sounds - you stop when you stop consuming.)
  • Node identity. Defined by the adapter; it determines the /// dedup key. For the EMF adapter this is the object-identity vs URI question above; for a diagram adapter it is element id.

8.1 Reference adapters

  • EMF graph adapter. Nodes wrap EObjects; // ≈ containment, /// ≈ all references. Attributes vs references (the @ sigil question, §13), derived/transient features, and proxy resolution are this adapter's concern.
  • Diagram graph adapter. Nodes are Drawio elements; // ≈ element nesting, /// ≈ all connections. This is what lets the same path expression run against a diagram and against the semantic model it maps to.

9. Security model

SpEL, Groovy, and OGNL are all capable of arbitrary code execution. Because NSML targets agentic systems, predicates may be authored by an AI agent, so this is a live threat, not a theoretical one.

  • The default dispatcher should run in a constrained mode (no arbitrary type references, no constructor/T() access, method allow-list) suitable for agent-authored expressions.
  • A separate, explicitly-opted-in full-power dispatcher for trusted, human-authored expressions.
  • Per-dispatcher policy hooks so integrators can tighten or sandbox.
  • External-reference resolution (<lang>@<URI>) must respect a URI allow-list; never resolve arbitrary URIs from untrusted input.

10. Agentic design considerations

  • Constrained navigation grammar - small, fixed, and documented, so an LLM emits valid paths reliably and a validator can reject malformed ones before evaluation.
  • Lazy evaluation of expensive predicates. When a predicate calls an LLM or external system, the async Flux (§6.4) ensures those calls happen only for nodes actually consumed, and stop on cancellation. An agent asking "find me up to 3 capabilities matching this judgement" pays for roughly 3 evaluations, not the whole reachable set.
  • Telemetry inheritance. Because traversal threads through TelemetryWaypoint, each predicate evaluation can be its own OpenTelemetry span - "the waypoint tree is the trace tree." Predicates that invoke models can carry gen-ai semantic-convention attributes (gen_ai.request.model, token usage, latency, cost), so the cost of evaluating a path is observable and attributable per step. Decision: granularity. Spanning every node on a large /// causes span explosion; span predicate evaluations (especially async/external ones) rather than every cheap structural step, and make this configurable/sampled.
  • Provenance - each result's waypoint parent chain records why it was selected (which steps, which predicates), structurally rather than reconstructed from logs.
  • Determinism - given the same graph and path, results must be stable and order-defined for agent reasoning to be reproducible; this is why ordered emission is the default (§6.4).

11. Relationship to NSML and Nasdanika

  • nxpath reuses NSML's expression convention (<language>:<expression>, <language>@<URI>) and language registry, so it is incremental over machinery NSML already requires rather than a separate stack.
  • nxpath is a navigation/selection facility; NSML rules remain the transformation layer (compiling to org.nasdanika.common.Transformer). nxpath expressions appear inside NSML conditions and selectors.
  • nxpath is a traversal engine over the Nasdanika graph API, threading waypoint for lineage, telemetry, and the reactive modality. It is assembled almost entirely from components that already exist - its own surface is grammar + axis mapping + predicate dispatch + stream assembly.
  • Consistent with the ecosystem's principles: formal models as the substrate for federation by URI, provenance, and AI-readability.

12. Non-goals

  • Not a full query/transformation language (that's NSML; for heavy constraint work, OCL/Epsilon remain available).
  • Not optimized for raw throughput on cheap predicates; power, laziness, and predictability over speed. (The reactive flavor is about not doing expensive work, not about doing cheap work faster.)
  • Not a new condition language - conditions are delegated, never reinvented.
  • Not a new graph or traversal model - the graph API and waypoint own those.
  • Not bound to EMF - EMF is one graph adapter among several.

13. Open design decisions

  1. Name. nxpath ties the brand to XPath (XML-flavored, 1.0-era) - now even less apt, since nxpath navigates arbitrary graphs, not XML. npath collides with the NPath complexity metric (avoid). A name signalling "path over graphs, your choice of condition language, reactive" is worth weighing before the repo name is fixed.
  2. Implicit vs explicit current-node reference in predicates (§7).
  3. Attribute/edge sigil (@) vs uniform steps - an adapter concern (§8).
  4. /// dedup identity key - adapter-defined; for EMF, object identity vs logical URI (§6.3, §8).
  5. /// edge directionality - outgoing edges only vs the adapter's full declared edge set including inverse/opposite (§8).
  6. ///[predicate] filter-only vs prune semantics (§8). Recommended default: filter-only.
  7. Ordered vs concurrent async emission (§6.4). Recommended: ordered (BFS) by default, opt-in concurrent/unordered.
  8. Telemetry granularity / sampling (§10) - span predicate evaluations, not every structural step.
  9. Cancellation of blocking predicates (§6.4) - bounded-elastic scheduler, best-effort, documented limits.
  10. Default dispatcher security posture and the trusted/untrusted split (§9).
  11. Whether nxpath is built on, or motivates, waypoint's proposed generic WaypointWalker (waypoint OQ #4).
  12. Whether nxpath produces a reusable compiled form (parse once, evaluate many) and what that artifact looks like.

Resolved since first draft: operates over the Nasdanika graph API (not EMF directly); // is the adapter's primary/containment-like edge closure (pre-order), /// is all-edges breadth-first with visit-once dedup; termination, lineage, telemetry, and the reactive modality are inherited from waypoint.


Draft design document. Contributions and corrections welcome once the repository is established.

About

XPath-inspired graph traversal language with pluggable condition languages and reactive traversal with cancellation

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors