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.
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.
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.
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:
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.
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:
Do not register the object separately for each interface.
Storage and type index
Use two logical structures:
A primary ID-to-object map.
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.
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:
Construct NenjimRegistryServiceImpl while retaining separate NenjimRegistryService and NenjimRegistryServiceAdmin views of the same object.
Call start() directly on the Registry application's NenjimApplication view.
Register the Registry object first, using nenjim.registry.service.
Register the manager object second, using nenjim.registry.service-manager.
Construct and register the remaining hardcoded components in dependency order.
Start only the explicitly enabled application subset, in the current dependency-safe order.
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:
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:
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:
assetaz.ticker.default
evelyn.service.prod
evelyn.service.test
evelyn.iou-burner.prod
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.
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:
An application receives NenjimRegistryService through constructor injection.
It obtains NenjimRegistryServiceManager through a normal Registry lookup.
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.
The manager creates the component and persists enough information to recreate it after reboot.
The manager registers it through the internal admin interface.
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.
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:
The existing project build and Detag/Java compilation pipeline.
OpenSpec validation.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Repository baseline
Implement this issue against:
https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref.git0.1-devb230e15ecea76babee31c73019430ca5d7976bb2Gitea is authoritative. Verify the baseline commit through the Gitea API before starting, fetch the current
0.1-devbranch, and resolve the commit from that branch. Do not rely on GitHub or an older local checkout.Summary
Introduce the first small version of
NenjimRegistryServiceand move construction, registration, dependency composition, and startup of the current Nenjim components out ofNenjimHubImpland intoNenjimRegistryServiceManagerImpl.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:
NenjimRegistryServiceManagerImplconstructs the Registry and the currently available hardcoded components.NenjimComponentId.NenjimRegistryServiceis a read-only catalogue used by ordinary consumers.NenjimRegistryServicethrough constructor injection.Terminology
Use
componentconsistently in the new API, implementation, documentation, diagnostics, variables, and method names.Do not introduce
NenjimPluginor plugin-specific Registry terminology. Existing packages that currently containpluginsshould be renamed where this issue directly touches them, as described below.Public component contracts
NenjimComponentAdd a pure marker interface:
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.
NenjimApplicationAdd the common startable-component contract:
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
NenjimApplicationwhen it has an active lifecycle.For example,
NenjimRegistryServiceImplis both the Registry service implementation and a startable Nenjim application.Component ID
Add this public value type:
Required behavior:
nullvalue withNullPointerException.String.strip()before validation and store the stripped value.nenjim.registry.service,assetaz.price-source.raydium-pool, andevelyn.service.prodare valid." nenjim.registry.service "canonicalize tonenjim.registry.service.Read-only Registry API
Add:
Both lookup methods intentionally begin with
getComponentso that IDE autocomplete exposes them together.getComponentIds(...)Required behavior:
nullwithNullPointerException.NenjimComponentwithIllegalArgumentException.null.getComponent(...)Required behavior:
nullargument withNullPointerException.expectedInterfacewhen it is not an interface extendingNenjimComponentwithIllegalArgumentException.nullif the ID is not registered.IllegalArgumentException. This is a caller programming error, not a missing component.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:
This is an internal implementation handle used by
NenjimRegistryServiceManagerImpl. It must not extendNenjimComponentorNenjimApplication, 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 publicNenjimRegistryServiceview.Required registration behavior:
nullID or component withNullPointerException.IllegalArgumentException.No removal operation is included in this issue.
Registry implementation
Add package-private
NenjimRegistryServiceImpl: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:
Do not register the object separately for each interface.
Storage and type index
Use two logical structures:
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:
NenjimComponent.NenjimRegistryServiceAdmininterface.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:IllegalStateException.IllegalStateException.start()does not construct, register, start, or stop any other component.Registry manager API
Add:
Do not add
getRegistryService(). Applications receiveNenjimRegistryServicethrough 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 bothNenjimRegistryServiceManagerandNenjimApplicationso 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:IllegalStateException.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:
NenjimRegistryServiceImplwhile retaining separateNenjimRegistryServiceandNenjimRegistryServiceAdminviews of the same object.start()directly on the Registry application'sNenjimApplicationview.nenjim.registry.service.nenjim.registry.service-manager.After startup, Registry lookups must therefore be able to retrieve:
NenjimRegistryService.NenjimApplication.NenjimRegistryServiceManager.NenjimApplication.The Registry and manager objects must not be the same instance.
Bootstrap class
NenjimHubbecomes only the outer Java bootstrap class. It is not a Registry component and does not implementNenjimApplication.Move the entry point out of
com.r35157.nenjim.hubd.impl.ref.Mainand use this shape:Update the Gradle application main class accordingly.
After all current construction and startup responsibility has moved to the Registry manager, remove the old
MainandNenjimHubImplbootstrap/composition implementation. Do not preserve a second composition path.Migrate the current hardcoded composition
Move the construction currently performed by
NenjimHubImplintoNenjimRegistryServiceManagerImplwithout changing the concrete production/test choices or active startup subset.Use explicit constants for component IDs. The following IDs are the required initial bindings:
nenjim.registry.serviceNenjimRegistryServiceandNenjimApplicationNenjimRegistryServiceImplnenjim.registry.service-managerNenjimRegistryServiceManagerandNenjimApplicationNenjimRegistryServiceManagerImplnenjim.object-cache.defaultObjectCacheObjectCacheImplassetaz.currency-identity.hardcodedCurrencyIdentityServiceHardcodedCurrencyIdentityServicesolana.blockchain.directSolanaBlockChainSolanaBlockChainImplsolana.blockchain.cachedSolanaBlockChainCachedSolanaBlockChainraydium.service.defaultRaydiumRaydiumImplsolana.wallet.evelyn-perps-testSolanaWalletsolana.wallet.evelyn-perps-prodSolanaWalletsolana.wallet.evelyn-burner-prodSolanaWalletassetaz.price-source.hardcodedPriceSourceHardcodedPriceSourceassetaz.price-source.raydium-pool.eve-usdtPriceSourceRaydiumPoolPriceSourceassetaz.ticker.defaultTickerServiceandNenjimApplicationTickerServiceImpljupiter.perps.evelyn-prodJupiterPerpsServiceAnchorIdlJupiterPerpsServiceImpljupiter.perps.evelyn-testJupiterPerpsServiceAnchorIdlJupiterPerpsServiceImpljupiter-perps-alarm.defaultJupiterPerpsAlarmandNenjimApplicationJupiterPerpsAlarmImplevelyn.service.prodEvelynandNenjimApplicationEvelynImplevelyn.service.testEvelynandNenjimApplicationEvelynImpljupiter.swap.evelyn-burner-prodJupiterSwapServiceJupiterSwapServiceImplnotification.discord.assetaz-tokenBoundNotificationServiceevelyn.iou-burner.prodEvelynIOUBurnerServiceandNenjimApplicationEvelynIOUBurnerServiceImplevelyn.mission-control.defaultEvelynMissionControlandNenjimApplicationEvelynMissionControlImplInfrastructure objects and value objects used only while constructing these components, such as
Clock,HttpClient,ObjectMapper, and the EVE/USDTTradingPair, 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 implementNenjimApplication. Do not force a domain service interface to expose lifecycle operations solely for Registry use.Constructor injection and dependency selection
Pass the public
NenjimRegistryServiceview 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:
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:
assetaz.ticker.defaultevelyn.service.prodevelyn.service.testevelyn.iou-burner.prodevelyn.mission-control.defaultKeep
jupiter-perps-alarm.defaultconstructed 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 implementNenjimApplication, 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.componentNenjimComponentNenjimApplicationcom.r35157.nenjim.service.registryNenjimRegistryServiceNenjimRegistryServiceManagercom.r35157.nenjim.service.registry.valuetypesNenjimComponentIdcom.r35157.nenjim.service.registry.impl.refNenjimRegistryServiceImplNenjimRegistryServiceAdminNenjimRegistryServiceManagerImplRename the directly affected Ticker package from:
to:
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
@NotNullor@Nullable.This includes public constructors and methods changed as part of dependency injection.
Use
@Nullablerather thanOptionalwhen absence is a valid model/API result. Specifically, a missing component ID is represented by the@Nullableresult ofgetComponent(...).Error handling and diagnostics
Use the following exception categories:
NullPointerExceptionfor explicitly non-null API arguments that arenull.IllegalArgumentExceptionfor an invalid component ID.IllegalArgumentExceptionfor a duplicate component ID.IllegalArgumentExceptionfor a non-component or non-interface query type.IllegalArgumentExceptionwhen an existing ID is requested as the wrong component interface.IllegalStateExceptionfor Registry queries before successful Registry startup.IllegalStateExceptionfor a second call to either Registry or managerstart().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:
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:
NenjimRegistryServicethrough constructor injection.NenjimRegistryServiceManagerthrough a normal Registry lookup.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:
NenjimHub.main(...)bootstrap.Keep the OpenSpec project valid after the change.
Out of scope
Do not implement any of the following in this issue:
NenjimClassLoaderintegration.stop()lifecycle.Acceptance criteria
0.1-devbranch containing commit8c608a7.NenjimComponentexists as a pure marker interface.NenjimApplicationextendsNenjimComponentand declaresstart() throws Exception.NenjimComponentIdstrips and validates IDs using the specified format.NenjimRegistryServiceexposes only the two agreed read-only lookup methods.nullfromgetComponent(...).IllegalArgumentException.getComponentIds(...)returns immutable registration-order snapshots.IllegalArgumentException.NenjimRegistryServiceAdminis package-private and inaccessible to ordinary consumers.NenjimRegistryServiceImplimplements the service, internal admin, and application views.NenjimRegistryServiceandNenjimApplication.NenjimRegistryServiceManagerImplis a different object implementing both manager and application views.start()call, including after a failed first attempt.NenjimHub.main(...)is the only outer bootstrap and starts a manager typed asNenjimApplication.MainandNenjimHubImplcomposition path is removed.NenjimHubbootstrap.pluginsin their names.Validation
Do not add permanent unit tests unless separately requested.
Validate the implementation with:
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.