7.6 KiB
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
NenjimHubImplthe unmistakable composition root and runtime lifecycle owner. - Keep Registry mutation inaccessible from ordinary
NenjimRegistryServiceconsumers 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
- Add the canonical process and Hub APIs plus the Registry assembly/passive manager boundary.
- Move the existing graph and runtime sequence into
NenjimHubImpl, replace modern lifecycle implementations/lookups, and updateMain. - Update directly affected architecture, terminology, HTML, and JavaDoc without changing archived OpenSpec history.
- 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.