Files
com_r35157_nenjim-hubd-impl…/openspec/specs/nenjim-component-registry/spec.md
T

182 lines
14 KiB
Markdown

# nenjim-component-registry Specification
## Purpose
Provide one typed, read-only catalogue of constructed Nenjim components and a lifecycle manager that owns the current hardcoded composition without introducing Context or dynamic-loading semantics.
## Requirements
### Requirement: Components and applications have minimal common contracts
`NenjimComponent` SHALL be a pure marker with no component-ID property. `NenjimApplication` SHALL extend `NenjimComponent` and expose only `start() throws Exception` as its common lifecycle operation. Domain service interfaces SHALL remain focused on their domain APIs, while a concrete service implementation with a no-argument active start lifecycle SHALL additionally implement `NenjimApplication`.
#### Scenario: One object has more than one registration identity
- **WHEN** the same component object is registered under two different unused component IDs
- **THEN** both IDs identify the same object because IDs are Registry metadata rather than object properties
#### Scenario: A service implementation is startable
- **WHEN** a service implementation has a no-argument active start lifecycle
- **THEN** the implementation is discoverable as `NenjimApplication` without adding lifecycle operations solely to its domain service interface
#### Scenario: Common stop is unavailable
- **WHEN** a caller uses `NenjimApplication`
- **THEN** the contract exposes no common stop operation
### Requirement: Component IDs are canonical validated values
`NenjimComponentId` SHALL reject null, strip surrounding Unicode whitespace before storage and validation, and accept only lower-case ASCII dot-separated segments whose first character is a letter and whose remaining subparts contain lower-case letters or digits separated by single internal hyphens. Standard record value equality and hashing SHALL define ID equality.
#### Scenario: Surrounding whitespace is canonicalized
- **WHEN** an ID is constructed from ` nenjim.registry.service `
- **THEN** its stored value is `nenjim.registry.service`
#### Scenario: Valid component IDs are accepted
- **WHEN** IDs such as `nenjim.registry.service`, `assetaz.price-source.raydium-pool`, or `evelyn.service.prod` are constructed
- **THEN** construction succeeds
#### Scenario: Invalid component IDs are rejected
- **WHEN** an ID is null or contains uppercase letters, underscores, internal whitespace, an empty segment, a leading or trailing dot, or a malformed hyphen
- **THEN** construction fails with the specified null or argument exception category and identifies the invalid canonicalized value without exposing secrets
### Requirement: Registry consumers receive only typed read-only lookup
`NenjimRegistryService` SHALL be a Nenjim component and SHALL expose exactly an ordered-ID lookup by component interface and a nullable typed lookup by component ID and expected component interface. Both operations SHALL reject null arguments. A query type SHALL be an interface extending `NenjimComponent`; concrete classes and ordinary interfaces SHALL be rejected.
#### Scenario: List matching component IDs
- **WHEN** a caller requests the IDs for a valid component interface
- **THEN** the Registry returns all registrations indexed under that interface in registration order as an immutable snapshot that is not changed by later registrations
#### Scenario: No component implements an interface
- **WHEN** a valid component interface has no indexed registration
- **THEN** the Registry returns an empty immutable list rather than null
#### Scenario: Retrieve a matching component
- **WHEN** an existing component ID is requested through an interface implemented by its object
- **THEN** the Registry returns that object cast to the requested interface
#### Scenario: Component ID is missing
- **WHEN** an unused component ID is requested through a valid component interface
- **THEN** the Registry returns null
#### Scenario: Existing component has the wrong type
- **WHEN** an existing component ID is requested through a component interface its object does not implement
- **THEN** lookup fails with `IllegalArgumentException` identifying the ID, requested interface name, and actual implementation class name
#### Scenario: Query token is not a component interface
- **WHEN** a caller supplies a concrete class, an ordinary interface, or null as a query token
- **THEN** the Registry rejects it with the specified argument or null exception category
#### Scenario: Consumer cannot administer registrations
- **WHEN** ordinary code receives `NenjimRegistryService`
- **THEN** that public view exposes no registration, removal, loading, resolver, class-loading, stop, permission, event, or persistence operation
### Requirement: Internal registration maintains an interface index
The internal Registry administration view SHALL remain package-private, SHALL not be a Nenjim component, and SHALL expose registration but no removal. Each registration SHALL atomically add a unique ID to a primary ID-to-object catalogue and to a secondary ordered index under every implemented interface in the recursive hierarchy that extends `NenjimComponent`. It SHALL not index concrete classes, ordinary interfaces, or the administration interface. Reads and registrations SHALL remain internally consistent when registrations occur after startup.
#### Scenario: Duplicate ID is rejected
- **WHEN** a component is registered under an ID already present in the Registry
- **THEN** registration fails with `IllegalArgumentException` identifying the duplicated ID and both the existing and attempted implementation class names without changing either index
#### Scenario: Null registration input is rejected
- **WHEN** a registration supplies a null ID or component
- **THEN** registration fails with `NullPointerException` without changing either index
#### Scenario: Same instance receives aliases
- **WHEN** the same object is registered under multiple different unused IDs
- **THEN** every registration succeeds and each ID resolves to the identical object reference
#### Scenario: Inherited component interfaces are indexed
- **WHEN** a registered object implements a component interface that extends another component interface
- **THEN** the registration ID appears under both interfaces, including inherited interfaces not directly declared by the implementation class
#### Scenario: One registration exposes multiple views
- **WHEN** one object implements multiple component interfaces and is registered once
- **THEN** its one ID appears under every implemented component interface and typed lookups preserve reference identity
#### Scenario: Registration occurs after startup
- **WHEN** internal administration registers another component after the Registry has started
- **THEN** subsequent queries observe a consistent registration while previously returned ID snapshots remain unchanged
### Requirement: Registry startup is single-use and gates queries
The Registry application's first `start()` call SHALL be the only accepted attempt and SHALL enable queries only after successful startup. A later attempt SHALL fail with `IllegalStateException`, including when the first attempt failed. Registration SHALL remain allowed before and after startup, and starting the Registry SHALL not construct, register, start, or stop another component.
#### Scenario: Query before startup
- **WHEN** either lookup operation is called before successful Registry startup
- **THEN** it fails with `IllegalStateException`
#### Scenario: Registry starts once
- **WHEN** the Registry is started successfully and start is requested again
- **THEN** the second request fails with `IllegalStateException`
#### Scenario: Failed start is not retried
- **WHEN** the first Registry start attempt fails
- **THEN** every later start request fails with `IllegalStateException`
### Requirement: Registry manager owns hardcoded composition and startup
`NenjimRegistryServiceManager` SHALL be a Nenjim component whose only public operation is `start() throws Exception`. Its public reference implementation SHALL be a distinct `NenjimApplication` object and SHALL accept only one start attempt, including after failure. The first attempt SHALL construct and directly start one Registry object, register that object first as `nenjim.registry.service`, register the manager second as `nenjim.registry.service-manager`, construct and register the complete required hardcoded component set in dependency order, start only the explicit active application subset in dependency-safe order, announce the existing online point, and preserve the existing blocking wait.
#### Scenario: Registry and manager views are registered
- **WHEN** manager composition reaches completed registration
- **THEN** the Registry object is retrievable by one ID as both `NenjimRegistryService` and `NenjimApplication`, the manager is retrievable by one different ID as both `NenjimRegistryServiceManager` and `NenjimApplication`, and the Registry and manager are different instances
#### Scenario: Required hardcoded bindings are registered
- **WHEN** manager composition completes
- **THEN** all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components
#### Scenario: Explicit startup subset preserves order
- **WHEN** the manager starts applications
- **THEN** it starts `assetaz.ticker.default`, `evelyn.service.prod`, `evelyn.service.test`, `evelyn.iou-burner.prod`, and `evelyn.mission-control.default` in that order
#### Scenario: Deliberately inactive applications remain inactive
- **WHEN** manager startup completes its explicit start sequence
- **THEN** `jupiter-perps-alarm.default` and the currently commented Composer, Process Manager, Test Tool, Soda Task Manager, and Suwimo Client are not started
#### Scenario: Manager start is not retried
- **WHEN** any first manager start attempt has begun and start is requested again
- **THEN** the later request fails with `IllegalStateException` whether the first attempt succeeded or failed
#### Scenario: Registration does not activate components
- **WHEN** the manager registers a component
- **THEN** registration alone neither starts nor stops that component
### Requirement: Applications own explicit dependency selection
A non-bootstrap application or service that owns a component choice SHALL receive the public Registry view through constructor injection, express the choice as one or more explicit component IDs, and use typed lookup. A required missing or wrong-type binding SHALL fail clearly rather than selecting the first component returned for an interface. The initial Ticker composition SHALL explicitly select only `assetaz.price-source.raydium-pool.eve-usdt`; `assetaz.price-source.hardcoded` SHALL remain registered but inactive.
#### Scenario: Ticker resolves its configured source
- **WHEN** the Ticker is constructed with the public Registry and the Raydium EVE/USDT source ID
- **THEN** it resolves that exact `PriceSource` and does not activate the hardcoded source
#### Scenario: Required configured dependency is missing
- **WHEN** an application resolves a required hardcoded component ID that is absent
- **THEN** construction or startup fails with a diagnostic identifying the required ID and interface without exposing component secrets
#### Scenario: Internal administration is not injected
- **WHEN** an ordinary application receives its Registry dependency
- **THEN** it receives only `NenjimRegistryService` and cannot self-register
### Requirement: Main is a thin outer bootstrap
`com.r35157.nenjim.hubd.Main` SHALL be a final non-component class that does not implement `NenjimApplication`. Its `main(String[])` SHALL construct `NenjimRegistryServiceManagerImpl` through a variable typed as `NenjimApplication` and start it. The Gradle application entry point SHALL name this class. Construction, registration, composition, and lifecycle ownership SHALL remain in `NenjimRegistryServiceManagerImpl`; the bootstrap SHALL perform none of those responsibilities beyond constructing and starting the manager.
#### Scenario: Start through the outer bootstrap
- **WHEN** the configured Java entry point is invoked
- **THEN** `Main.main(...)` constructs one Registry manager as a Nenjim application and delegates startup to it
#### Scenario: No duplicate composition path remains
- **WHEN** the source tree is inspected after migration
- **THEN** the old Hub interface, `com.r35157.nenjim.hubd.impl.ref.Main`, and the `NenjimHubImpl` composition implementation are absent, while `com.r35157.nenjim.hubd.Main` contains no replacement composition path
### Requirement: Registry terminology and package boundaries are consistent
Public component contracts SHALL live in `com.r35157.nenjim.component`; public Registry service contracts SHALL live in `com.r35157.nenjim.service.registry`; the component ID SHALL live in its `.valuetypes` package; and internal administration and Registry storage SHALL live in `.impl.ref`. Directly affected Ticker price-source API and implementation packages SHALL use `.pricesource` rather than `.plugins.pricesource`. Public reference-type parameters, return values, and record components added or changed by this capability SHALL declare explicit nullability.
#### Scenario: Ticker source packages are inspected
- **WHEN** source packages and imports are searched after migration
- **THEN** no directly affected Ticker price-source package uses the `plugins` segment
#### Scenario: Internal Registry administration is inspected
- **WHEN** an ordinary consumer compiles against the public Registry packages
- **THEN** the package-private administration interface and Registry implementation are inaccessible
### Requirement: Deferred runtime features remain absent
The Registry SHALL represent one catalogue of already constructed components and SHALL not implement Contexts, artifact-version identity, resolution, classloading, automatic discovery, contributor APIs, public runtime loading or registration, removal, persistence, events, subscribers, user configuration, automatic selection, automatic start-all, ownership tracking, usage counts, permissions, scopes, filtered views, or a common stop lifecycle.
#### Scenario: Consumer inspects the first Registry API
- **WHEN** a consumer examines the public Registry and manager contracts
- **THEN** only the agreed read-only lookup and manager start operations are available and no deferred feature is exposed