Introduce the Nenjim component registry and move runtime composition into its manager #76

Open
opened 2026-08-26 17:05:40 +02:00 by minimons · 0 comments
Owner

Repository baseline

Implement this issue against:

  • Repository: https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref.git
  • Branch: 0.1-dev
  • Baseline commit: b230e15ecea76babee31c73019430ca5d7976bb2
  • Related roadmap issue: #73
  • Journal Service reference implementation: issue #75

Gitea is authoritative. Verify the baseline commit through the Gitea API before starting, fetch the current 0.1-dev branch, and resolve the commit from that branch. Do not rely on GitHub or an older local checkout.

Summary

Introduce the first small version of NenjimRegistryService and move construction, registration, dependency composition, and startup of the current Nenjim components out of NenjimHubImpl and into NenjimRegistryServiceManagerImpl.

The fundamental term is component, not plugin. A component is any finished Nenjim LEGO brick that is constructed and ready for use. Whether a component is treated as a plugin, service, application, algorithm, adapter, or something else depends on how an application uses it.

This first version is deliberately based on one Registry. Contexts, version resolution, class loading, dynamic discovery, runtime loading, and component removal are separate later tasks.

Conceptual model

Nenjim is a box of completed LEGO bricks. Applications are assembly instructions that select and connect some of the available bricks.

The responsibilities are:

  • NenjimRegistryServiceManagerImpl constructs the Registry and the currently available hardcoded components.
  • The manager registers each component with a NenjimComponentId.
  • NenjimRegistryService is a read-only catalogue used by ordinary consumers.
  • An application receives NenjimRegistryService through constructor injection.
  • An application uses the Registry to locate the components it needs.
  • The application, not the Registry, knows its own configuration and decides which component IDs to select.
  • The application decides selection, ordering, wiring, and activation.
  • Multiple applications may select different subsets of the same Registry contents.
  • Multiple IDs may deliberately refer to the same object instance.
  • The Registry does not track users, usage counts, activation state, or ownership of a component's active lifecycle.
  • Component IDs are lookup identities in one Registry. They are not authorization boundaries and do not yet include Context or artifact-version semantics.

Terminology

Use component consistently in the new API, implementation, documentation, diagnostics, variables, and method names.

Do not introduce NenjimPlugin or plugin-specific Registry terminology. Existing packages that currently contain plugins should be renamed where this issue directly touches them, as described below.

Public component contracts

NenjimComponent

Add a pure marker interface:

package com.r35157.nenjim.component;

public interface NenjimComponent {
}

The component ID must not be a property of this interface or of the component object. It is registration metadata owned by the Registry. This permits the same object instance to be registered under more than one ID.

NenjimApplication

Add the common startable-component contract:

package com.r35157.nenjim.component;

public interface NenjimApplication extends NenjimComponent {
    void start() throws Exception;
}

There is no common stop() operation in this version.

A service implementation may also be an application. Its service interface should remain focused on its domain API, while the concrete implementation additionally implements NenjimApplication when it has an active lifecycle.

For example, NenjimRegistryServiceImpl is both the Registry service implementation and a startable Nenjim application.

Component ID

Add this public value type:

package com.r35157.nenjim.service.registry.valuetypes;

import org.jetbrains.annotations.NotNull;

public record NenjimComponentId(@NotNull String value) {
    public NenjimComponentId {
        value = value.strip();

        if (!value.matches(
                "[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*")) {
            throw new IllegalArgumentException(
                    "Invalid Nenjim component ID: '" + value + "'");
        }
    }
}

Required behavior:

  • Reject a null value with NullPointerException.
  • Apply String.strip() before validation and store the stripped value.
  • The canonical format is lower-case ASCII, dot-separated segments, with optional internal hyphens.
  • The first character of every dot-separated segment must be a lower-case ASCII letter.
  • Upper-case letters, underscores, whitespace inside the ID, empty segments, leading or trailing dots, and malformed hyphens are invalid.
  • IDs such as nenjim.registry.service, assetaz.price-source.raydium-pool, and evelyn.service.prod are valid.
  • Inputs such as " nenjim.registry.service " canonicalize to nenjim.registry.service.
  • The record's standard value-based equality and hash code define ID equality.

Read-only Registry API

Add:

package com.r35157.nenjim.service.registry;

import com.r35157.nenjim.component.NenjimComponent;
import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

public interface NenjimRegistryService extends NenjimComponent {
    @NotNull
    List<NenjimComponentId> getComponentIds(
            @NotNull Class<? extends NenjimComponent> componentInterface);

    @Nullable
    <T extends NenjimComponent> T getComponent(
            @NotNull NenjimComponentId componentId,
            @NotNull Class<T> expectedInterface);
}

Both lookup methods intentionally begin with getComponent so that IDE autocomplete exposes them together.

getComponentIds(...)

Required behavior:

  • The argument represents a component interface, not a concrete implementation class.
  • Reject null with NullPointerException.
  • Reject a class token that is not an interface extending NenjimComponent with IllegalArgumentException.
  • Return the IDs of all registered components that implement the requested interface.
  • Preserve component registration order.
  • Return an immutable snapshot. Later registrations must not mutate a list that was already returned.
  • Return an empty immutable list when no components implement the interface.
  • Never return null.

getComponent(...)

Required behavior:

  • Reject either null argument with NullPointerException.
  • Reject expectedInterface when it is not an interface extending NenjimComponent with IllegalArgumentException.
  • Return null if the ID is not registered.
  • Return the registered object cast to the requested interface when it implements that interface.
  • If the ID exists but its object does not implement the requested interface, throw IllegalArgumentException. This is a caller programming error, not a missing component.
  • The diagnostic must include the component ID, requested interface name, and actual implementation class name.

An ordinary consumer must not be able to register or remove components through NenjimRegistryService.

Internal Registry administration

Add a package-private interface in the reference implementation package:

package com.r35157.nenjim.service.registry.impl.ref;

import com.r35157.nenjim.component.NenjimComponent;
import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId;
import org.jetbrains.annotations.NotNull;

interface NenjimRegistryServiceAdmin {
    void registerComponent(
            @NotNull NenjimComponentId componentId,
            @NotNull NenjimComponent component);
}

This is an internal implementation handle used by NenjimRegistryServiceManagerImpl. It must not extend NenjimComponent or NenjimApplication, and it must not be indexed as a discoverable component interface.

Java requires the implementation method to be public, but the implementation class and admin interface remain package-private. Ordinary consumers should only receive the public NenjimRegistryService view.

Required registration behavior:

  • Reject a null ID or component with NullPointerException.
  • Registering an already used ID must throw IllegalArgumentException.
  • The duplicate diagnostic must contain the duplicated ID and both the existing and attempted implementation class names.
  • Registering the same object instance under a different unused ID is valid.
  • The Registry guarantees ID uniqueness, not object uniqueness.
  • A component is registered once for each ID. The Registry itself indexes that one registration under every component interface implemented by the object.
  • Registration must not start or stop the component.

No removal operation is included in this issue.

Registry implementation

Add package-private NenjimRegistryServiceImpl:

final class NenjimRegistryServiceImpl implements
        NenjimRegistryService,
        NenjimRegistryServiceAdmin,
        NenjimApplication {
    // ...
}

The Registry implementation is one object with multiple views. Register that object only once, using one component ID. Its one registration must be returned through both of its discoverable component interfaces:

NenjimRegistryService serviceView =
        registryService.getComponent(
                new NenjimComponentId("nenjim.registry.service"),
                NenjimRegistryService.class);

NenjimApplication applicationView =
        registryService.getComponent(
                new NenjimComponentId("nenjim.registry.service"),
                NenjimApplication.class);

assert serviceView == applicationView;

Do not register the object separately for each interface.

Storage and type index

Use two logical structures:

  1. A primary ID-to-object map.
  2. A secondary interface-to-ordered-ID-list index.

The type index is required so getComponentIds(interface) does not scan every registered object on every call.

When registering an object, recursively inspect its implemented interface hierarchy and index the ID under every interface that extends NenjimComponent. This must include inherited component interfaces, not only interfaces directly declared by the implementation class.

Do not index:

  • Concrete classes.
  • Interfaces that do not extend NenjimComponent.
  • The package-private NenjimRegistryServiceAdmin interface.

The implementation may use immutable snapshots, copy-on-write data, synchronization, or another simple thread-safe approach. Reads and registrations must remain internally consistent if registration is added after startup. The Registry must not be permanently sealed by start().

Registry lifecycle

NenjimRegistryServiceImpl.start() is single-use:

  • The first call transitions the Registry to started and enables queries.
  • Any later call throws IllegalStateException.
  • This also applies when the first start attempt failed: a failed instance is not restarted.
  • A query before successful startup throws IllegalStateException.
  • Internal registration is allowed both before and after successful startup.
  • start() does not construct, register, start, or stop any other component.

Registry manager API

Add:

package com.r35157.nenjim.service.registry;

import com.r35157.nenjim.component.NenjimComponent;

public interface NenjimRegistryServiceManager extends NenjimComponent {
    void start() throws Exception;
}

Do not add getRegistryService(). Applications receive NenjimRegistryService through constructor injection and must use that instance for lookups.

The public manager interface has no registration, removal, resolver, class-loading, or stop operation in this version.

Add public NenjimRegistryServiceManagerImpl. It must implement both NenjimRegistryServiceManager and NenjimApplication so the outer bootstrap can treat it like any other startable Nenjim component.

The manager and Registry service are two different objects.

Manager lifecycle

NenjimRegistryServiceManagerImpl.start() is single-use:

  • The first call creates and starts the Registry, registers the complete hardcoded component set, and starts the explicit hardcoded application subset.
  • Any later call throws IllegalStateException.
  • This also applies when the first start attempt failed: a failed manager instance is not restarted.

The manager is responsible for calling registerComponent(...). Individual components must not be required to self-register.

Required bootstrap order

The manager must perform these operations in this order:

  1. Construct NenjimRegistryServiceImpl while retaining separate NenjimRegistryService and NenjimRegistryServiceAdmin views of the same object.
  2. Call start() directly on the Registry application's NenjimApplication view.
  3. Register the Registry object first, using nenjim.registry.service.
  4. Register the manager object second, using nenjim.registry.service-manager.
  5. Construct and register the remaining hardcoded components in dependency order.
  6. Start only the explicitly enabled application subset, in the current dependency-safe order.
  7. Preserve the current online wait/blocking behavior after startup.

After startup, Registry lookups must therefore be able to retrieve:

  • The Registry object as NenjimRegistryService.
  • The same Registry object as NenjimApplication.
  • The manager object as NenjimRegistryServiceManager.
  • The same manager object as NenjimApplication.

The Registry and manager objects must not be the same instance.

Bootstrap class

NenjimHub becomes only the outer Java bootstrap class. It is not a Registry component and does not implement NenjimApplication.

Move the entry point out of com.r35157.nenjim.hubd.impl.ref.Main and use this shape:

package com.r35157.nenjim.hubd;

import com.r35157.nenjim.component.NenjimApplication;
import com.r35157.nenjim.service.registry.impl.ref.NenjimRegistryServiceManagerImpl;
import org.jetbrains.annotations.NotNull;

public final class NenjimHub {
    private NenjimHub() {
    }

    public static void main(@NotNull String[] args) throws Exception {
        NenjimApplication registryServiceManager =
                new NenjimRegistryServiceManagerImpl();
        registryServiceManager.start();
    }
}

Update the Gradle application main class accordingly.

After all current construction and startup responsibility has moved to the Registry manager, remove the old Main and NenjimHubImpl bootstrap/composition implementation. Do not preserve a second composition path.

Migrate the current hardcoded composition

Move the construction currently performed by NenjimHubImpl into NenjimRegistryServiceManagerImpl without changing the concrete production/test choices or active startup subset.

Use explicit constants for component IDs. The following IDs are the required initial bindings:

Component ID Registered component view Current implementation or role
nenjim.registry.service NenjimRegistryService and NenjimApplication NenjimRegistryServiceImpl
nenjim.registry.service-manager NenjimRegistryServiceManager and NenjimApplication NenjimRegistryServiceManagerImpl
nenjim.object-cache.default ObjectCache ObjectCacheImpl
assetaz.currency-identity.hardcoded CurrencyIdentityService HardcodedCurrencyIdentityService
solana.blockchain.direct SolanaBlockChain SolanaBlockChainImpl
solana.blockchain.cached SolanaBlockChain CachedSolanaBlockChain
raydium.service.default Raydium RaydiumImpl
solana.wallet.evelyn-perps-test SolanaWallet Existing Evelyn perps test wallet instance
solana.wallet.evelyn-perps-prod SolanaWallet Existing Evelyn perps production wallet instance
solana.wallet.evelyn-burner-prod SolanaWallet Existing Evelyn burner production wallet instance
assetaz.price-source.hardcoded PriceSource HardcodedPriceSource
assetaz.price-source.raydium-pool.eve-usdt PriceSource Existing EVE/USDT RaydiumPoolPriceSource
assetaz.ticker.default TickerService and NenjimApplication TickerServiceImpl
jupiter.perps.evelyn-prod JupiterPerpsService Production AnchorIdlJupiterPerpsServiceImpl
jupiter.perps.evelyn-test JupiterPerpsService Test AnchorIdlJupiterPerpsServiceImpl
jupiter-perps-alarm.default JupiterPerpsAlarm and NenjimApplication JupiterPerpsAlarmImpl
evelyn.service.prod Evelyn and NenjimApplication Production EvelynImpl
evelyn.service.test Evelyn and NenjimApplication Test EvelynImpl
jupiter.swap.evelyn-burner-prod JupiterSwapService JupiterSwapServiceImpl
notification.discord.assetaz-token BoundNotificationService Existing Discord notification implementation
evelyn.iou-burner.prod EvelynIOUBurnerService and NenjimApplication EvelynIOUBurnerServiceImpl
evelyn.mission-control.default EvelynMissionControl and NenjimApplication EvelynMissionControlImpl

Infrastructure objects and value objects used only while constructing these components, such as Clock, HttpClient, ObjectMapper, and the EVE/USDT TradingPair, do not need Registry bindings in this issue. They are not independently exposed Nenjim components.

Make every interface used as a Registry query type extend NenjimComponent. Do not make records, value types, configuration models, or incidental implementation helpers into components merely because they are constructor dependencies.

Concrete implementations with a no-argument active start() lifecycle must additionally implement NenjimApplication. Do not force a domain service interface to expose lifecycle operations solely for Registry use.

Constructor injection and dependency selection

Pass the public NenjimRegistryService view into non-bootstrap applications/services that need to select other registered components. They must not receive the internal admin view.

Where an application owns selection, express its hardcoded initial configuration as component IDs and resolve the typed objects through:

@Nullable
<T extends NenjimComponent> T getComponent(
        @NotNull NenjimComponentId componentId,
        @NotNull Class<T> expectedInterface);

Fail clearly during construction or startup if a required hardcoded dependency is absent. Do not silently select the first result of an interface query when a specific component ID is required.

In particular, preserve the current Ticker selection of the Raydium EVE/USDT price source. The hardcoded price source remains registered but inactive. The selection belongs to the Ticker composition/configuration, not to the Registry.

Initial startup subset and order

Keep the currently active startup set and dependency order:

  1. assetaz.ticker.default
  2. evelyn.service.prod
  3. evelyn.service.test
  4. evelyn.iou-burner.prod
  5. evelyn.mission-control.default

Keep jupiter-perps-alarm.default constructed and registered but not started, matching the current commented-out startup.

Do not introduce a generic getComponentIds(NenjimApplication.class) start-all loop in this issue. The manager itself and the Registry service also implement NenjimApplication, and some registered applications are intentionally inactive. Use an explicit hardcoded startup list.

Do not activate the currently commented-out Composer, Process Manager, Test Tool, Soda Task Manager, or Suwimo Client applications as part of this migration.

Preserve the current Done - Now online! point and wait/blocking behavior unless a minimal relocation is required by the new class structure.

Package cleanup

Use these packages for the new types:

  • com.r35157.nenjim.component
    • NenjimComponent
    • NenjimApplication
  • com.r35157.nenjim.service.registry
    • NenjimRegistryService
    • NenjimRegistryServiceManager
  • com.r35157.nenjim.service.registry.valuetypes
    • NenjimComponentId
  • com.r35157.nenjim.service.registry.impl.ref
    • NenjimRegistryServiceImpl
    • NenjimRegistryServiceAdmin
    • NenjimRegistryServiceManagerImpl

Rename the directly affected Ticker package from:

com.r35157.assetaz.services.ticker.plugins.pricesource

to:

com.r35157.assetaz.services.ticker.pricesource

Move its implementation subpackages with it and update all imports. This prevents the old plugin terminology from becoming part of the new component design.

Do not perform unrelated large-scale package renames.

Nullability

Follow the repository convention and explicitly annotate every public reference-type parameter, return value, and record component with @NotNull or @Nullable.

This includes public constructors and methods changed as part of dependency injection.

Use @Nullable rather than Optional when absence is a valid model/API result. Specifically, a missing component ID is represented by the @Nullable result of getComponent(...).

Error handling and diagnostics

Use the following exception categories:

  • NullPointerException for explicitly non-null API arguments that are null.
  • IllegalArgumentException for an invalid component ID.
  • IllegalArgumentException for a duplicate component ID.
  • IllegalArgumentException for a non-component or non-interface query type.
  • IllegalArgumentException when an existing ID is requested as the wrong component interface.
  • IllegalStateException for Registry queries before successful Registry startup.
  • IllegalStateException for a second call to either Registry or manager start().

Diagnostics must contain enough detail to identify the ID and involved types. Do not include credentials, private keys, wallet secrets, webhook secrets, or other sensitive constructor data in Registry messages or logs.

Security boundary for this version

This Registry is not an authorization system.

For now:

  • A holder of the Registry can see all registered IDs for a component interface.
  • Component IDs do not grant or deny access.
  • Do not add scopes, permissions, filtered Registry views, ownership, user activation data, or application-specific visibility rules.
  • Keep the Registry instance injected rather than globally static so restricted views can be introduced later without redesigning all consumers.
  • Do not make Registry registration public merely to anticipate future loading.

Runtime mutation boundary

The internal Registry representation must support registrations after startup, because later work will allow an application to ask the manager to bring a new component to life.

That future flow is expected to be:

  1. An application receives NenjimRegistryService through constructor injection.
  2. It obtains NenjimRegistryServiceManager through a normal Registry lookup.
  3. It asks a future public manager method to load/create a component, supplying a component ID plus whatever recipe/version/context information is eventually required.
  4. The manager creates the component and persists enough information to recreate it after reboot.
  5. The manager registers it through the internal admin interface.
  6. The application retrieves it through the read-only Registry API.

Do not add that public manager loading method now. A component ID alone is not enough unless the manager already knows the construction recipe, and resolver, persistence, Context, version, and class-loading semantics have not been designed yet.

Do not seal the Registry or implement a cache that can only be correct when no later registration occurs.

Documentation and OpenSpec

Update relevant Markdown documentation and OpenSpec material so that it consistently describes:

  • The component terminology.
  • The distinction between component ID and component object.
  • Registry read-only consumers versus manager/internal administration.
  • One registration being discoverable through multiple component interfaces.
  • Multiple IDs being allowed to reference the same object.
  • Applications owning component selection and activation.
  • The manager owning construction and registration.
  • The thin NenjimHub.main(...) bootstrap.
  • Contexts, versions, runtime loading, and removal as future work.

Keep the OpenSpec project valid after the change.

Out of scope

Do not implement any of the following in this issue:

  • Multiple Contexts or one Registry per Context.
  • Artifact-version semantics in component IDs.
  • Dependency resolver behavior.
  • NenjimClassLoader integration.
  • Automatic component discovery.
  • Registry contributors.
  • Public runtime component registration/loading.
  • Component removal.
  • Registry persistence or recreation after reboot.
  • Registry events or subscribers.
  • User configuration storage.
  • Automatic component selection.
  • Automatic start/stop of every registered application.
  • Usage counts or ownership tracking.
  • Permissions, scopes, or filtered Registry views.
  • Migration of unrelated legacy applications.
  • A common stop() lifecycle.
  • New unit tests.

Acceptance criteria

  • The implementation is based on the verified current 0.1-dev branch containing commit 8c608a7.
  • NenjimComponent exists as a pure marker interface.
  • NenjimApplication extends NenjimComponent and declares start() throws Exception.
  • NenjimComponentId strips and validates IDs using the specified format.
  • NenjimRegistryService exposes only the two agreed read-only lookup methods.
  • Missing IDs return null from getComponent(...).
  • Existing IDs requested through the wrong interface produce a descriptive IllegalArgumentException.
  • getComponentIds(...) returns immutable registration-order snapshots.
  • A primary ID map and secondary interface-to-ID index are maintained consistently.
  • Recursive inherited component interfaces are indexed.
  • Concrete classes, ordinary interfaces, and the admin interface are not type-index keys.
  • Duplicate IDs fail with IllegalArgumentException.
  • The same object can be registered under multiple different IDs.
  • NenjimRegistryServiceAdmin is package-private and inaccessible to ordinary consumers.
  • NenjimRegistryServiceImpl implements the service, internal admin, and application views.
  • The Registry object is registered once and the same instance is retrievable as both NenjimRegistryService and NenjimApplication.
  • NenjimRegistryServiceManagerImpl is a different object implementing both manager and application views.
  • The manager constructs, starts, and registers the Registry before registering itself and the remaining components.
  • Neither Registry nor manager permits a second start() call, including after a failed first attempt.
  • Registry queries fail before successful Registry startup.
  • Internal registration remains valid after Registry startup.
  • The public manager API does not expose a Registry getter, registration, removal, loading, or stop operation.
  • Applications/services receive the public Registry view through constructor injection where they own component selection.
  • The current Raydium price source remains the Ticker's selected source and the hardcoded source remains inactive.
  • The current active startup subset and ordering are preserved.
  • The alarm and other currently commented applications are not newly activated.
  • NenjimHub.main(...) is the only outer bootstrap and starts a manager typed as NenjimApplication.
  • The old Main and NenjimHubImpl composition path is removed.
  • The Gradle main class points to the new NenjimHub bootstrap.
  • Directly affected Ticker price-source packages no longer use plugins in their names.
  • Public reference types have explicit nullability annotations.
  • Documentation and OpenSpec reflect the new architecture.
  • The project builds successfully with the existing build pipeline.

Validation

Do not add permanent unit tests unless separately requested.

Validate the implementation with:

  1. The existing project build and Detag/Java compilation pipeline.
  2. OpenSpec validation.
  3. Temporary, non-committed probes if useful for confirming:
    • ID stripping and validation.
    • Duplicate-ID rejection.
    • Same-object aliases under different IDs.
    • One registration being retrievable through multiple component interfaces with reference identity preserved.
    • Inherited-interface indexing.
    • Immutable ordered snapshots.
    • Missing-ID and wrong-type behavior.
    • Pre-start queries.
    • Single-use Registry and manager startup.
    • Registration after Registry startup.

Do not use a full production main() run as the primary validation method. The current startup may block indefinitely, open UI, access external services, or perform financially relevant actions.

## Repository baseline Implement this issue against: - Repository: `https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref.git` - Branch: `0.1-dev` - Baseline commit: `b230e15ecea76babee31c73019430ca5d7976bb2` - Related roadmap issue: #73 - Journal Service reference implementation: issue #75 Gitea is authoritative. Verify the baseline commit through the Gitea API before starting, fetch the current `0.1-dev` branch, and resolve the commit from that branch. Do not rely on GitHub or an older local checkout. ## Summary Introduce the first small version of `NenjimRegistryService` and move construction, registration, dependency composition, and startup of the current Nenjim components out of `NenjimHubImpl` and into `NenjimRegistryServiceManagerImpl`. The fundamental term is **component**, not plugin. A component is any finished Nenjim LEGO brick that is constructed and ready for use. Whether a component is treated as a plugin, service, application, algorithm, adapter, or something else depends on how an application uses it. This first version is deliberately based on one Registry. Contexts, version resolution, class loading, dynamic discovery, runtime loading, and component removal are separate later tasks. ## Conceptual model Nenjim is a box of completed LEGO bricks. Applications are assembly instructions that select and connect some of the available bricks. The responsibilities are: - `NenjimRegistryServiceManagerImpl` constructs the Registry and the currently available hardcoded components. - The manager registers each component with a `NenjimComponentId`. - `NenjimRegistryService` is a read-only catalogue used by ordinary consumers. - An application receives `NenjimRegistryService` through constructor injection. - An application uses the Registry to locate the components it needs. - The application, not the Registry, knows its own configuration and decides which component IDs to select. - The application decides selection, ordering, wiring, and activation. - Multiple applications may select different subsets of the same Registry contents. - Multiple IDs may deliberately refer to the same object instance. - The Registry does not track users, usage counts, activation state, or ownership of a component's active lifecycle. - Component IDs are lookup identities in one Registry. They are not authorization boundaries and do not yet include Context or artifact-version semantics. ## Terminology Use `component` consistently in the new API, implementation, documentation, diagnostics, variables, and method names. Do not introduce `NenjimPlugin` or plugin-specific Registry terminology. Existing packages that currently contain `plugins` should be renamed where this issue directly touches them, as described below. ## Public component contracts ### `NenjimComponent` Add a pure marker interface: ```java package com.r35157.nenjim.component; public interface NenjimComponent { } ``` The component ID must not be a property of this interface or of the component object. It is registration metadata owned by the Registry. This permits the same object instance to be registered under more than one ID. ### `NenjimApplication` Add the common startable-component contract: ```java package com.r35157.nenjim.component; public interface NenjimApplication extends NenjimComponent { void start() throws Exception; } ``` There is no common `stop()` operation in this version. A service implementation may also be an application. Its service interface should remain focused on its domain API, while the concrete implementation additionally implements `NenjimApplication` when it has an active lifecycle. For example, `NenjimRegistryServiceImpl` is both the Registry service implementation and a startable Nenjim application. ## Component ID Add this public value type: ```java package com.r35157.nenjim.service.registry.valuetypes; import org.jetbrains.annotations.NotNull; public record NenjimComponentId(@NotNull String value) { public NenjimComponentId { value = value.strip(); if (!value.matches( "[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*")) { throw new IllegalArgumentException( "Invalid Nenjim component ID: '" + value + "'"); } } } ``` Required behavior: - Reject a `null` value with `NullPointerException`. - Apply `String.strip()` before validation and store the stripped value. - The canonical format is lower-case ASCII, dot-separated segments, with optional internal hyphens. - The first character of every dot-separated segment must be a lower-case ASCII letter. - Upper-case letters, underscores, whitespace inside the ID, empty segments, leading or trailing dots, and malformed hyphens are invalid. - IDs such as `nenjim.registry.service`, `assetaz.price-source.raydium-pool`, and `evelyn.service.prod` are valid. - Inputs such as `" nenjim.registry.service "` canonicalize to `nenjim.registry.service`. - The record's standard value-based equality and hash code define ID equality. ## Read-only Registry API Add: ```java package com.r35157.nenjim.service.registry; import com.r35157.nenjim.component.NenjimComponent; import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; public interface NenjimRegistryService extends NenjimComponent { @NotNull List<NenjimComponentId> getComponentIds( @NotNull Class<? extends NenjimComponent> componentInterface); @Nullable <T extends NenjimComponent> T getComponent( @NotNull NenjimComponentId componentId, @NotNull Class<T> expectedInterface); } ``` Both lookup methods intentionally begin with `getComponent` so that IDE autocomplete exposes them together. ### `getComponentIds(...)` Required behavior: - The argument represents a component interface, not a concrete implementation class. - Reject `null` with `NullPointerException`. - Reject a class token that is not an interface extending `NenjimComponent` with `IllegalArgumentException`. - Return the IDs of all registered components that implement the requested interface. - Preserve component registration order. - Return an immutable snapshot. Later registrations must not mutate a list that was already returned. - Return an empty immutable list when no components implement the interface. - Never return `null`. ### `getComponent(...)` Required behavior: - Reject either `null` argument with `NullPointerException`. - Reject `expectedInterface` when it is not an interface extending `NenjimComponent` with `IllegalArgumentException`. - Return `null` if the ID is not registered. - Return the registered object cast to the requested interface when it implements that interface. - If the ID exists but its object does not implement the requested interface, throw `IllegalArgumentException`. This is a caller programming error, not a missing component. - The diagnostic must include the component ID, requested interface name, and actual implementation class name. An ordinary consumer must not be able to register or remove components through `NenjimRegistryService`. ## Internal Registry administration Add a package-private interface in the reference implementation package: ```java package com.r35157.nenjim.service.registry.impl.ref; import com.r35157.nenjim.component.NenjimComponent; import com.r35157.nenjim.service.registry.valuetypes.NenjimComponentId; import org.jetbrains.annotations.NotNull; interface NenjimRegistryServiceAdmin { void registerComponent( @NotNull NenjimComponentId componentId, @NotNull NenjimComponent component); } ``` This is an internal implementation handle used by `NenjimRegistryServiceManagerImpl`. It must not extend `NenjimComponent` or `NenjimApplication`, and it must not be indexed as a discoverable component interface. Java requires the implementation method to be `public`, but the implementation class and admin interface remain package-private. Ordinary consumers should only receive the public `NenjimRegistryService` view. Required registration behavior: - Reject a `null` ID or component with `NullPointerException`. - Registering an already used ID must throw `IllegalArgumentException`. - The duplicate diagnostic must contain the duplicated ID and both the existing and attempted implementation class names. - Registering the same object instance under a different unused ID is valid. - The Registry guarantees ID uniqueness, not object uniqueness. - A component is registered once for each ID. The Registry itself indexes that one registration under every component interface implemented by the object. - Registration must not start or stop the component. No removal operation is included in this issue. ## Registry implementation Add package-private `NenjimRegistryServiceImpl`: ```java final class NenjimRegistryServiceImpl implements NenjimRegistryService, NenjimRegistryServiceAdmin, NenjimApplication { // ... } ``` The Registry implementation is one object with multiple views. Register that object only once, using one component ID. Its one registration must be returned through both of its discoverable component interfaces: ```java NenjimRegistryService serviceView = registryService.getComponent( new NenjimComponentId("nenjim.registry.service"), NenjimRegistryService.class); NenjimApplication applicationView = registryService.getComponent( new NenjimComponentId("nenjim.registry.service"), NenjimApplication.class); assert serviceView == applicationView; ``` Do not register the object separately for each interface. ### Storage and type index Use two logical structures: 1. A primary ID-to-object map. 2. A secondary interface-to-ordered-ID-list index. The type index is required so `getComponentIds(interface)` does not scan every registered object on every call. When registering an object, recursively inspect its implemented interface hierarchy and index the ID under every interface that extends `NenjimComponent`. This must include inherited component interfaces, not only interfaces directly declared by the implementation class. Do not index: - Concrete classes. - Interfaces that do not extend `NenjimComponent`. - The package-private `NenjimRegistryServiceAdmin` interface. The implementation may use immutable snapshots, copy-on-write data, synchronization, or another simple thread-safe approach. Reads and registrations must remain internally consistent if registration is added after startup. The Registry must not be permanently sealed by `start()`. ### Registry lifecycle `NenjimRegistryServiceImpl.start()` is single-use: - The first call transitions the Registry to started and enables queries. - Any later call throws `IllegalStateException`. - This also applies when the first start attempt failed: a failed instance is not restarted. - A query before successful startup throws `IllegalStateException`. - Internal registration is allowed both before and after successful startup. - `start()` does not construct, register, start, or stop any other component. ## Registry manager API Add: ```java package com.r35157.nenjim.service.registry; import com.r35157.nenjim.component.NenjimComponent; public interface NenjimRegistryServiceManager extends NenjimComponent { void start() throws Exception; } ``` Do not add `getRegistryService()`. Applications receive `NenjimRegistryService` through constructor injection and must use that instance for lookups. The public manager interface has no registration, removal, resolver, class-loading, or stop operation in this version. Add public `NenjimRegistryServiceManagerImpl`. It must implement both `NenjimRegistryServiceManager` and `NenjimApplication` so the outer bootstrap can treat it like any other startable Nenjim component. The manager and Registry service are two different objects. ### Manager lifecycle `NenjimRegistryServiceManagerImpl.start()` is single-use: - The first call creates and starts the Registry, registers the complete hardcoded component set, and starts the explicit hardcoded application subset. - Any later call throws `IllegalStateException`. - This also applies when the first start attempt failed: a failed manager instance is not restarted. The manager is responsible for calling `registerComponent(...)`. Individual components must not be required to self-register. ### Required bootstrap order The manager must perform these operations in this order: 1. Construct `NenjimRegistryServiceImpl` while retaining separate `NenjimRegistryService` and `NenjimRegistryServiceAdmin` views of the same object. 2. Call `start()` directly on the Registry application's `NenjimApplication` view. 3. Register the Registry object first, using `nenjim.registry.service`. 4. Register the manager object second, using `nenjim.registry.service-manager`. 5. Construct and register the remaining hardcoded components in dependency order. 6. Start only the explicitly enabled application subset, in the current dependency-safe order. 7. Preserve the current online wait/blocking behavior after startup. After startup, Registry lookups must therefore be able to retrieve: - The Registry object as `NenjimRegistryService`. - The same Registry object as `NenjimApplication`. - The manager object as `NenjimRegistryServiceManager`. - The same manager object as `NenjimApplication`. The Registry and manager objects must not be the same instance. ## Bootstrap class `NenjimHub` becomes only the outer Java bootstrap class. It is not a Registry component and does not implement `NenjimApplication`. Move the entry point out of `com.r35157.nenjim.hubd.impl.ref.Main` and use this shape: ```java package com.r35157.nenjim.hubd; import com.r35157.nenjim.component.NenjimApplication; import com.r35157.nenjim.service.registry.impl.ref.NenjimRegistryServiceManagerImpl; import org.jetbrains.annotations.NotNull; public final class NenjimHub { private NenjimHub() { } public static void main(@NotNull String[] args) throws Exception { NenjimApplication registryServiceManager = new NenjimRegistryServiceManagerImpl(); registryServiceManager.start(); } } ``` Update the Gradle application main class accordingly. After all current construction and startup responsibility has moved to the Registry manager, remove the old `Main` and `NenjimHubImpl` bootstrap/composition implementation. Do not preserve a second composition path. ## Migrate the current hardcoded composition Move the construction currently performed by `NenjimHubImpl` into `NenjimRegistryServiceManagerImpl` without changing the concrete production/test choices or active startup subset. Use explicit constants for component IDs. The following IDs are the required initial bindings: | Component ID | Registered component view | Current implementation or role | | --- | --- | --- | | `nenjim.registry.service` | `NenjimRegistryService` and `NenjimApplication` | `NenjimRegistryServiceImpl` | | `nenjim.registry.service-manager` | `NenjimRegistryServiceManager` and `NenjimApplication` | `NenjimRegistryServiceManagerImpl` | | `nenjim.object-cache.default` | `ObjectCache` | `ObjectCacheImpl` | | `assetaz.currency-identity.hardcoded` | `CurrencyIdentityService` | `HardcodedCurrencyIdentityService` | | `solana.blockchain.direct` | `SolanaBlockChain` | `SolanaBlockChainImpl` | | `solana.blockchain.cached` | `SolanaBlockChain` | `CachedSolanaBlockChain` | | `raydium.service.default` | `Raydium` | `RaydiumImpl` | | `solana.wallet.evelyn-perps-test` | `SolanaWallet` | Existing Evelyn perps test wallet instance | | `solana.wallet.evelyn-perps-prod` | `SolanaWallet` | Existing Evelyn perps production wallet instance | | `solana.wallet.evelyn-burner-prod` | `SolanaWallet` | Existing Evelyn burner production wallet instance | | `assetaz.price-source.hardcoded` | `PriceSource` | `HardcodedPriceSource` | | `assetaz.price-source.raydium-pool.eve-usdt` | `PriceSource` | Existing EVE/USDT `RaydiumPoolPriceSource` | | `assetaz.ticker.default` | `TickerService` and `NenjimApplication` | `TickerServiceImpl` | | `jupiter.perps.evelyn-prod` | `JupiterPerpsService` | Production `AnchorIdlJupiterPerpsServiceImpl` | | `jupiter.perps.evelyn-test` | `JupiterPerpsService` | Test `AnchorIdlJupiterPerpsServiceImpl` | | `jupiter-perps-alarm.default` | `JupiterPerpsAlarm` and `NenjimApplication` | `JupiterPerpsAlarmImpl` | | `evelyn.service.prod` | `Evelyn` and `NenjimApplication` | Production `EvelynImpl` | | `evelyn.service.test` | `Evelyn` and `NenjimApplication` | Test `EvelynImpl` | | `jupiter.swap.evelyn-burner-prod` | `JupiterSwapService` | `JupiterSwapServiceImpl` | | `notification.discord.assetaz-token` | `BoundNotificationService` | Existing Discord notification implementation | | `evelyn.iou-burner.prod` | `EvelynIOUBurnerService` and `NenjimApplication` | `EvelynIOUBurnerServiceImpl` | | `evelyn.mission-control.default` | `EvelynMissionControl` and `NenjimApplication` | `EvelynMissionControlImpl` | Infrastructure objects and value objects used only while constructing these components, such as `Clock`, `HttpClient`, `ObjectMapper`, and the EVE/USDT `TradingPair`, do not need Registry bindings in this issue. They are not independently exposed Nenjim components. Make every interface used as a Registry query type extend `NenjimComponent`. Do not make records, value types, configuration models, or incidental implementation helpers into components merely because they are constructor dependencies. Concrete implementations with a no-argument active `start()` lifecycle must additionally implement `NenjimApplication`. Do not force a domain service interface to expose lifecycle operations solely for Registry use. ### Constructor injection and dependency selection Pass the public `NenjimRegistryService` view into non-bootstrap applications/services that need to select other registered components. They must not receive the internal admin view. Where an application owns selection, express its hardcoded initial configuration as component IDs and resolve the typed objects through: ```java @Nullable <T extends NenjimComponent> T getComponent( @NotNull NenjimComponentId componentId, @NotNull Class<T> expectedInterface); ``` Fail clearly during construction or startup if a required hardcoded dependency is absent. Do not silently select the first result of an interface query when a specific component ID is required. In particular, preserve the current Ticker selection of the Raydium EVE/USDT price source. The hardcoded price source remains registered but inactive. The selection belongs to the Ticker composition/configuration, not to the Registry. ### Initial startup subset and order Keep the currently active startup set and dependency order: 1. `assetaz.ticker.default` 2. `evelyn.service.prod` 3. `evelyn.service.test` 4. `evelyn.iou-burner.prod` 5. `evelyn.mission-control.default` Keep `jupiter-perps-alarm.default` constructed and registered but not started, matching the current commented-out startup. Do not introduce a generic `getComponentIds(NenjimApplication.class)` start-all loop in this issue. The manager itself and the Registry service also implement `NenjimApplication`, and some registered applications are intentionally inactive. Use an explicit hardcoded startup list. Do not activate the currently commented-out Composer, Process Manager, Test Tool, Soda Task Manager, or Suwimo Client applications as part of this migration. Preserve the current `Done - Now online!` point and wait/blocking behavior unless a minimal relocation is required by the new class structure. ## Package cleanup Use these packages for the new types: - `com.r35157.nenjim.component` - `NenjimComponent` - `NenjimApplication` - `com.r35157.nenjim.service.registry` - `NenjimRegistryService` - `NenjimRegistryServiceManager` - `com.r35157.nenjim.service.registry.valuetypes` - `NenjimComponentId` - `com.r35157.nenjim.service.registry.impl.ref` - `NenjimRegistryServiceImpl` - `NenjimRegistryServiceAdmin` - `NenjimRegistryServiceManagerImpl` Rename the directly affected Ticker package from: ```text com.r35157.assetaz.services.ticker.plugins.pricesource ``` to: ```text com.r35157.assetaz.services.ticker.pricesource ``` Move its implementation subpackages with it and update all imports. This prevents the old plugin terminology from becoming part of the new component design. Do not perform unrelated large-scale package renames. ## Nullability Follow the repository convention and explicitly annotate every public reference-type parameter, return value, and record component with `@NotNull` or `@Nullable`. This includes public constructors and methods changed as part of dependency injection. Use `@Nullable` rather than `Optional` when absence is a valid model/API result. Specifically, a missing component ID is represented by the `@Nullable` result of `getComponent(...)`. ## Error handling and diagnostics Use the following exception categories: - `NullPointerException` for explicitly non-null API arguments that are `null`. - `IllegalArgumentException` for an invalid component ID. - `IllegalArgumentException` for a duplicate component ID. - `IllegalArgumentException` for a non-component or non-interface query type. - `IllegalArgumentException` when an existing ID is requested as the wrong component interface. - `IllegalStateException` for Registry queries before successful Registry startup. - `IllegalStateException` for a second call to either Registry or manager `start()`. Diagnostics must contain enough detail to identify the ID and involved types. Do not include credentials, private keys, wallet secrets, webhook secrets, or other sensitive constructor data in Registry messages or logs. ## Security boundary for this version This Registry is not an authorization system. For now: - A holder of the Registry can see all registered IDs for a component interface. - Component IDs do not grant or deny access. - Do not add scopes, permissions, filtered Registry views, ownership, user activation data, or application-specific visibility rules. - Keep the Registry instance injected rather than globally static so restricted views can be introduced later without redesigning all consumers. - Do not make Registry registration public merely to anticipate future loading. ## Runtime mutation boundary The internal Registry representation must support registrations after startup, because later work will allow an application to ask the manager to bring a new component to life. That future flow is expected to be: 1. An application receives `NenjimRegistryService` through constructor injection. 2. It obtains `NenjimRegistryServiceManager` through a normal Registry lookup. 3. It asks a future public manager method to load/create a component, supplying a component ID plus whatever recipe/version/context information is eventually required. 4. The manager creates the component and persists enough information to recreate it after reboot. 5. The manager registers it through the internal admin interface. 6. The application retrieves it through the read-only Registry API. Do not add that public manager loading method now. A component ID alone is not enough unless the manager already knows the construction recipe, and resolver, persistence, Context, version, and class-loading semantics have not been designed yet. Do not seal the Registry or implement a cache that can only be correct when no later registration occurs. ## Documentation and OpenSpec Update relevant Markdown documentation and OpenSpec material so that it consistently describes: - The component terminology. - The distinction between component ID and component object. - Registry read-only consumers versus manager/internal administration. - One registration being discoverable through multiple component interfaces. - Multiple IDs being allowed to reference the same object. - Applications owning component selection and activation. - The manager owning construction and registration. - The thin `NenjimHub.main(...)` bootstrap. - Contexts, versions, runtime loading, and removal as future work. Keep the OpenSpec project valid after the change. ## Out of scope Do not implement any of the following in this issue: - Multiple Contexts or one Registry per Context. - Artifact-version semantics in component IDs. - Dependency resolver behavior. - `NenjimClassLoader` integration. - Automatic component discovery. - Registry contributors. - Public runtime component registration/loading. - Component removal. - Registry persistence or recreation after reboot. - Registry events or subscribers. - User configuration storage. - Automatic component selection. - Automatic start/stop of every registered application. - Usage counts or ownership tracking. - Permissions, scopes, or filtered Registry views. - Migration of unrelated legacy applications. - A common `stop()` lifecycle. - New unit tests. ## Acceptance criteria - [ ] The implementation is based on the verified current `0.1-dev` branch containing commit `8c608a7`. - [ ] `NenjimComponent` exists as a pure marker interface. - [ ] `NenjimApplication` extends `NenjimComponent` and declares `start() throws Exception`. - [ ] `NenjimComponentId` strips and validates IDs using the specified format. - [ ] `NenjimRegistryService` exposes only the two agreed read-only lookup methods. - [ ] Missing IDs return `null` from `getComponent(...)`. - [ ] Existing IDs requested through the wrong interface produce a descriptive `IllegalArgumentException`. - [ ] `getComponentIds(...)` returns immutable registration-order snapshots. - [ ] A primary ID map and secondary interface-to-ID index are maintained consistently. - [ ] Recursive inherited component interfaces are indexed. - [ ] Concrete classes, ordinary interfaces, and the admin interface are not type-index keys. - [ ] Duplicate IDs fail with `IllegalArgumentException`. - [ ] The same object can be registered under multiple different IDs. - [ ] `NenjimRegistryServiceAdmin` is package-private and inaccessible to ordinary consumers. - [ ] `NenjimRegistryServiceImpl` implements the service, internal admin, and application views. - [ ] The Registry object is registered once and the same instance is retrievable as both `NenjimRegistryService` and `NenjimApplication`. - [ ] `NenjimRegistryServiceManagerImpl` is a different object implementing both manager and application views. - [ ] The manager constructs, starts, and registers the Registry before registering itself and the remaining components. - [ ] Neither Registry nor manager permits a second `start()` call, including after a failed first attempt. - [ ] Registry queries fail before successful Registry startup. - [ ] Internal registration remains valid after Registry startup. - [ ] The public manager API does not expose a Registry getter, registration, removal, loading, or stop operation. - [ ] Applications/services receive the public Registry view through constructor injection where they own component selection. - [ ] The current Raydium price source remains the Ticker's selected source and the hardcoded source remains inactive. - [ ] The current active startup subset and ordering are preserved. - [ ] The alarm and other currently commented applications are not newly activated. - [ ] `NenjimHub.main(...)` is the only outer bootstrap and starts a manager typed as `NenjimApplication`. - [ ] The old `Main` and `NenjimHubImpl` composition path is removed. - [ ] The Gradle main class points to the new `NenjimHub` bootstrap. - [ ] Directly affected Ticker price-source packages no longer use `plugins` in their names. - [ ] Public reference types have explicit nullability annotations. - [ ] Documentation and OpenSpec reflect the new architecture. - [ ] The project builds successfully with the existing build pipeline. ## Validation Do not add permanent unit tests unless separately requested. Validate the implementation with: 1. The existing project build and Detag/Java compilation pipeline. 2. OpenSpec validation. 3. Temporary, non-committed probes if useful for confirming: - ID stripping and validation. - Duplicate-ID rejection. - Same-object aliases under different IDs. - One registration being retrievable through multiple component interfaces with reference identity preserved. - Inherited-interface indexing. - Immutable ordered snapshots. - Missing-ID and wrong-type behavior. - Pre-start queries. - Single-use Registry and manager startup. - Registration after Registry startup. Do not use a full production `main()` run as the primary validation method. The current startup may block indefinitely, open UI, access external services, or perform financially relevant actions.
minimons added the enhancement label 2026-08-26 17:05:40 +02:00
minimons self-assigned this 2026-08-26 17:05:40 +02:00
minimons added this to the AssetAZ project 2026-08-26 17:05:40 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: r35157/com_r35157_nenjim-hubd-impl_ref#76