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

This commit is contained in:
2026-09-02 14:10:17 +02:00
parent a252c256c4
commit 3dd050f9a8
24 changed files with 838 additions and 401 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,30 @@
## 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.
## 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,154 @@
## 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: 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 runtime APIs
- **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,25 @@
## 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.
## 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.