Reaktor Graph Visitors

Capture and observability. Graph evidence and Shadow capture separate visitor policy compilation from runtime hook installation. Traversal scope is not call lifetime; a visitor alone does not capture every closure argument, reference escape or downstream effect. Installed instrumentation has bounded scope, coverage and owned teardown.

Visitors derive layers of the Reaktor Living Graph, its proposed single logical, heterogeneous, nested port graph. Selectors choose relationships and scope; bounded traversals determine order; visitors produce typed results; rewrites produce patches. Source, composition, runtime, data, delivery and agent views retain the same element identities while selecting different relationships, revisions and observations.

What it is

A Reaktor app already composes typed nodes, ports, edges, routes, containers, actors and services. LivingGraph extends that shared vocabulary across definition, build, runtime and operational facts. A visitor derives a view, index, finding or plan from those facts. Executing an effect or installing a runtime subscription is a separate authorized operation; a read visitor does not acquire mutation authority by traversing an element.

The lowest-level traversal primitives (the bare Selector/Traverser/PortGraphVisitor over Visitable) currently sit in reaktor-graph-port; graph-specific selectors/hooks sit in reaktor-graph. The proposed graph-aware pass, patch and finding APIs build on that seam. Higher modules should consume stable query contracts instead of mutating graph internals. Exact package placement must preserve graph-agnostic library APIs and the framework/editor/host layer boundaries.

This matters because the same small spine answers very different questions. You do not add an analytics engine, an auth engine, a deep-link router, or a workflow compiler to the graph core. You write a pass.

What it powers

The following proposed capabilities reuse selectors and passes over the same logical graph. A pass can discover requirements and produce an installation plan; the corresponding runtime adapter still owns execution, lifecycle and effects.

CapabilitySelectorPass(es)
Deep linksRouteSelectorRouteIndexPass → DeepLinkInstallPass (URL ↔ NavCommand)
Agentic tooling / control planeStructuralSelectorGraphDocumentPass (deterministic snapshot) + Findings — the knowledge graph an agent reads
AnalyticsReactiveActiveSelectorAnalyticsInstallPass (fires screen_view on active-route change)
Telemetryreactive lifecycle / nav / portinstrument passes over the live event stream (OTel spans per node)
AuthRouteSelectorAuthCoveragePass + inheritable AuthAttr.RequiresUser
Workflow DAGsWorkflowSelectorWorkflowPlanPass (Topological traversal)
WiringPortConnectivitySelectorWirePass + RequireFullyWiredPass (via PortMatcher)
Module contractsModuleContractSelectorModuleContractPass, CrossGraphReachabilityPass

The design cases come from two real compositions: the BestBuds product graph (nested routing, services, state, feature flags, parent-owned routes handed into child graphs) and the Reaktor Engine workbench graph that inspects apps including itself. Their existence does not establish that this proposed pass library has been executed or verified.

Principles

PrincipleMeaningConsequence
One logical graphDefinition, implementation, runtime and operational facts share identity and typed relations.A layer is a query; runtime epochs do not overwrite definition revisions.
Selectors are viewsStructural, routing, dependency, active-route, task and module views select relations from the same substrate.Selectors and visitors compose without creating another authored graph.
Arcs carry meaningA VisitArc preserves relationship kind, endpoint roles and provenance.A dependency binding is not confused with a data emission or observed call.
Pure queries, explicit changesVisitors read immutable snapshot elements and return results or proposed patches.Effects, installation and activation occur through authorized executors.
Reproducible within a declared cutSnapshot vector, query, ordering and coverage identify exactly what was inspected.A partial scan cannot produce a complete-policy verdict or an exact graph diff.
Typed resultsResults include coverage, findings and dependencies alongside domain values.Cached results can be invalidated correctly; unknown is not success.
Bounded executionWork, depth, time, output and cancellation are explicit.Agents and interactive views can query large or cyclic graphs without an unbounded walk.
Reactive when askedState projections and loss-accounted event subscriptions are different contracts.Display updates may conflate; audit, control and delivery events may not silently disappear.
Polyglot contractsCross-language compatibility uses versioned schemas and protocol requirements.A failed contract check cannot fall through to a weaker local type match.

The model

Six proposed types form the spine, with two companions (a patch value and a shared-results context). The names are illustrative, not frozen public API declarations; Living Graph ownership and identity remain canonical. In these sketches, Visitable means an immutable snapshot element/reference, not a mutable runtime Node passed through to an agent. Runtime-specific adapters resolve activation references only in the executor.

interface Visitable                       // anything that can be visited

data class VisitArc(                       // a typed relationship
    val from: Visitable?,
    val to: Visitable,
    val kind: VisitKind,
    val label: String = "",
    val attributes: Attributes = Attributes.Empty,
)

fun interface Selector {                   // a view: outgoing arcs from a point
    fun outgoing(ctx: VisitContext, current: Visitable): Sequence<VisitArc>
}

interface Traversal {                      // bounded query order
    fun <R> traverse(root: Visitable, selector: Selector, visitor: Visitor<R>): R
}

interface Visitor<R> {                      // read-only logic, typed result
    fun enter(ctx: VisitContext): VisitEnter = VisitEnter()
    fun finish() {}
    fun result(): R
}

interface GraphPass<R> {                    // a packaged, composable capability
    val name: String
    val key: PassKey<R> get() = PassKey(name)
    val selector: Selector
    val traversal: Traversal
    val dependsOn: List<PassKey<*>> get() = emptyList()
    fun visitor(ctx: PassContext): Visitor<R>
}

// companions
sealed interface GraphPatch { val description: String }   // mutation as data
interface PassContext {
    val snapshot: GraphSnapshot
    val query: GraphQuerySpec
    operator fun <R> get(key: PassKey<R>): R
}

The traversal hands each visitor a VisitContext so visitors never recompute parent, depth, path, graph chain, or route chain themselves:

data class VisitContext(
    val root: Visitable, val current: Visitable, val parent: Visitable?,
    val incoming: VisitArc?, val depth: Int, val path: List<VisitArc>,
    val traversal: Traversal, val selector: Selector, val passContext: PassContext,
) {
    inline fun <reified T : Visitable> nearest(): T?     // walk back up the path
    val graph: GraphRef?                                // owning scope in this snapshot
    val graphPath: List<GraphRef>                        // scope chain to root
    val route: RouteRef?
    val routePath: List<RouteRef>
}

Current state & direction

Today's code (reaktor-graph-port/.../visitor/Visitor.kt and reaktor-graph/.../visitor/Visitor.kt) is small and useful but limited: a Selector returns List<Visitable> (bare neighbors, no relation meaning); PortGraphVisitor returns an untyped ExitScope closure; HierarchyVisitor exposes a lateinit rootMap; there is no context, no traversal control, no typed result, no patch model, no findings. The Structural/Connectivity/Routing selectors and HierarchyVisitor are duplicated across the port and graph layers, and BreadthFirstTraverser silently drops the exit scope so post-order only works under DFS.

More precisely, structural/connectivity variants exist in both layers, while the routing selector is graph-specific. Graph structural traversal enters ContainerNode.graphs. Connectivity follows a starting node's consumer dependencies toward providers; starting at Graph does not discover its nodes. The routing selector follows route/controller attachments, not the full navigation or execution trace. DFS uses a visited set but recursive calls and exit cleanup have no budget/cancellation contract; visitors receive mutable objects, so purity is not enforced.

Current runtime edge orientation is consumer → provider. ReaktorFlowAssembly renders these bindings provider → consumer as Data, and GraphDocument.AddEdge uses the latter pin convention. Preserve canonical endpoint roles and relationship kind in VisitArc; presentation direction must not silently change "depends on" into "observed value flow". Current PortEvent reports Created/Connected/Disconnected topology, not calls or traffic.

The direction is an additive upgrade, each piece landing without breaking a shipping app:

From

  • Selector → List<Visitable> (bare neighbors)
  • visit() → ExitScope (untyped closure)
  • lateinit rootMap result
  • walk-everything traversal
  • mutations by direct connect()/autoWire()
  • port match by exact Type equality (string plus optional KClass, without generic/schema contracts)
  • duplicated selectors; BFS drops exit

To

  • Selector → Sequence<VisitArc> with VisitKind
  • Visitor<R> with VisitContext + result()
  • VisitDecision (Continue / SkipChildren / Stop)
  • GraphPatch applied transactionally via Wiring
  • explicit PortMatcher modes: versioned cross-language contracts; separately bounded local compatibility
  • composable selectors; passes form a DAG
  • structured Findings; reactive + incremental

Legacy entry points can become adapters after their behavior is covered by tests. autoWire() must not become a wrapper that silently commits an unreviewed live patch: build-time binding and runtime reconfiguration have different activation requirements. toJsonElement() can adapt a query projection, and neighbor-style selectors can adapt through NeighborSelector.asSelector().

Selectors

Selectors are reusable views over the same graph snapshot. Changing the selector changes the relationship view; runtime changes create a new observation/epoch rather than mutating the snapshot under a visitor.

SelectorView / use case
StructuralSelectorcontainment tree — export, route indexing, validation, recursive compile.
RouteSelectorroutes — route index, deep links, sitemap, auth coverage.
NavigationSelectornavigation edges incl. CrossGraphEdge — reachability, dead-route detection.
PortConnectivitySelectorports/edges — wiring, capability analysis.
ActiveSelectoractive route chain — URL sync, analytics, focused back handling (has a reactive companion).
WorkflowSelectorworkflow dependency edges — task DAG extraction.
ModuleContractSelectormodule tree — boundary & provides/consumes inspection.
SupervisionSelectoractor supervision tree — deploy/devtools fault visualization.

Combinators — a view of a view is a view

infix fun Selector.union(other: Selector): Selector            // arcs from both
infix fun Selector.intersect(other: Selector): Selector        // arcs in both
fun Selector.filter(predicate: (VisitArc) -> Boolean): Selector
inline fun <reified T : Visitable> Selector.restrictTo(): Selector
fun Selector.relabel(transform: (VisitArc) -> VisitArc): Selector
fun Selector.stopAt(kind: VisitKind): Selector                 // boundary

// e.g. ports declared only on controllers, within this module:
val view = PortConnectivitySelector
    .filter { it.from is ControllerNode<*> }
    .restrictTo<ConsumerPort<*>>()
val withinModule = StructuralSelector.stopAt(VisitKind.ModuleBoundary)

Reactive selectors

Some views change over time. The following proposed ReactiveSelector illustrates a current-state projection, such as the active route. It may skip intermediate states and is distinct from an ordered event subscription. A plain selector remains a pure query over a fixed snapshot.

interface ReactiveSelector { fun arcs(graph: Graph): Flow<VisitArc> }

object ReactiveActiveSelector : ReactiveSelector {            // active route changes
    override fun arcs(graph: Graph) = graph.backStack.entries
        .map { it.lastOrNull()?.edge?.end }.distinctUntilChanged().filterNotNull()
        .map { route -> VisitArc(graph, route, VisitKind.ActiveRoute) }
}

An event subscription instead needs an initial snapshot vector, a resume cursor, ordered changes within a declared partition, bounded buffering, and explicit gaps/reset/expiry. A dropped event or disconnected source invalidates dependent exact results until a complete resnapshot or replay restores coverage. conflate() is suitable only for a display that needs the latest state; it must not be the default for audit events, activation receipts, analytics event accounting, mailbox work or policy decisions. Subscriptions must close with their owning activation/installation and release listeners and resources.

Traversals

Depth-first is the default. Breadth-first supports search/indexing. Topological drives workflow compilation and deploy ordering. A suspend variant runs alongside for IO-bound passes.

object DepthFirst : Traversal { /* visited-set, builds VisitContext, honors VisitDecision, runs onExit post-order */ }
object BreadthFirst : Traversal { /* search / level-order indexing */ }
object Topological : Traversal { /* DAG order; cycles surface as Findings, not exceptions */ }

interface SuspendTraversal { suspend fun <R> traverse(root, selector, visitor: SuspendVisitor<R>): R }

fun <R> GraphSnapshot.visit(selector: Selector = StructuralSelector, traversal: Traversal = DepthFirst, visitor: Visitor<R>): R
suspend fun <R> GraphSnapshot.visitAsync(selector, traversal: SuspendTraversal = SuspendDepthFirst, visitor: SuspendVisitor<R>): R

Snapshot consistency and coverage

Each query must name its definition revision, relevant activation epochs, source/index generations and source-partition watermarks: its snapshot vector. In a federated graph that vector is not automatically one globally atomic instant. The result must state the consistency guarantee the participating sources actually provide. A cross-source invariant needs a validated consistent cut or reconciliation evidence; a vector of unrelated timestamps alone cannot establish it.

Query scope also names selected relations and direction, containment/boundary policy, target/environment, authority-filtered visibility and traversal budgets. Results record covered and missing scopes/partitions, truncation, freshness, conflicts and source errors. Exact diff, reachability, compatibility and policy claims require complete coverage of the declared domain at the specified vector. Incomplete or inaccessible coverage yields Unknown/Partial, never "no dependency" or a passing policy verdict. A bounded partial result can still drive an exploratory display if it carries that status.

Caching keys include the vector, query, schema and pass versions plus the relevant authority scope. Dependencies track which elements, relationships and source generations the result read. Changes, source loss, cursor gaps, schema upgrades, expired freshness or changed authority invalidate affected results. Reuse across epochs is permitted only where the pass can prove its dependencies unchanged.

Visitors

A visitor supports typed results, early stop, child skipping, and exit hooks. ReaktorVisitor<R> dispatches to typed immutable view hooks so a pass author overrides only what it cares about. These views preserve references to runtime-shaped elements without exposing their mutable object implementations.

sealed interface VisitDecision { object Continue; object SkipChildren; object Stop }
data class VisitEnter(val decision: VisitDecision = Continue, val onExit: () -> Unit = {})

open class ReaktorVisitor<R> : Visitor<R> {
    open fun visitGraph(ctx, graph: GraphView): VisitEnter = VisitEnter()
    open fun visitContainer(ctx, container: ContainerView): VisitEnter = VisitEnter()
    open fun visitRoute(ctx, route: RouteView): VisitEnter = VisitEnter()
    open fun visitController(ctx, node: ControllerView): VisitEnter = VisitEnter()
    open fun visitActor(ctx, node: ActorView): VisitEnter = VisitEnter()
    open fun visitService(ctx, node: ServiceView): VisitEnter = VisitEnter()
    open fun visitConsumerPort(ctx, port: ConsumerPortView): VisitEnter = VisitEnter()
    open fun visitProviderPort(ctx, port: ProviderPortView): VisitEnter = VisitEnter()
    open fun visitEdge(ctx, edge: EdgeView): VisitEnter = VisitEnter()
}

Composition: run two visitors over one traversal with a.with(b): Visitor<Pair<A,B>> (export + validate in a single walk). Beyond two, prefer multiple passes — the compiler deduplicates work.

Incremental, proposed: a long-lived inspector can update its projection from a versioned change feed after an initial snapshot. onAttach/onDetach/onConnect/onDisconnect alone are not enough: it must account for attribute/schema changes, source refreshes, epoch changes and gaps, and periodically reconcile where the source cannot guarantee a complete feed. Today's PortEvent does not implement this complete invalidation protocol.

Mutations are patches

The proposed safety property: a Visitor<R> is read-only. A rewrite returns stable element/port references and expected prior values in a GraphPatch; it does not carry mutable runtime objects or ambient authority. A patch is based on a named definition revision and read snapshot vector.

sealed interface GraphPatch {
    data class ConnectPort(val consumer: PortRef, val provider: PortRef,
                           val expectedBinding: BindingRef?) : GraphPatch
    data class AttachNode(val scope: ElementId, val definition: NodeDefinition) : GraphPatch
    data class SetAttribute<T : Any>(val target: ElementId, val key: AttributeKey<T>,
                                    val expected: T?, val value: T) : GraphPatch
    data class Composite(val patches: List<GraphPatch>) : GraphPatch
}

interface RewriteVisitor<R> : Visitor<Pair<R, GraphPatch?>>   // still a pure query

// Sketch: caller submits a proposal, not a direct runtime mutation.
fun GraphSnapshot.propose(patch: GraphPatch): GraphChangeProposal

The change path is snapshot/query → proposal → normalize/validate → fingerprinted impact and activation plan → authority/policy checks → definition commit → execution/reconciliation → receipts. Validate the entire patch, including schema, scope ownership, cardinality, effects and expected base revision. A rejected or stale definition patch must leave the definition unchanged. The runtime executor prepares and validates new instances before a specified activation barrier, accounting for in-flight calls, actor admission/drain and state migration.

Existing grants cover routine authorized work; a pass or patch does not introduce a new confirmation step by itself. Additional approval depends on the actual operation and configured policy. See Living Graph commands and activation.

Atomicity is scoped. A definition commit or one process's binding publication can be atomic; an external deployment, database migration and message send cannot be described as one atomic graph transaction. Plans must declare affected environments, partial completion, compensation and recovery. Undo restores a definition edit; it does not automatically reverse external effects.

This machinery is not implemented by today's connect() or autoWire(): bulk connect can install some pairs before returning a failure, and synchronous listener exceptions can interrupt mutation reporting. The refinement plan is a proposal to close that gap, not proof that transactional wiring already exists.

Passes & diagnostics

A pass packages a selector + traversal + visitor into a named, reusable capability. Passes form a DAG: a pass declares dependsOn and reads upstream results through a typed PassContext, so DeepLinkInstallPass reuses the RouteIndex that RouteIndexPass already built instead of re-traversing.

val report = BestBuds.compile {
    +WirePass()                       // emits a GraphPatch
    +RequireFullyWiredPass()
    +RouteIndexPass()
    +CrossGraphReachabilityPass()
    +AuthCoveragePass(defaultRequiresUser = true) { public("/"); public("/onboarding") }
    +DeepLinkInstallPass()            // dependsOn = [RouteIndex]
    +AnalyticsInstallPass(analytics)  // reactive
}
if (!report.success) report.errors.forEach { Logger.e("compile.failed", it) }
val plans = report.installationPlans      // execute only through the authorized runtime executor

The compiler topologically sorts by dependsOn, runs each pass, caches results in the PassContext, and aggregates structured findings:

enum class Severity { Info, Warn, Error, Fatal }
data class Finding(val severity: Severity, val element: Visitable?, val code: String,
                   val message: String, val hint: String? = null, val cause: Throwable? = null)

Codes are stable strings — the workbench filters and groups by code, so renames are breaking and new codes are additive:

CodeSeverityMeaning
wiring.missingErrorConsumer port has no matching provider.
wiring.ambiguousErrorMore than one provider matches an unnamed consumer.
wiring.crossgraphInfoConsumer matched a provider from a parent graph's DI scope.
route.deadendWarnRoute has no attached node or no incoming edges.
route.duplicateErrorTwo routes share a pattern in the same graph.
module.contract.unsatisfiedErrorA module consumes a contract no upstream module provides.
navigation.unreachableErrorCross-graph edge whose target graph is not mounted under a container.
introspection.cycleFatalSelf-referential visitor hit a cycle without an IntrospectionExcluded mark.
pass.failedFatalA pass threw before completing.

Standard passes

The proposed standard library. Each addresses a use case from §What it powers; entries that install behavior produce installation plans, with execution outside the visitor.

PassPurposeMutates
WirePassConnect unmatched consumers to providers via PortMatcher.emits GraphPatch
RequireFullyWiredPassValidate every consumer has an edge.no — Findings
RouteIndexPassBuild a path-pattern → route map (reused downstream).no
GraphJsonPassRecursive JSON dump (backs legacy toJsonElement()).no
GraphDocumentPassThe Engine's first-class graph document: deterministic ordering, stable IDs, introspection-exclusion filter by default.no
DeepLinkInstallPassWire URL changes to NavCommand (dependsOn RouteIndexPass).via install
AnalyticsInstallPassEmit screen_view on active-route change (a ReactivePass).via install
AuthCoveragePassReport which routes are guarded vs public.no
WorkflowPlanPassCompile a task DAG from workflow edges (Topological).no
ModuleContractPassValidate module consumes are satisfied by some provides.no
CrossGraphReachabilityPassDetect cross-graph edges with no reachable target (today's silent Logger.w failure).no
SourceMapPassCollect source-code locations for nodes/ports for jump-to-definition (suspend).no

Example — GraphDocumentPass excludes introspection-marked nodes by default, so the workbench can render itself:

class GraphDocumentPass(val appId: String, val appLabel: String) : GraphPass<GraphDocument> {
    override val selector = StructuralSelector
        .filter { it.from?.attributes?.get(VisitorAttr.IntrospectionExcluded) != true }
    override val traversal = DepthFirst
    override fun visitor(ctx: PassContext) = GraphDocumentVisitor(appId, appLabel)
}

Cross-cutting concerns

Polyglot wiring — PortMatcher

The proposed cross-language rule is stronger than today's exact Type equality. ContractId must resolve a versioned schema and operation/protocol requirements; equal names alone are insufficient. Matching checks request/result types, variance where defined, streaming/cancellation, effects and supported target/transport behavior. It returns compatible, incompatible or unknown, with evidence.

PortMatcher.CrossLanguage = VersionedContractMatcher(schemaRegistry)
PortMatcher.LocalKotlin = LocalKTypeMatcher(explicitLocalPolicy)

These are explicit modes, not a fallback chain. A missing or incompatible cross-language contract cannot fall through to erased KClass/simple-name equality. Legacy local bindings may use a documented compatibility adapter, visibly scoped to that local implementation and excluded from polyglot compatibility claims. Adapters/conversions are explicit artifacts whose own contracts are validated.

Attributes & inheritance

Visitors need metadata the graph core must not know about. A typed Attributes bag carries it; keys opt into inheritance (node → owning graph → parent → root):

object AuthAttr {
  val RequiresUser =
    AttributeKey<Boolean>("auth.requiresUser",
      inheritable = true)   // set once on /home
}

Introspection & cycles

The workbench inspects graphs that may include itself. Three levels: the per-traversal visited set; an IntrospectionExcluded attribute that visualization selectors filter; and an explicit CyclePolicy (Stop / ReportAsError / Allow) for passes that must traverse cyclic structures. Visualizing the Engine must terminate.

Lifecycle integration

Proposed scheduling can associate pass/installation plans with Compile, Attaching, Active, Saving and Destroying. A query stays side-effect-free; a separate executor installs or tears down approved subscriptions at a validated activation boundary. It must account for draining, cancellation and state ownership rather than assuming that a lifecycle label makes a change safe. Today's attach starts lifecycle before later autoWire(), so prepare/wire/activate ordering needs explicit implementation and tests.

The use cases, concretely

Each higher-level capability can reuse query and planning passes; its adapters still own runtime behavior and effects. The following remain proposed integrations.

  • Deep links — RouteIndexPass builds the pattern→route map; DeepLinkInstallPass (depending on it) binds URL changes to NavCommand and back. See Deep Link Integration.
  • Agentic tooling — GraphDocumentPass produces an authorized snapshot with stable IDs, vector and coverage; Findings describe problems. Wiring/reachability checks establish those specific structural properties, not application health. See The Control Plane.
  • Analytics — AnalyticsInstallPass is a ReactivePass over ReactiveActiveSelector; it fires screen_view per distinct active route, attributing by AnalyticsAttr.Screen.
  • Telemetry — instrument passes subscribe to the live lifecycle/navigation/port event stream and emit OTel spans keyed by node id. See Performance & Observability.
  • Auth — AuthCoveragePass reports guarded vs public routes; the inheritable AuthAttr.RequiresUser set once on /home applies to every child route.
  • Workflow DAGs — WorkflowSelector + WorkflowPlanPass over the Topological traversal compile a task DAG; cycles surface as findings, not crashes.

Testing

Pushing capabilities into passes makes them testable in isolation — one fixture per pass.

val g = testGraph { /* build nodes + ports */ }

// assert a selector emits exactly these arcs, in order
StructuralSelector.assertEmits(from = g, expected = listOf(VisitKind.GraphNode to childNode))

// run a pass and assert findings by stable code
val report = g.compile { +WirePass() }
report.assertFinding("wiring.ambiguous", Severity.Error)

Acceptance bar: RouteIndexPass finds every route in its declared revision/feature-input scope; wiring yields a deterministic report and an all-or-nothing definition patch; repeating a query against the same vector yields identical IDs/order; bounded workbench self-inspection terminates. A Kotlin/TS pair binds only when the versioned schema and protocol requirements are compatible, and a mismatch cannot fall through to an erased local match. Add tests for incomplete coverage, cursor gaps, stale revisions, invalidation, cross-source cuts, rejected patches, actor quiescence and failed activation with in-flight requests.

Where it lives & build order

Package layout — the generic substrate in reaktor-graph-port, everything graph-aware in reaktor-graph:

reaktor-graph-port/.../portgraph/visitor/
  Visitable.kt  VisitArc.kt  VisitContext.kt
  Selector.kt (+ combinators)  ReactiveSelector.kt
  Traversal.kt (DepthFirst, BreadthFirst, Topological)  SuspendTraversal.kt
  Visitor.kt  SuspendVisitor.kt  Attributes.kt

reaktor-graph/.../graph/visitor/
  GraphSelectors.kt (Structural, Route, Navigation, PortConnectivity, Active,
                     Workflow, ModuleContract, Supervision)
  ReaktorVisitor.kt  GraphPass.kt (+ PassContext, dependsOn)  GraphCompiler.kt
  GraphPatch.kt  PortMatcher.kt  Finding.kt  IncrementalVisitor.kt
  ReactivePass.kt  ScheduledGraphPass.kt  GraphInstallation.kt

reaktor-graph/.../graph/passes/
  WirePass · RequireFullyWiredPass · RouteIndexPass · GraphJsonPass · GraphDocumentPass
  DeepLinkInstallPass · AnalyticsInstallPass · AuthCoveragePass · WorkflowPlanPass
  ModuleContractPass · CrossGraphReachabilityPass · SourceMapPass

Proposed build order (compatibility must be demonstrated at each step):

  1. Freeze the current Engine graph-document contract with snapshot tests for the BestBuds and Engine graphs.
  2. Land snapshot identity/vector, coverage and query-budget contracts alongside VisitArc, VisitKind, VisitContext, VisitDecision, Attributes, PassKey and PassContext; adapt old neighbor selectors. Resolve canonical endpoint roles and the BFS exit contract.
  3. Land result-producing bounded traversals and immutable snapshot visitors with stable ordering, cancellation, consistency tests and authority-filtered coverage.
  4. Land typed patches, whole-patch validation and revision conflict checks; separately implement runtime prepare/activate/reconcile. Make WirePass emit a proposal and adapt legacy wiring only after its semantics are verified.
  5. Land GraphDocumentPass (introspection-filtered) behind reaktorGraphDocument(appId) with byte-parity snapshot tests; then RouteIndexPass/DeepLinkInstallPass/AuthCoveragePass.
  6. Land Graph.compile { } (DAG + PassContext), ReactivePass/AnalyticsInstallPass, IncrementalVisitor for the workbench panels.
  7. Land ModuleContractPass/CrossGraphReachabilityPass, PortMatcher ContractId, and product bundles (BestBudsRuntimePasses for installs, EngineSnapshotPasses for read-only export).

Open questions

  • Patch conflict. Two passes in one compile emit patches touching the same port — conflicts should surface as a patch.conflict finding (the multi-agent case from the control plane needs this).
  • Reactive back-pressure. Choose retention, replay and overflow behavior by stream kind. Latest-state display conflation is allowed; control/audit/event streams need loss accounting, bounded back-pressure and a recovery cursor.
  • Federated snapshot mechanism. Choose how sources provide versioned snapshots and consistent cuts. Per-collection copies and best-effort scans may serve an explicitly partial display, never an exact or policy query.
  • Variance scope. Should covariant/contravariant matching be per-WirePass or a per-port attribute so one graph can mix strict and permissive?
  • Source-map storage. Where does SourceMapStore live — reaktor-io, reaktor-compiler output, or Engine-only?
  • Module validation timing. Compile-phase only, or re-validated on each Attaching transition?
  • Graph Kernel - Use to keep the core language small while modules add vocabulary.
  • Graph Sources - Use for source provenance, coverage, identity reconciliation and action applicability independent of provenance.
  • Deep Links - Use before adding route-driven external entry points.
  • Analytics + Telemetry - Use before adding app analytics events, telemetry spans, signal sinks, dashboards, privacy lint, or graph-linked Insights panels.
  • Unified Graph - Use before building Reaktor Engine graph importers, cloud/resource nodes, operation providers, or MCP graph tools.
  • Graph Migration - Use when evolving reaktor-graph without breaking product apps.

References

  1. Graph Kernel Analysis — the primitive vocabulary (Snapshot/Event/Command/Visitor) and the ContractId polyglot rule visitors honor.
  2. Graph Refinement Plan — the transactional Wiring machinery patches commit through, full-KType ports, and the module contract layer.
  3. The Control Plane — the largest consumer: GraphDocumentPass snapshots + findings + GraphPatch mutation are its read/change spine.
  4. Deep Link Integration — RouteIndexPass + DeepLinkInstallPass in full.
  5. Performance & Observability — instrument passes and the PerformanceOverlayVisitor.
  6. Actor Model — SupervisionSelector walks the supervision tree.

Code anchors: reaktor-graph-port/.../portgraph/visitor/Visitor.kt and reaktor-graph/.../graph/visitor/Visitor.kt (current Selector/Traverser/Visitor), reaktor-graph/.../core/GraphJson.kt (the export this generalizes), and Graph.autoWire() (the wiring this replaces with WirePass).