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

14 KiB

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