77: Make NenjimHub the runtime owner and replace NenjimApplication with NenjimProcess

This commit is contained in:
2026-09-02 15:59:56 +02:00
parent a252c256c4
commit f5fc5bb3b5
29 changed files with 1012 additions and 461 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-02
@@ -0,0 +1,72 @@
## Context
See `proposal.md` for motivation and the delta specification for observable behavior. At baseline, `NenjimRegistryServiceManagerImpl` mixes three concerns: privileged Registry registration, construction of the complete hardcoded object graph, and runtime lifecycle. The thin `Main` starts that manager through `NenjimApplication`, while the Registry object itself also implements the same lifecycle type. Registry storage and its mutation view are package-private; the new Hub implementation must remain in its own implementation package without weakening that boundary.
Human-written Java remains in `.tjava`, Detag runs through the normal Gradle build, and the exact operational component bindings from issue #76 must be preserved without executing production startup as verification. Legacy process and Hub prototypes under `crypto.r35157` and `com.r35157.nenjim.kicker` remain outside the modern API migration.
## Goals / Non-Goals
**Goals:**
- Make one `NenjimHubImpl` the unmistakable composition root and runtime lifecycle owner.
- Keep Registry mutation inaccessible from ordinary `NenjimRegistryService` consumers while allowing a Hub in another package to receive a narrow manager handle.
- Preserve the exact current graph, enabled sequence, inactive set, online point, wait behavior, and Registry indexing semantics.
- Make startup tracking linearizable enough for concurrent snapshot readers and truthful under partial failure.
- Keep public module packages extractable and explicitly nullable where reference values cross new or changed APIs.
**Non-Goals:**
- No configurable graph, dependency resolver, automatic process discovery/start-all, generic rollback, or process health inference.
- No stop/restart contract, test hooks, permanent automated tests, Journal integration, Process Manager wiring, or legacy prototype cleanup.
- No change to embedded operational values, business constructors, or component behavior while relocating composition.
## Decisions
### NenjimProcess is the only modern common lifecycle type
Add `com.r35157.nenjim.process.NenjimProcess` as a marker-derived interface with only `start() throws Exception`. Replace every modern `NenjimApplication` implementation and lookup, then delete `NenjimApplication` rather than retaining an alias. Service APIs stay domain-focused, and application remains a descriptive role. Keeping an alias was rejected because it would preserve two canonical lifecycle classifications and make future Registry queries ambiguous.
Legacy `crypto.r35157.nenjim.NenjimProcess` and `com.r35157.nenjim.kicker.NenjimProcess` are not imported by the modern graph and remain untouched. Their packages and contracts are explicitly non-canonical and outside issue #77.
### A small implementation-only assembly crosses the Registry package boundary
Add a public reference-implementation assembly type under `com.r35157.nenjim.service.registry.impl.ref` with a static construction operation and three read-only accessors. It creates one package-private `NenjimRegistryServiceImpl`, exposes that identical object as both `NenjimRegistryService` and `NenjimProcess`, and creates a distinct `NenjimRegistryServiceManagerImpl` holding only the package-private admin view.
The assembly exposes neither the implementation nor `NenjimRegistryServiceAdmin`, has no lifecycle methods, and is not a component. Its constructor is private, so callers cannot manufacture inconsistent triples. Returning the manager from `NenjimRegistryService` or making the admin interface public was rejected because either would let ordinary consumers escalate a read-only service reference into mutation access.
### The Registry manager delegates one operation and owns no state policy
`NenjimRegistryServiceManagerImpl` retains only a required `NenjimRegistryServiceAdmin` reference and delegates `registerComponent(...)`. Its constructor remains inaccessible outside the reference implementation package; the Hub obtains it through the assembly. It has no start flag, Registry getter, component marker, graph fields, IDs, logging, or wait latch. Registration remains synchronized and atomic inside the existing Registry implementation.
### NenjimHubImpl owns the moved graph and explicit startup sequence
Move the complete existing composition code and its exact constants into `com.r35157.nenjim.hubd.impl.ref.NenjimHubImpl`. At the beginning of the one accepted `start()` attempt, the Hub obtains the Registry views and private manager, starts the Registry process, registers the Registry service, and records its ID. Only then does it build and register the remaining graph through the manager, because the Ticker constructor performs a Registry lookup.
After composition, a helper resolves each explicitly enabled ID as `NenjimProcess`, calls `start()`, and records the ID only after successful return. Keeping an explicit sequence rather than iterating every process registration preserves deliberately inactive components and avoids inventing dependency ordering.
### Snapshot state uses one lock and copy-on-read
The Hub stores successful IDs in an insertion-ordered `ArrayList`. A small private synchronized append operation and a synchronized `getStartedProcessIds()` returning `List.copyOf(...)` make calls safe before, during, and after startup while preventing callers or later starts from mutating earlier snapshots. `AtomicBoolean.compareAndSet` consumes the Hub's single start attempt before any work, including attempts that later fail.
This is intentionally startup history, not a running/healthy registry. Concurrent collections or a richer process-state model were rejected because there is one writer, a tiny list, and no common stop or health signal.
### Main remains a two-line runtime delegation
`com.r35157.nenjim.hubd.Main` constructs `NenjimHubImpl` into a `NenjimHub` variable and invokes `start()` once. It does not retain or expose Registry views. Gradle's configured entry point stays unchanged.
## Risks / Trade-offs
- [Relocating a large hardcoded graph can accidentally change an operational binding] → Move the graph mechanically, keep constants and constructor arguments unchanged, and inspect a redacted final diff against issue #76.
- [A later startup failure leaves earlier processes active] → Preserve fail-fast propagation and truthful successful history; do not invent rollback without a common stop contract.
- [The implementation-only assembly is publicly nameable] → Keep it under `.impl.ref`, make construction internally consistent, expose no admin type, and provide no reverse path from an active service view to its manager.
- [The Hub's final wait makes ordinary end-to-end execution unsafe and non-terminating] → Validate through compilation, strict OpenSpec checks, static acceptance auditing, and narrowly scoped temporary probes that do not invoke production startup.
- [Legacy similarly named process types remain] → Document the canonical fully qualified name and limit migration to modern packages, as required by issue #77.
## Migration Plan
1. Add the canonical process and Hub APIs plus the Registry assembly/passive manager boundary.
2. Move the existing graph and runtime sequence into `NenjimHubImpl`, replace modern lifecycle implementations/lookups, and update `Main`.
3. Update directly affected architecture, terminology, HTML, and JavaDoc without changing archived OpenSpec history.
4. Strictly validate the active change, build through Detag and Java, run safe narrow probes, and audit the redacted diff while leaving the change active.
Rollback before review is normal source-control reversal of these uncommitted files. No persistent data or external Registry state is migrated.
@@ -0,0 +1,32 @@
## Why
Issue #76 made the Registry manager the composition root and lifecycle owner, but design review established that these are responsibilities of one running NenjimHub rather than of the Registry subsystem. The architecture now needs a canonical process contract and an explicit Hub runtime owner before later Journal and process-management work builds on the wrong boundary.
## What Changes
- **BREAKING**: Replace `NenjimApplication` with the canonical minimal `NenjimProcess` component contract.
- Reintroduce `NenjimHub` as the global composition and runtime owner, with single-attempt startup and an immutable ordered snapshot of successfully started process IDs.
- Move Registry construction, the complete hardcoded component graph, explicit process startup, the online point, and the blocking runtime wait into `NenjimHubImpl`.
- **BREAKING**: Reduce `NenjimRegistryServiceManager` to a passive registration handle that is retained by the Hub and is neither a component nor a process.
- Add an implementation-only Registry construction boundary that provides read-only service, process, and manager views without exposing the package-private administration interface.
- Preserve all current component bindings, enabled process choices and order, inactive components, Registry lookup guarantees, and fail-fast startup behavior.
- Keep `com.r35157.nenjim.hubd.Main` as the thin configured entry point and update directly affected documentation and JavaDoc.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `nenjim-component-registry`: Corrects component lifecycle terminology, Registry-manager responsibility, global Hub ownership, startup tracking, and outer bootstrap behavior.
- `assetaz-currency-identity-service`: Transfers hardcoded catalogue composition ownership from the Registry manager to `NenjimHub`.
- `assetaz-ticker-service`: Transfers price-source construction and registration ownership from the Registry manager to `NenjimHub`.
## Impact
- Changes public lifecycle and runtime APIs under `com.r35157.nenjim.process`, `com.r35157.nenjim.hubd`, and `com.r35157.nenjim.service.registry`.
- Moves the current reference composition from `NenjimRegistryServiceManagerImpl` to `NenjimHubImpl` while preserving its operational bindings and sequence.
- Updates all modern startable reference implementations to the new process contract and removes `NenjimApplication` without a compatibility alias.
- Updates the active OpenSpec delta, Nenjim architecture documentation, terminology, and public HTML; no Journal, Context, classloader, configurable autorun, process-manager, stop/restart/health, or business behavior is introduced.
@@ -0,0 +1,16 @@
## MODIFIED Requirements
### Requirement: Immutable conflict-free hardcoded catalogue
The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ currency UUIDs, maintain one current currency value for each UUID, support multiple external references per UUID, reject conflicting UUID or external-identity mappings during initialization, expose no public mutation operation, and be safe for concurrent reads after construction. The NenjimHub SHALL create exactly one hardcoded service instance for the current hardcoded component graph, register it as `assetaz.currency-identity.hardcoded`, and SHALL pass that same instance to every component in that graph that requires currency identities.
#### Scenario: Conflicting external mapping
- **WHEN** initialization maps the same namespace and external identifier to two different AssetAZ UUIDs
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Conflicting currency metadata
- **WHEN** initialization provides conflicting current values for the same UUID
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Compose hardcoded components
- **WHEN** the NenjimHub constructs the current hardcoded component graph
- **THEN** it creates and registers one hardcoded Currency Identity Service and injects that same instance into every component that requires it
@@ -0,0 +1,40 @@
## MODIFIED Requirements
### Requirement: Price sources are discovered and lifecycle-managed
Nenjim SHALL be able to construct and register each price source independently in an initialized but unstarted state without supplying a ticker or sink. The ticker SHALL receive the public Nenjim Registry through constructor injection and resolve its explicitly configured `PriceSource` component IDs, while ordinary ticker clients SHALL NOT receive a public operation for adding or managing sources. The ticker SHALL fail clearly when a configured ID is missing or names a component that does not implement `PriceSource`; it SHALL NOT silently select the first source listed for the interface. The ticker SHALL identify each resolved source by its `TradingPair` and stable source name, reject duplicate identities clearly, start each active source by passing itself as `PriceSink`, and stop every source it started. Source names SHALL be non-empty safe directory components and SHALL reject `/`, `\`, `..`, and control characters without rewriting them. A source SHALL announce typed prices and timestamps only through the sink received at start and SHALL remain unaware of its persistence path. A repeated source start SHALL reject the call before replacing its existing sink.
#### Scenario: Sources are constructed independently
- **WHEN** the NenjimHub constructs and registers a price source before constructing or starting the ticker
- **THEN** source construction requires neither a `PriceSink` nor a ticker reference and leaves the source unstarted
#### Scenario: Ticker resolves configured source components
- **WHEN** the ticker initializes its source set from explicit component IDs and an injected public Registry
- **THEN** it resolves each ID as `PriceSource`, not `PriceSink`, and manages only the resolved configured sources internally
#### Scenario: Missing configured source fails clearly
- **WHEN** the ticker is configured with an absent component ID or an ID whose object is not a `PriceSource`
- **THEN** construction fails with a diagnostic identifying the ID and required interface
#### Scenario: Ordinary clients cannot manage sources
- **WHEN** a caller uses the public `TickerService` contract
- **THEN** the contract exposes ticker lifecycle and latest-price queries but no source-registration operation
#### Scenario: Distinct configured sources are accepted
- **WHEN** the ticker resolves two configured sources with different source names or trading pairs
- **THEN** it accepts both and manages each source independently
#### Scenario: Duplicate persistent identity is rejected
- **WHEN** the ticker resolves sources with the same trading pair and source name
- **THEN** initialization fails with an error identifying the duplicate source history
#### Scenario: Unsafe source name is rejected
- **WHEN** a source name is empty or contains a slash, backslash, `..`, or a control character
- **THEN** ticker initialization or startup fails clearly without rewriting the source name or creating filesystem content
#### Scenario: Active source lifecycle is managed
- **WHEN** the ticker starts and later stops with an active Registry-resolved source
- **THEN** it calls `start` with itself as `PriceSink` and subsequently stops the source without leaving source-owned resources running
#### Scenario: Repeated source start preserves existing sink
- **WHEN** an already-started source receives another start call with a different sink
- **THEN** it rejects the call before replacing the sink from the successful start
@@ -0,0 +1,158 @@
## ADDED Requirements
### 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 complete hardcoded component set from issue #76 in dependency order, resolve only the explicitly enabled 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.
#### Scenario: Registry starts before graph construction
- **WHEN** the Hub performs its first startup attempt
- **THEN** it starts the Registry, registers it as `nenjim.registry.service`, and records that ID before constructing components whose constructors may query the Registry
#### Scenario: Registry is process-discoverable
- **WHEN** the Registry registration has completed
- **THEN** the same object is retrievable as both `NenjimRegistryService` and `NenjimProcess`
#### Scenario: Required hardcoded bindings are registered
- **WHEN** Hub composition completes
- **THEN** all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components and excluding the former Registry-manager ID
#### Scenario: Explicit startup subset preserves order
- **WHEN** the Hub starts processes after the Registry
- **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 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: 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** a 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
## MODIFIED Requirements
### 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: 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
## REMOVED Requirements
### Requirement: Components and applications have minimal common contracts
**Reason**: `NenjimApplication` incorrectly combines application role with active lifecycle classification and is replaced by the canonical `NenjimProcess` contract.
**Migration**: Modern startable implementations and typed Registry lookups use `com.r35157.nenjim.process.NenjimProcess`; service and application remain descriptive roles rather than common lifecycle interfaces.
### Requirement: Registry manager owns hardcoded composition and startup
**Reason**: Composition, startup policy, started-process state, the online point, and the blocking runtime lifecycle belong to one global NenjimHub, not to the Registry subsystem.
**Migration**: `NenjimHubImpl` owns the existing graph and startup sequence; `NenjimRegistryServiceManager` becomes an unregistered passive registration handle.
@@ -0,0 +1,26 @@
## 1. Lifecycle and Registry Boundaries
- [x] 1.1 Add the canonical minimal `NenjimProcess`, migrate every modern startable implementation and lookup, and remove `NenjimApplication` without an alias.
- [x] 1.2 Reduce the public Registry manager contract and reference implementation to passive registration only.
- [x] 1.3 Add the implementation-only Registry assembly that preserves service/process identity and keeps administration package-private.
- [x] 1.4 Strengthen changed API and internal-admin JavaDoc, including the warning against publicly exposing the Registry mutation interface.
## 2. Hub Composition and Runtime Ownership
- [x] 2.1 Add the public `NenjimHub` contract with single-start delegation and an immutable ordered started-process snapshot.
- [x] 2.2 Move the complete hardcoded component graph and all registration into `NenjimHubImpl` without changing component bindings.
- [x] 2.3 Start and record the Registry first, then preserve the exact enabled process order, inactive set, fail-fast behavior, online point, and blocking wait.
- [x] 2.4 Update `Main` to construct and start exactly one `NenjimHub` while keeping the configured entry point unchanged.
## 3. Documentation
- [x] 3.1 Update `docs/Nenjim.md` and `docs/Terminologi.md` to distinguish component, service, application role, process lifecycle, Hub ownership, and passive Registry administration.
- [x] 3.2 Update directly affected public HTML and operational documentation without changing deferred-feature or business behavior.
- [x] 3.3 Add ownership deltas for AssetAZ Currency Identity and Ticker so their composition requirements name `NenjimHub` rather than the Registry manager.
## 4. Verification
- [x] 4.1 Strictly validate the active OpenSpec change and all repository specs without synchronizing or archiving the change.
- [x] 4.2 Run the existing Gradle build and Detag/Java compilation without invoking production `main()`.
- [x] 4.3 Run safe temporary probes for Registry assembly identity/admin boundaries and pre-start immutable Hub snapshots, then remove all probe artifacts.
- [x] 4.4 Inspect the complete redacted diff, working tree, package references, generated-source status, acceptance criteria, and out-of-scope boundaries without committing or pushing.
@@ -3,9 +3,7 @@
## Purpose
Provides one authoritative AssetAZ identity model for currencies and translates stable UUIDs and namespaced external identifiers without treating display symbols as identity.
## Requirements
### Requirement: Stable AssetAZ currency identity
The system SHALL represent a currency as an immutable `CurrencyType` with a non-null UUID, name, and symbol. Its UUID is its complete stable identity. Equality and hash codes SHALL depend only on that UUID, regardless of name, symbol, instance identity, or metadata changes.
@@ -67,7 +65,7 @@ The Currency Identity Service SHALL return all configured external references fo
- **THEN** the result contains its configured Solana mint reference
### Requirement: Immutable conflict-free hardcoded catalogue
The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ currency UUIDs, maintain one current currency value for each UUID, support multiple external references per UUID, reject conflicting UUID or external-identity mappings during initialization, expose no public mutation operation, and be safe for concurrent reads after construction. The Nenjim Registry service manager SHALL create exactly one hardcoded service instance for the current hardcoded component graph, register it as `assetaz.currency-identity.hardcoded`, and SHALL pass that same instance to every component in that graph that requires currency identities.
The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ currency UUIDs, maintain one current currency value for each UUID, support multiple external references per UUID, reject conflicting UUID or external-identity mappings during initialization, expose no public mutation operation, and be safe for concurrent reads after construction. The NenjimHub SHALL create exactly one hardcoded service instance for the current hardcoded component graph, register it as `assetaz.currency-identity.hardcoded`, and SHALL pass that same instance to every component in that graph that requires currency identities.
#### Scenario: Conflicting external mapping
- **WHEN** initialization maps the same namespace and external identifier to two different AssetAZ UUIDs
@@ -78,7 +76,7 @@ The hardcoded Currency Identity Service SHALL preserve the existing AssetAZ curr
- **THEN** initialization fails with a clear conflict exception
#### Scenario: Compose hardcoded components
- **WHEN** the Registry service manager constructs the current hardcoded component graph
- **WHEN** the NenjimHub constructs the current hardcoded component graph
- **THEN** it creates and registers one hardcoded Currency Identity Service and injects that same instance into every component that requires it
### Requirement: Trading pairs use service-owned identities
@@ -3,14 +3,12 @@
## Purpose
Provide AssetAZ callers with typed latest prices backed by explicitly enabled, durable observation histories.
## Requirements
### Requirement: Price sources are discovered and lifecycle-managed
Nenjim SHALL be able to construct and register each price source independently in an initialized but unstarted state without supplying a ticker or sink. The ticker SHALL receive the public Nenjim Registry through constructor injection and resolve its explicitly configured `PriceSource` component IDs, while ordinary ticker clients SHALL NOT receive a public operation for adding or managing sources. The ticker SHALL fail clearly when a configured ID is missing or names a component that does not implement `PriceSource`; it SHALL NOT silently select the first source listed for the interface. The ticker SHALL identify each resolved source by its `TradingPair` and stable source name, reject duplicate identities clearly, start each active source by passing itself as `PriceSink`, and stop every source it started. Source names SHALL be non-empty safe directory components and SHALL reject `/`, `\`, `..`, and control characters without rewriting them. A source SHALL announce typed prices and timestamps only through the sink received at start and SHALL remain unaware of its persistence path. A repeated source start SHALL reject the call before replacing its existing sink.
#### Scenario: Sources are constructed independently
- **WHEN** the Registry manager constructs and registers a price source before constructing or starting the ticker
- **WHEN** the NenjimHub constructs and registers a price source before constructing or starting the ticker
- **THEN** source construction requires neither a `PriceSink` nor a ticker reference and leaves the source unstarted
#### Scenario: Ticker resolves configured source components
+108 -53
View File
@@ -3,24 +3,7 @@
## 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.
@@ -95,7 +78,7 @@ The internal Registry administration view SHALL remain package-private, SHALL no
- **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.
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
@@ -109,33 +92,6 @@ The Registry application's first `start()` call SHALL be the only accepted attem
- **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.
@@ -152,18 +108,22 @@ A non-bootstrap application or service that owns a component choice SHALL receiv
- **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.
`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 Registry manager as a Nenjim application and delegates startup to it
- **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 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
- **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`; 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.
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
@@ -171,11 +131,106 @@ Public component contracts SHALL live in `com.r35157.nenjim.component`; public R
#### 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
- **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 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 and manager contracts
- **THEN** only the agreed read-only lookup and manager start operations are available and no deferred feature is exposed
- **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 complete hardcoded component set from issue #76 in dependency order, resolve only the explicitly enabled 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.
#### Scenario: Registry starts before graph construction
- **WHEN** the Hub performs its first startup attempt
- **THEN** it starts the Registry, registers it as `nenjim.registry.service`, and records that ID before constructing components whose constructors may query the Registry
#### Scenario: Registry is process-discoverable
- **WHEN** the Registry registration has completed
- **THEN** the same object is retrievable as both `NenjimRegistryService` and `NenjimProcess`
#### Scenario: Required hardcoded bindings are registered
- **WHEN** Hub composition completes
- **THEN** all component IDs and implementation choices enumerated by issue #76 are present exactly as configured, including intentionally inactive components and excluding the former Registry-manager ID
#### Scenario: Explicit startup subset preserves order
- **WHEN** the Hub starts processes after the Registry
- **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 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: 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** a 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