reaktor-capability

The missing runtime plane that turns raw platform facts, permissions, network quality, power state, renderer constraints, and measured app behavior into normalized capability profiles and strategy decisions. Product code should ask for a capability tier, not branch on Android, iOS, browser, Worker, JVM, model strings, or one-off adapter checks.

Verdict

Build reaktor-capability as a small KMP runtime module, not as another platform adapter collection and not as an expansion of reaktor-core. It should own normalized environment snapshots, capability profiles, policy evaluation, and strategy selection. Existing adapters keep doing privileged work; capability selects which adapter behavior is currently allowed and worth using.

Keep the API small

Expose profiles, requirements, strategies, and decisions. Do not create giant one-shot APIs like uploadSmartly() or renderBestUi().

Normalize for privacy

Bucket model strings, hardware facts, user-agent values, and network hints into coarse classes. Raw identifiers must not become business logic switches.

Make degradation visible

Every fallback should be inspectable in reaktorDesktop, traceable in telemetry, replayable in shadow, and testable with synthetic snapshots.

Source scan

The scan covered the spec chapter in /tmp/reaktor-spec.md, the docs in bestbuds/targets/reaktorWeb/public/docs, the Reaktor core and graph modules, platform adapters, performance/work/cloud modules, and the current reaktorDesktop workbench surfaces.

SourceRelevant findingDesign implication
/tmp/reaktor-spec.md chapter 20Defines the target vocabulary: EnvironmentSnapshot, normalized profiles, collectors, measured telemetry, tiers, and strategy policies.This doc treats the spec as the intended direction and turns it into an implementation plan.
reaktor-coreCapability is a minimal AutoCloseable lifecycle primitive. Feature is a process-level slot registry. Adapter bridges platform controllers through weak references.Do not overload existing graph/runtime capabilities. Add a new runtime module and install it through a Feature.Capability slot.
reaktor-graph and reaktor-graph-portGraph composes lifecycle, dependency, concurrency, navigation, and port capabilities. Ports already make dependencies explicit and inspectable.Capability requirements should attach to graph nodes, ports, and facets, then be interpreted by visitors and the workbench.
Platform adaptersPermissionAdapter, CameraAdapter, LocationAdapter, TaskManager, PubSubAdapter, Cloudflare bindings, and telemetry adapters already model platform-specific power.Capability should select strategy and tier around these adapters, not replace them.
Existing docsGraph Kernel calls capability/environment a canonical runtime plane. Notifications explicitly blocks on capability tiers. React x Compose needs signed, capability-scoped dynamic UI bundles.The first milestone should unblock notifications and make capability visible to graph tooling.
reaktor-tactileThe module exists but is currently a placeholder.Tactile should consume capability decisions for haptics, shaders, animation intensity, reduced motion, and renderer strategy.
reaktor-performancePerformance models already cover app vitals, web vitals, server vitals, flamegraph frames, tool runs, and budget enforcement.Measured signals should feed capability decisions, and capability decisions should be included in perf reports.
reaktorDesktop workbenchThe workbench has EnvironmentState, graph catalogs, graph overlays, command queue state, and an inspector, but no first-class capability plane.Add a visual capability plane that overlays requirements, tiers, fallbacks, collector health, and strategy decision traces on the existing graph.

Concept boundaries

The word capability already appears in the codebase. The new module needs strict language so it does not become a grab bag.

ConceptQuestion answeredExamplesOwner
Feature slotWhich process-level service or adapter is installed?Feature.Auth, Feature.Database, Feature.Permission, Feature.Work.reaktor-core
Runtime mixin capabilityWhich behavior does this graph/node object compose?Lifecycle, concurrency, DI, navigation, port capability.reaktor-core, reaktor-graph, reaktor-graph-port
PermissionHas the OS, browser, platform, or user granted a privileged action?Camera, location, gallery, notifications, storage.Adapters and collectors
EnvironmentWhat coarse execution conditions exist right now?Performance class, network quality, power mode, thermal state, renderer, foreground/background.reaktor-capability
Capability profileCan this runtime perform this operation, and at what quality tier?Push notification full, local notification basic, camera still fallback, socket unavailable.reaktor-capability
StrategyWhich implementation should run under the current snapshot and policy?Remote push vs local schedule, WebSocket vs polling, multipart upload vs queued outbox.reaktor-capability plus owning module

Public model

The public API should be expressive enough to compose strategies, but small enough that feature modules remain readable. The main flow is request -> profile -> decision.

package dev.shibasis.reaktor.capability

import dev.shibasis.reaktor.core.framework.CreateSlot
import dev.shibasis.reaktor.core.framework.Feature
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable

@Serializable
value class CapabilityId(val value: String)

@Serializable
enum class CapabilityTier {
    Unavailable,
    Fallback,
    Basic,
    Standard,
    Full,
}

@Serializable
data class CapabilityRequest(
    val id: CapabilityId,
    val purpose: String,
    val requiredTier: CapabilityTier = CapabilityTier.Fallback,
    val preferredTier: CapabilityTier = CapabilityTier.Full,
    val attributes: Map<String, String> = emptyMap(),
)

@Serializable
data class CapabilityProfile(
    val id: CapabilityId,
    val tier: CapabilityTier,
    val available: Boolean,
    val reason: DegradationReason? = null,
    val requiredPermissions: List<PermissionRef> = emptyList(),
    val strategies: List<CapabilityStrategyDescriptor> = emptyList(),
    val observedAtEpochMs: Long,
)

@Serializable
data class CapabilityDecision(
    val request: CapabilityRequest,
    val profile: CapabilityProfile,
    val selectedStrategyId: String?,
    val tier: CapabilityTier,
    val trace: List<DecisionTraceEntry> = emptyList(),
)

interface CapabilityRuntime : AutoCloseable {
    val environment: StateFlow<EnvironmentSnapshot>
    val profiles: StateFlow<Map<CapabilityId, CapabilityProfile>>
    val events: Flow<CapabilityRuntimeEvent>

    suspend fun refresh(): EnvironmentSnapshot
    fun profile(id: CapabilityId): StateFlow<CapabilityProfile>
    suspend fun resolve(request: CapabilityRequest): CapabilityDecision
}

var Feature.Capability by CreateSlot<CapabilityRuntime>()

Environment snapshots

Public snapshots must expose normalized buckets only. Raw model strings, exact CPU names, precise device identifiers, user-agent values, and vendor-specific details are not product logic inputs.

@Serializable
data class EnvironmentSnapshot(
    val device: DeviceProfile,
    val execution: ExecutionProfile,
    val network: NetworkProfile,
    val renderer: RendererProfile,
    val storage: StorageProfile,
    val permissions: PermissionProfile,
    val observed: ObservedQualityProfile,
    val version: Long,
    val createdAtEpochMs: Long,
)

@Serializable
data class DeviceProfile(
    val platform: PlatformFamily,
    val formFactor: FormFactor,
    val performanceClass: PerformanceClass = PerformanceClass.Unknown,
    val memoryClass: MemoryClass = MemoryClass.Unknown,
    val pointerClass: PointerClass = PointerClass.Unknown,
)

@Serializable
data class ExecutionProfile(
    val appState: AppState,
    val powerMode: PowerMode = PowerMode.Normal,
    val thermalClass: ThermalClass = ThermalClass.Normal,
)

@Serializable
data class NetworkProfile(
    val connectivity: ConnectivityState,
    val transports: Set<NetworkTransport> = emptySet(),
    val cost: NetworkCost = NetworkCost.Unknown,
    val quality: NetworkQuality = NetworkQuality.Unknown,
    val rttMsHint: Double? = null,
    val downlinkMbpsHint: Double? = null,
)

Stable public buckets

PerformanceClass, MemoryClass, NetworkQuality, NetworkCost, PowerMode, ThermalClass, FormFactor, RendererClass, PermissionState.

Private raw facts

Platform APIs, browser hints, user-agent values, hardware metrics, OS permission payloads, and cloud binding metadata. Convert them to buckets, then drop or redact them.

Runtime architecture

The runtime has three layers: collectors gather raw signals, the normalizer converts them into one EnvironmentSnapshot, and the strategy engine resolves module-defined capability strategies.

Platform collectors             Common runtime                 Module strategies
-------------------             --------------                 -----------------
DeviceCollector          \
PermissionCollector       \
NetworkCollector           +-> EnvironmentNormalizer -> EnvironmentSnapshot
PowerThermalCollector     /
RendererCollector        /
ObservedTelemetryCollector
                                      |
                                      v
                              CapabilityRegistry
                                      |
                                      v
CapabilityRequest -> StrategySelector -> CapabilityDecision -> module executes selected behavior

Collector contracts

interface CapabilitySignalCollector<Signal : Any> : AutoCloseable {
    val id: String
    val updates: Flow<Signal>
    suspend fun collect(): Signal
}

interface EnvironmentNormalizer {
    fun normalize(
        previous: EnvironmentSnapshot?,
        signals: CapabilitySignalSet,
        observed: ObservedQualityProfile,
    ): EnvironmentSnapshot
}

interface CapabilityRegistry {
    fun registerProfileProvider(provider: CapabilityProfileProvider)
    fun registerStrategy(strategy: CapabilityStrategy<*, *>)
    suspend fun resolve(request: CapabilityRequest, snapshot: EnvironmentSnapshot): CapabilityDecision
}
Signal classExamplesDecision timescale
Static or slow-changingPlatform family, form factor, approximate performance class, renderer support, installed cloud/runtime bindings.Route start, graph attach, default UI density, default media strategy.
Dynamic runtimePermission grants, foreground/background, connectivity, network cost, power saver, thermal state, storage pressure.Ongoing adaptation: reduce animation, queue work, switch socket mode, downgrade media quality.
Measured app qualityBackend RTT, failure rate, dropped frames, long tasks, queue backlog, last successful upload, provider error class.Real quality estimation when platform hints are absent, stale, or wrong.

Strategy selection

Strategies are supplied by the module that owns the operation. The capability runtime evaluates requirements against the current snapshot and returns a decision, including why stronger strategies were rejected.

interface CapabilityStrategy<In : Any, Out : Any> {
    val id: String
    val capability: CapabilityId
    val tier: CapabilityTier
    val policy: CapabilityStrategyPolicy

    suspend fun execute(input: In): Out
}

@Serializable
data class CapabilityStrategyPolicy(
    val minPerformanceClass: PerformanceClass = PerformanceClass.Unknown,
    val maxThermalClass: ThermalClass = ThermalClass.Critical,
    val minNetworkQuality: NetworkQuality = NetworkQuality.Offline,
    val allowMetered: Boolean = true,
    val allowExpensive: Boolean = true,
    val allowLowPower: Boolean = true,
    val requiresForeground: Boolean = false,
    val requiredPermissions: List<PermissionRef> = emptyList(),
)

suspend fun sendCampaignReminder(input: CampaignReminder) {
    val decision = Feature.Capability.resolve(
        CapabilityRequest(
            id = CapabilityIds.PushNotification,
            purpose = "campaign-reminder",
            requiredTier = CapabilityTier.Fallback,
        )
    )

    when (decision.tier) {
        CapabilityTier.Full,
        CapabilityTier.Standard -> remotePush.send(input)
        CapabilityTier.Basic -> localNotification.schedule(input)
        CapabilityTier.Fallback -> inboxBadge.mark(input.userId)
        CapabilityTier.Unavailable -> Unit
    }
}

Representative degradation paths

CapabilityFull or standardBasicFallbackUnavailable
Push notificationRemote provider pushLocal notification scheduleInbox badge or in-app bannerSuppress and log decision
Media uploadMultipart direct upload to R2Sequential signed uploadPersist to outbox and retryDisable send action with reason
CameraLive preview and captureStill captureGallery/file pickerManual attachment unavailable state
LocationPrecise live locationCoarse locationManual city or regionLocation-dependent feature hidden
RealtimeWebSocket or Durable Object roomLong pollingQueued sends and manual refreshRead-only offline state
Tactile renderingShaders, springs, advanced hapticsSimple animation and basic hapticsStatic transitionsNo tactile effects

Module integration

The capability runtime should land once and then be adopted by modules where fallback logic already leaks into product code.

ModuleRole in capabilityFirst integration
reaktor-coreProvides Feature, CreateSlot, Adapter, and lightweight lifecycle Capability.Add Feature.Capability extension in the new module, not core.
reaktor-notificationOwns push/local notification profiles and strategies.Implement PushNotification and LocalNotification tiers to unblock notification design phase 1.
reaktor-media and reaktor-locationExpose camera/gallery/location availability from adapters and permissions.Move start failures into profiles: permission failure, missing controller, no hardware, degraded strategy.
reaktor-workConsumes network, power, foreground, and storage profiles for scheduling constraints.Map work constraints to capability policies and expose worker eligibility decisions.
reaktor-performanceFeeds measured quality and receives decision samples.Add capability decision metrics, collector budgets, and degradation counts to reports.
reaktor-telemetryObserves capability events as spans and attributes.Span names: capability.refresh, capability.resolve, capability.strategy.reject.
reaktor-cloudflare and reaktor-googleReport runtime/binding availability and backend quality signals.Profiles for Worker runtime, Durable Object state, R2 object storage, Pub/Sub, Hyperdrive, and service bindings.
reaktor-tactileConsumes renderer, haptic, accessibility, power, thermal, and frame-budget signals.Define TactileRuntimePolicy from EnvironmentSnapshot and capability decisions.
reaktor-graphDeclares and visualizes requirements on graph nodes/ports/facets.Add capability requirement facets and visitor passes after v0 runtime is stable.

Visual plane in reaktorDesktop

The workbench already has the right shell: environment state, graph catalog, graph pane overlays, side inspector, bottom drawer, command queue, performance panes, and source-opening workflows. The new work is to add a capability plane that projects capability requirements and live decisions onto the graph.

What the visual plane should show

Graph overlay

  • Color nodes by strongest required capability tier.
  • Badge nodes with active degradation count.
  • Draw capability requirement edges without creating fake runtime edges.
  • Overlay provider health for adapters, Worker bindings, Pub/Sub, R2, and local device services.

Inspector panel

  • Selected node requirements.
  • Current profile and selected strategy.
  • Rejected strategies with reasons.
  • Permission state and next user action.
  • Last measured quality and last successful execution.

Environment controls

  • Live, simulated, and shadow snapshot sources.
  • Device target, network quality, power mode, thermal class, permission matrix.
  • One-click scenario presets: offline commute, low-end Android, hot device, metered network, Worker binding missing.

Decision timeline

  • Capability refresh events.
  • Strategy upgrades and downgrades.
  • Fallback execution counts.
  • Links to traces, performance samples, source, tests, and shadow replay capsules.

Workbench data additions

@Serializable
data class WorkbenchCapabilityPlane(
    val snapshot: EnvironmentSnapshot,
    val summaries: List<WorkbenchCapabilitySummary>,
    val selectedDecision: CapabilityDecision? = null,
    val collectorHealth: List<CollectorHealth> = emptyList(),
)

@Serializable
data class WorkbenchCapabilitySummary(
    val capabilityId: String,
    val tier: CapabilityTier,
    val requiredByGraphNodeIds: List<String>,
    val selectedStrategyId: String?,
    val degradationReason: String?,
)

@Serializable
data class WorkbenchCapabilityOverlay(
    val graphNodeId: String,
    val requiredCapabilities: List<String>,
    val activeDegradations: Int,
    val worstTier: CapabilityTier,
)

Implementation should extend the existing EnvironmentState instead of replacing it. The current fields such as project, environment, device target, locale, theme mode, and network mode become UI controls that synthesize an EnvironmentSnapshot for simulation and shadow runs. The existing WorkbenchGraphCatalog can add capability overlay metadata without changing graph rendering internals.

Build phases

P0 · Vocabulary and golden snapshots

Create the common model package, normalized enums, fixture snapshots, privacy rules, and sample decision traces. Include Android, iOS, web, Worker, JVM server, and desktop examples.

P1 · Runtime module v0

Add :reaktor-capability to Gradle. Implement CapabilityRuntime, no-op/default collectors, Feature.Capability, profile registry, strategy registry, and deterministic selector tests.

P2 · First collectors

Land platform, permission, network, execution, renderer, storage, and measured-quality collectors. Keep raw signal APIs internal and publish only normalized snapshots.

P3 · Notifications and Tactile adoption

Implement push/local notification strategies and the first TactileRuntimePolicy. This proves strategy selection across product-visible features and UI adaptation.

P4 · Media, location, work, and cloud profiles

Move existing adapter failure states into profiles. Add R2, Worker, Durable Object, Pub/Sub, and server/runtime profiles where they materially affect strategy.

P5 · Graph requirements and workbench plane

Add capability requirement facets, visitor output, workbench overlays, inspector panel, synthetic snapshot controls, and command-queue edits for policy/scenario changes.

P6 · Telemetry, shadow, and perf loop

Emit OTel spans, include capability decisions in perf reports, replay shadow traffic against synthetic snapshots, and fail CI on unsupported fallback regressions or hot-path budget violations.

Testing and performance

This module is a decision engine, so tests need to focus on determinism, privacy, and hot-path cost.

Test typeWhat it provesExamples
Golden snapshot testsRaw signals normalize consistently across targets.Low-end Android on metered LTE, iOS low power mode, browser missing network hints, Worker with R2 but no Hyperdrive.
Strategy selector testsFallback and upgrade behavior is deterministic and explainable.Remote push rejected for permission denied, upload queued for offline, shaders disabled for hot thermal state.
Privacy testsPublic snapshots do not expose raw fingerprinting material.No model strings, raw user agents, exact CPU names, vendor IDs, or unique hardware scores.
Collector contract testsCollectors are async, bounded, and safe when platform APIs are missing.Browser lacks navigator.connection, Android permission adapter missing, Cloudflare binding absent.
Workbench visual testsThe capability plane matches decisions and no UI labels overlap.Select degraded node, inspect rejected strategies, switch to offline preset, verify overlay updates.
Performance harnessesDecision resolution is cheap and collectors do not run on hot recomposition paths.Benchmark resolve(), profile refresh fan-out, enforce collector timeout budgets.

Runtime constraints

  • resolve() should be pure against the latest snapshot and registry state, with no platform probing in the hot path.
  • Collectors emit deltas into a conflated flow; the normalizer produces versioned immutable snapshots.
  • Decision caches should key by request identity, snapshot version, and policy version.
  • Expensive probes must be explicit, asynchronous, budgeted, and observable.
  • Telemetry should record normalized tiers and reasons, not raw device facts.

Open decisions

Graph dependency boundary

Keep v0 graph-free, then add graph requirement facets through reaktor-graph or a small integration module. Avoid a hard graph dependency in the core capability runtime.

Policy source of truth

Decide whether product policy lives in code, serialized graph facets, server-configured manifests, or all three with clear precedence.

Permission prompts

Capability should describe required permissions and next actions, but the prompt UX should remain owned by product/platform modules.

Server capability shape

Client capability is mostly device/environment. Server and Worker capability also includes bindings, regional reachability, queue health, and provider quota.

Decision history retention

Workbench and telemetry need enough history for debugging, but retention must avoid turning normalized signals back into fingerprinting material.

Generated UI grants

React/Compose dynamic UI bundles should carry capability manifests. The runtime should reject undeclared native calls and surface missing grants in the workbench.

  • Core - Use before touching the platform boundary: registering a Feature slot, writing a platform Adapter, or branching on a device/network capability (which must be a normalized tier, never a raw model string).
  • Graph Sources - Use when planning how existing non-Reaktor code enters the system graph: source scanning, runtime observation, ServiceNode wrapping, source navigation, and action gating by trust level.
  • UI & Design System - Use before touching reaktor-ui tokens/components/theming, designing the tactile runtime (interaction/physics/field/skins/host-tree/reconciler), or planning adaptive layouts and AI/server-driven UI.
  • Perf and Observability - Use before optimizing or adding instrumentation.
  • Server - Use before adding any production backend primitive (resilience, messaging, data patterns) or designing the Spring/Workers server runtime.