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

249 lines
20 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: 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 process'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: 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. Its `main(String[])` SHALL construct `com.r35157.nenjim.hubd.impl.ref.NenjimHubImpl` through a variable typed as `NenjimHub` and SHALL call `start()` exactly once. The Gradle application entry point SHALL continue to name this class. The bootstrap SHALL not construct the Registry, manager, or business graph.
#### Scenario: Start through the outer bootstrap
- **WHEN** the configured Java entry point is invoked
- **THEN** `Main.main(...)` constructs one reference Hub through the public Hub interface and delegates startup to it exactly once
#### Scenario: No duplicate composition path remains
- **WHEN** the source tree is inspected after migration
- **THEN** the former Registry-manager composition path is absent, `Main` contains no composition path, and `NenjimHubImpl` is the sole owner of the current hardcoded component graph
#### Scenario: Bootstrap has no composition responsibility
- **WHEN** the outer bootstrap source is inspected
- **THEN** composition, process tracking, online announcement, and the blocking wait are absent from `Main` and owned by the Hub implementation
### Requirement: Registry terminology and package boundaries are consistent
Public component contracts SHALL live in `com.r35157.nenjim.component`; the canonical process contract SHALL live in `com.r35157.nenjim.process`; the global Hub contract SHALL live in `com.r35157.nenjim.hubd`; public Registry service contracts SHALL live in `com.r35157.nenjim.service.registry`; and the component ID SHALL live in its `.valuetypes` package. Registry storage and its package-private administration view SHALL live in `.impl.ref`, where an implementation-only construction boundary MAY also live without exposing administration. Directly affected Ticker price-source API and implementation packages SHALL continue to use `.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, and the public service exposes no path to its active manager
#### Scenario: Canonical process API is inspected
- **WHEN** modern source packages are searched after migration
- **THEN** exactly one canonical `com.r35157.nenjim.process.NenjimProcess` contract exists and no `NenjimApplication` type remains
### 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. The Hub SHALL not add configurable autorun, process-manager integration, stop, restart, upgrade, health monitoring, recovery, or context-bound dependency access.
#### Scenario: Consumer inspects the first Registry API
- **WHEN** a consumer examines the public Registry, Registry-manager, process, and Hub contracts
- **THEN** only read-only Registry lookup, passive registration, minimal process startup, and Hub startup/snapshot operations are available and no deferred feature is exposed
### Requirement: Components and processes have minimal orthogonal contracts
`NenjimComponent` SHALL remain a pure marker with no component-ID or lifecycle property. The one canonical `com.r35157.nenjim.process.NenjimProcess` SHALL extend `NenjimComponent` and SHALL expose only `start() throws Exception` as its common lifecycle operation. Service and application SHALL remain independent domain or orchestration roles: an implementation in either role SHALL additionally implement `NenjimProcess` only when it has a Hub-managed active startup lifecycle. Passive components SHALL not implement `NenjimProcess`, and no `NenjimApplication` compatibility type SHALL remain.
#### 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 startup lifecycle managed by the Hub
- **THEN** the implementation is discoverable as `NenjimProcess` without adding lifecycle operations solely to its domain service interface
#### Scenario: Application role and process lifecycle are independent
- **WHEN** a component performs a user-facing or orchestration application role
- **THEN** that role alone neither makes it a process nor changes its domain interfaces
#### Scenario: Common stop is unavailable
- **WHEN** a caller uses `NenjimProcess`
- **THEN** the contract exposes no stop, restart, health, context, or dependency-lookup operation
### Requirement: Registry construction preserves the administrative boundary
The reference Registry construction boundary SHALL create one Registry object exposed through both `NenjimRegistryService` and `NenjimProcess`, plus one distinct `NenjimRegistryServiceManager` bound to that Registry. The service and process views SHALL preserve object identity. The construction helper SHALL own no lifecycle, SHALL be neither a component nor a process, SHALL not be Registry-discoverable, and SHALL expose neither the package-private administration interface nor a way to derive the active Registry's manager from an ordinary service reference.
#### Scenario: Registry views preserve identity
- **WHEN** the Hub obtains a newly constructed Registry assembly
- **THEN** its service and process views reference the identical Registry object while its manager is a different object
#### Scenario: Ordinary service reference remains read-only
- **WHEN** an ordinary consumer receives the public Registry service view
- **THEN** the consumer cannot name or derive the package-private administration view or obtain the manager bound to that active Registry
### Requirement: Registry manager is a passive registration handle
`NenjimRegistryServiceManager` SHALL expose exactly registration by component ID and component. It SHALL be neither a Nenjim component nor a Nenjim process, SHALL remain privately retained by the Hub, and SHALL delegate registration to the Registry's internal administration view while preserving duplicate-ID, interface-indexing, and post-start registration behavior. It SHALL not construct the business graph, own lifecycle, start or stop any process, expose the Registry service, or be registered under `nenjim.registry.service-manager` or any other component ID.
#### Scenario: Hub registers a component
- **WHEN** the Hub passes an unused component ID and a non-null component to the manager
- **THEN** the component becomes available through the Registry's existing typed interface index without being activated
#### Scenario: Registry manager is not discoverable
- **WHEN** Registry registrations are inspected after Hub composition
- **THEN** no component ID identifies the manager and no lookup returns it as a component or process
### Requirement: NenjimHub owns composition and runtime startup
`NenjimHub` SHALL be the global runtime owner and SHALL expose `start() throws Exception` plus an immutable ordered snapshot of successfully started process IDs. It SHALL be neither a component nor a process and SHALL not register itself. Its only accepted startup attempt SHALL construct the Registry subsystem, start the Registry process first, register it as `nenjim.registry.service`, construct and register the Journal subsystem as `nenjim.journal.service`, start Journal second, and continue to the complete hardcoded business component set from issue #76 only after Journal startup succeeds. The Hub SHALL then resolve only the explicitly enabled business IDs through `NenjimProcess`, start them in dependency-safe order, announce the existing online point, and preserve the existing blocking wait. Registration SHALL never imply startup, and both the Registry and Journal managers SHALL remain distinct passive, unregistered handles retained privately by the Hub.
#### Scenario: Registry starts before graph construction
- **WHEN** the Hub performs its first startup attempt
- **THEN** it starts, registers and records Registry first, before constructing Journal or components in the business graph
#### Scenario: Journal starts before business composition
- **WHEN** Registry startup and self-registration have succeeded
- **THEN** the Hub registers, starts and records Journal before constructing components in the business graph
#### Scenario: Registry is process-discoverable
- **WHEN** the Registry registration has completed
- **THEN** the same object is retrievable as both `NenjimRegistryService` and `NenjimProcess`
#### Scenario: Journal is process-discoverable
- **WHEN** the Journal registration has completed
- **THEN** the same object is retrievable as both `JournalService` and `NenjimProcess` under `nenjim.journal.service`, while its manager is not registered
#### Scenario: Required hardcoded bindings are registered
- **WHEN** Hub composition completes
- **THEN** Journal and all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components and excluding both service managers
#### Scenario: Explicit startup subset preserves order
- **WHEN** Hub startup succeeds through the business-process phase
- **THEN** successful process order is `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `evelyn.service.prod`, `evelyn.service.test`, `evelyn.iou-burner.prod`, and `evelyn.mission-control.default`
#### Scenario: Deliberately inactive processes remain inactive
- **WHEN** Hub startup reaches its online point
- **THEN** `jupiter-perps-alarm.default` and the dormant Composer, Process Manager, Test Tool, Soda Task Manager, and Suwimo Client paths have not been started
#### Scenario: Journal failure is fail-fast
- **WHEN** Journal process startup throws
- **THEN** Registry remains the only recorded process, no business component is constructed or started, the exception propagates, and the Hub neither announces online nor enters its final wait
#### Scenario: Hub start is not retried
- **WHEN** any first Hub startup attempt has begun and `start()` is requested again
- **THEN** the later request fails with `IllegalStateException` whether the first attempt succeeded or failed
#### Scenario: Startup failure is fail-fast
- **WHEN** any process startup throws
- **THEN** the exception propagates, no later process starts, the Hub neither announces online nor enters its final wait, and no generic rollback or retry is attempted
### Requirement: Started-process snapshots are ordered immutable and truthful
`NenjimHub.getStartedProcessIds()` SHALL be safe before, during, and after startup and SHALL return an immutable point-in-time snapshot in successful startup order. A process ID SHALL be appended only after that process's `start()` call returns successfully. Earlier successful entries SHALL remain visible after a later failure, while a failing process SHALL never be included. The snapshot SHALL make no health or continued-liveness guarantee.
#### Scenario: Snapshot before process startup
- **WHEN** the snapshot is requested before any process has started successfully
- **THEN** it is an empty immutable list
#### Scenario: Successful process is recorded
- **WHEN** a process's `start()` call returns successfully
- **THEN** its ID appears after all previously successful process IDs in every later snapshot
#### Scenario: Failed process is not recorded
- **WHEN** a process's `start()` call throws
- **THEN** its ID is absent while all earlier successful process IDs remain visible
#### Scenario: Snapshot is a point-in-time value
- **WHEN** the Hub starts another process after a caller obtained a snapshot
- **THEN** the earlier snapshot is unchanged and cannot be mutated by its caller