79: Integrate NenjimProcessManager with NenjimHub runtime status

This commit is contained in:
2026-09-02 21:51:05 +02:00
parent 7b1f6d18eb
commit 0cb125ca9d
11 changed files with 552 additions and 21 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-09-02
@@ -0,0 +1,116 @@
## Context
`NenjimHub` already constructs the runtime graph, starts explicitly enabled
`NenjimProcess` objects, and records each component ID only after `start()`
returns successfully. Its immutable `getStartedProcessIds()` snapshot is the
authoritative successful-start history. The old Process Manager is a dormant
JavaFX window with three hardcoded labels and no Hub dependency.
The existing `JavaFxRuntime` owns JavaFX toolkit initialization and schedules
work with `Platform.runLater`. Nenjim has no general stop lifecycle, so this
change must not add a Process Manager-specific public stop operation.
## Goals / Non-Goals
**Goals:**
- Make the Process Manager a normally registered and started Hub process.
- Display exactly the ordered component-ID values reported by its owning Hub.
- Poll outside the JavaFX application thread immediately and every three
seconds.
- Replace the complete JavaFX list on the JavaFX application thread after
every successful poll.
- Keep polling after ordinary failures and terminate cleanly on interruption.
- Allow the JVM to terminate despite the polling worker.
**Non-Goals:**
- Lifecycle events, subscriptions, push delivery, controls, configurable
autorun, health or liveness monitoring, supervision, history, timestamps,
display aliases, icons, colours, persistence, Contexts, classloading,
Journal or Registry changes, or a general stop lifecycle.
- General unit-test infrastructure or NenjimTestTool functionality.
## Decisions
### The Process Manager is an ordinary specialized process
`NenjimProcessManager` extends the canonical `NenjimProcess` and adds no
methods. `NenjimProcessManagerImpl` requires and retains a non-null
`NenjimHub`, which is the only source of displayed runtime status. Its startup
has a one-attempt guard so one Process Manager instance cannot create multiple
polling loops.
### Hub composition and startup use the existing path
During application composition, `NenjimHubImpl` constructs the Process Manager
with `this` and registers it as `nenjim.process-manager.default`. After all
currently enabled business processes have started, the Hub resolves and starts
the Process Manager through the same `startProcess` helper. The helper records
its ID only after `start()` returns, with no Process Manager-specific status
rule.
The first poll can race with the Hub's post-start recording and therefore need
not contain the Process Manager's own ID. Repeated complete snapshots make the
ID appear naturally on a later successful poll.
### One daemon worker performs complete-snapshot polling
`start()` first asks the shared JavaFX runtime to show the existing window and
then starts one named daemon thread. The worker polls before its first wait and
sleeps three seconds between attempts. Daemon status prevents this temporary
observer from keeping the JVM alive in the absence of a common stop lifecycle.
Each attempt obtains one immutable Hub snapshot and maps every ID to `value()`
before scheduling publication. No local additions/removals are inferred, so
Hub ordering and snapshot authority are preserved.
### JavaFX publication replaces the entire list
Window construction and list replacement are both scheduled through
`JavaFxRuntime`. The window task is queued before the polling worker starts,
and each successful poll schedules `setAll` for the complete mapped snapshot.
The title, dimensions, and multiple-selection behavior remain unchanged.
### Failures retain the last successful display
A runtime failure while querying, mapping, or scheduling a polled snapshot is
logged and does not enqueue partial data. A JavaFX publication failure is also
logged on the JavaFX thread. The worker waits the ordinary interval and retries
instead of accelerating or terminating. An interrupted sleep restores the
thread's interrupted status and returns from the polling loop.
Synchronous JavaFX runtime initialization, task scheduling, and worker-start
failures from `start()` remain ordinary process-start failures and propagate
through the Hub's existing fail-fast path.
### Polling is intentionally replaceable infrastructure
Polling is the delivered observation mechanism, not part of the Process
Manager's public API. A later change can replace it with Hub events without
changing the Process Manager's responsibility or list presentation model; no
event type or subscription behavior is specified here.
## Risks / Trade-offs
- [The immediate poll may precede the Hub recording the Process Manager] → The
next normal poll obtains the later authoritative snapshot and displays the
manager's ID.
- [A closed window does not stop the worker] → No component-specific stop
lifecycle is added; the daemon remains harmless to JVM termination and keeps
the view current if the JavaFX window remains present.
- [Polling has bounded staleness] → Three seconds is explicitly required for
this first vertical slice and keeps event design out of scope.
- [JavaFX tasks are asynchronous] → The shared runtime preserves JavaFX thread
confinement, while synchronous startup failures still follow Hub fail-fast
behavior.
## Migration Plan
1. Specialize the Process Manager contract and inject its owning Hub.
2. Replace hardcoded rows with daemon-backed ordered snapshot polling and
JavaFX-thread publication.
3. Register the Process Manager during Hub composition and start it last via
the common lifecycle helper.
4. Strictly validate this active OpenSpec change and build the repository,
leaving synchronization and archival until review acceptance.
@@ -0,0 +1,40 @@
## Why
Issues #77 and #78 made `NenjimHub` the owner of runtime composition,
process startup, and immutable started-process snapshots, but the existing
Process Manager is still disconnected from that runtime. It is neither
Hub-managed nor backed by the Hub's truthful process IDs.
## What Changes
- Make `NenjimProcessManager` a specialized canonical `NenjimProcess`.
- Inject the owning `NenjimHub` into the reference Process Manager.
- Construct and register the Process Manager as
`nenjim.process-manager.default`, then start it last through the Hub's
ordinary process-start mechanism.
- Replace the hardcoded JavaFX rows with complete ordered snapshots of
`NenjimComponentId.value()` strings from the Hub.
- Poll immediately and every three seconds on one daemon background worker,
while publishing all JavaFX mutations through the existing runtime helper.
- Retain the last successful display after a polling failure and stop the
worker cleanly when interrupted.
## Capabilities
### Added Capabilities
- `nenjim-process-manager`: define the Hub-managed Process Manager lifecycle,
polling, JavaFX publication, failure handling, and displayed-ID semantics.
### Modified Capabilities
- `nenjim-component-registry`: extend Hub composition and successful startup
order with the Process Manager while preserving the common lifecycle rule.
## Impact
Affected code is limited to the Process Manager contract and reference
implementation, the Hub composition root, and this active OpenSpec change.
Polling is a temporary observation mechanism; no events, controls, health
model, metadata, persistence, stop lifecycle, test framework, or deployment
change is introduced.
@@ -0,0 +1,78 @@
## MODIFIED Requirements
### 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. Except for the Process Manager's read-only polling of the existing started-process snapshot, the Hub SHALL not add configurable autorun, lifecycle events, subscriptions, process controls, 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, Hub, and Process Manager contracts
- **THEN** only read-only Registry lookup, passive registration, minimal process startup, Hub startup/snapshots, and the Process Manager role are available and no deferred runtime feature is exposed
### 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. During application composition the Hub SHALL construct the Process Manager with itself and register it as `nenjim.process-manager.default`. The Hub SHALL resolve only the explicitly enabled IDs through `NenjimProcess`, start them in dependency-safe order with the Process Manager last, announce the existing online point, and preserve the existing blocking wait. Registration SHALL never imply startup, and 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: Process Manager is process-discoverable
- **WHEN** Hub-owned application composition has completed
- **THEN** the object constructed with the current Hub is retrievable as both `NenjimProcessManager` and `NenjimProcess` under `nenjim.process-manager.default`
#### Scenario: Required hardcoded bindings are registered
- **WHEN** Hub composition completes
- **THEN** Journal, the Process Manager, 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 Process Manager 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`, `evelyn.mission-control.default`, and `nenjim.process-manager.default`
#### Scenario: Deliberately inactive processes remain inactive
- **WHEN** Hub startup reaches its online point
- **THEN** `jupiter-perps-alarm.default` and the dormant Composer, 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, including `nenjim.process-manager.default`. 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: Process Manager uses the common recording rule
- **WHEN** Process Manager `start()` returns successfully
- **THEN** the Hub records `nenjim.process-manager.default` through the same post-start rule used for every other process
#### 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
@@ -0,0 +1,88 @@
## ADDED Requirements
### Requirement: Process Manager is a Hub-managed process
`com.r35157.nenjim.npm.NenjimProcessManager` SHALL extend the canonical `NenjimProcess` and SHALL add no public refresh, polling, event, start, stop, or query operation. `NenjimProcessManagerImpl` SHALL require and retain its owning non-null `NenjimHub` as an explicit constructor dependency and SHALL use that Hub as the sole source of displayed process status.
#### Scenario: Process Manager is used through the common lifecycle
- **WHEN** the registered Process Manager is resolved through `NenjimProcess`
- **THEN** its inherited no-argument `start()` is the only public lifecycle operation used by the Hub
#### Scenario: Owning Hub is required
- **WHEN** reference Process Manager construction receives a null Hub
- **THEN** construction fails with `NullPointerException` before startup
#### Scenario: Public API remains minimal
- **WHEN** a consumer examines `NenjimProcessManager`
- **THEN** it exposes no Process Manager-specific refresh, polling, event, stop, status, or query method
### Requirement: Process Manager polls immediately on one non-blocking worker
On its accepted startup attempt, the Process Manager SHALL start or reuse the existing JavaFX runtime mechanism, request the existing Process Manager window, and start exactly one dedicated background polling loop. The loop SHALL request `NenjimHub.getStartedProcessIds()` immediately and again after each normal three-second interval. Polling work SHALL not execute on the JavaFX application thread, and the worker SHALL be a daemon that does not prevent JVM termination.
#### Scenario: First snapshot is immediate
- **WHEN** Process Manager startup schedules its JavaFX window and starts its polling worker
- **THEN** the worker requests a Hub snapshot before waiting for the first three-second interval
#### Scenario: Polling continues at the required interval
- **WHEN** one polling attempt completes without interruption
- **THEN** the same worker waits three seconds and performs the next attempt
#### Scenario: Polling is not JavaFX work
- **WHEN** the Hub snapshot is queried and mapped
- **THEN** that work runs on the dedicated background polling thread rather than the JavaFX application thread
#### Scenario: Worker does not own JVM lifetime
- **WHEN** only the Process Manager polling worker would otherwise keep the JVM alive
- **THEN** its daemon status permits normal JVM termination
### Requirement: Successful snapshots replace the JavaFX list exactly
Each successful poll SHALL map every returned `NenjimComponentId` to its exact `value()` string in Hub-provided order and SHALL publish that complete point-in-time list through the existing JavaFX runtime integration. Every JavaFX control mutation SHALL execute on the JavaFX application thread, and publication SHALL replace the complete displayed list rather than infer additions or removals. The existing title, dimensions, and multiple-selection behavior MAY remain unchanged, and no hardcoded process rows SHALL remain.
#### Scenario: Ordered ID values are displayed
- **WHEN** the Hub returns an ordered snapshot of component IDs
- **THEN** the JavaFX list is replaced with the corresponding `value()` strings in the same order
#### Scenario: A later snapshot removes an ID
- **WHEN** a successful later snapshot omits an ID present in the previously displayed snapshot
- **THEN** complete replacement removes that string without the Process Manager maintaining authoritative lifecycle state
#### Scenario: Process Manager eventually displays itself
- **WHEN** the Hub records `nenjim.process-manager.default` after its `start()` returns successfully
- **THEN** a later successful poll displays that ID in the position supplied by the Hub
#### Scenario: JavaFX controls remain thread-confined
- **WHEN** the window is created or a successful snapshot is published
- **THEN** all JavaFX control creation and mutation executes through the existing JavaFX runtime on the JavaFX application thread
### Requirement: Polling failures preserve the last successful view
A polling failure SHALL be logged clearly, SHALL enqueue no invented or partial replacement, SHALL not terminate the JavaFX application thread, and SHALL leave the last successfully displayed snapshot visible. The background loop SHALL retry after the next normal three-second interval. If background waiting is interrupted, the worker SHALL restore its interrupted status and terminate the polling loop cleanly. Synchronous failures from the Process Manager's `start()` SHALL remain ordinary fail-fast Hub process-start failures.
#### Scenario: Hub polling fails
- **WHEN** obtaining or mapping a Hub snapshot throws a runtime failure
- **THEN** the failure is logged, the displayed list is unchanged, and the worker retries after the normal interval
#### Scenario: JavaFX publication fails
- **WHEN** publishing a successfully polled snapshot fails on the JavaFX application thread
- **THEN** the failure is logged without deliberately terminating that application thread or publishing invented fallback data
#### Scenario: Polling worker is interrupted
- **WHEN** the background worker is interrupted while waiting for its next poll
- **THEN** it restores its interrupted status and exits without another polling attempt
#### Scenario: Initial process start fails
- **WHEN** a synchronous failure prevents `NenjimProcessManager.start()` from completing
- **THEN** the failure propagates through the Hub, the Process Manager ID is not recorded, and no later Hub process is started
### Requirement: Displayed IDs are startup milestones only
The Process Manager list SHALL mean exactly the component IDs in the latest successfully published `NenjimHub.getStartedProcessIds()` snapshot. It SHALL make no claim about current health, worker activity, or continued liveness and SHALL add no names, aliases, metadata, timestamps, controls, icons, colours, history, or persistence. Polling SHALL be treated as a temporary internal observation mechanism without specifying Hub events as delivered functionality.
#### Scenario: Consumer interprets a displayed ID
- **WHEN** a component ID appears in the Process Manager list
- **THEN** it means that the Hub recorded a successful return from that process's `start()` and does not guarantee current health or liveness
#### Scenario: Presentation remains limited to IDs
- **WHEN** the Process Manager renders a Hub snapshot
- **THEN** every row contains only the exact component-ID value with no translated name or additional process metadata
#### Scenario: Event functionality remains absent
- **WHEN** the delivered Process Manager implementation is inspected
- **THEN** it observes the Hub by polling and exposes no event type, subscription, or push-update contract
@@ -0,0 +1,26 @@
## 1. Process Manager contract and polling
- [x] 1.1 Make `NenjimProcessManager` extend the canonical `NenjimProcess`
without adding lifecycle or status methods.
- [x] 1.2 Require and retain a non-null owning `NenjimHub` in the reference
implementation.
- [x] 1.3 Replace hardcoded rows with immediate and three-second complete
snapshot polling on one daemon background worker.
- [x] 1.4 Publish ordered `NenjimComponentId.value()` snapshots with JavaFX
`setAll`, logging failures and terminating cleanly on interruption.
## 2. Hub integration
- [x] 2.1 Construct and register the Process Manager as
`nenjim.process-manager.default` during Hub-owned application composition.
- [x] 2.2 Start it after every currently enabled business process through the
common `startProcess` lifecycle and successful-history rule.
## 3. OpenSpec and verification
- [x] 3.1 Add Process Manager capability requirements and only the affected
Hub/Registry deltas to this active change.
- [x] 3.2 Strictly validate this active change and all canonical specs without
synchronizing or archiving it.
- [x] 3.3 Run the complete Gradle build and inspect the final diff against issue
#79's acceptance criteria without invoking production `main()`.
@@ -138,11 +138,11 @@ Public component contracts SHALL live in `com.r35157.nenjim.component`; the cano
- **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.
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. Except for the Process Manager's read-only polling of the existing started-process snapshot, the Hub SHALL not add configurable autorun, lifecycle events, subscriptions, process controls, 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
- **WHEN** a consumer examines the public Registry, Registry-manager, process, Hub, and Process Manager contracts
- **THEN** only read-only Registry lookup, passive registration, minimal process startup, Hub startup/snapshots, and the Process Manager role are available and no deferred runtime 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.
@@ -186,15 +186,15 @@ The reference Registry construction boundary SHALL create one Registry object ex
- **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.
`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. During application composition the Hub SHALL construct the Process Manager with itself and register it as `nenjim.process-manager.default`. The Hub SHALL resolve only the explicitly enabled IDs through `NenjimProcess`, start them in dependency-safe order with the Process Manager last, announce the existing online point, and preserve the existing blocking wait. Registration SHALL never imply startup, and 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
- **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
- **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
@@ -204,17 +204,21 @@ The reference Registry construction boundary SHALL create one Registry object ex
- **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: Process Manager is process-discoverable
- **WHEN** Hub-owned application composition has completed
- **THEN** the object constructed with the current Hub is retrievable as both `NenjimProcessManager` and `NenjimProcess` under `nenjim.process-manager.default`
#### 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
- **THEN** Journal, the Process Manager, 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`
- **WHEN** Hub startup succeeds through the Process Manager 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`, `evelyn.mission-control.default`, and `nenjim.process-manager.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
- **THEN** `jupiter-perps-alarm.default` and the dormant Composer, Test Tool, Soda Task Manager, and Suwimo Client paths have not been started
#### Scenario: Journal failure is fail-fast
- **WHEN** Journal process startup throws
@@ -229,7 +233,7 @@ The reference Registry construction boundary SHALL create one Registry object ex
- **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.
`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, including `nenjim.process-manager.default`. 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
@@ -239,6 +243,10 @@ The reference Registry construction boundary SHALL create one Registry object ex
- **WHEN** a process's `start()` call returns successfully
- **THEN** its ID appears after all previously successful process IDs in every later snapshot
#### Scenario: Process Manager uses the common recording rule
- **WHEN** Process Manager `start()` returns successfully
- **THEN** the Hub records `nenjim.process-manager.default` through the same post-start rule used for every other process
#### 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
@@ -0,0 +1,92 @@
# nenjim-process-manager Specification
## Purpose
TBD - created by archiving change 79-integrate-process-manager-runtime-status. Update Purpose after archive.
## Requirements
### Requirement: Process Manager is a Hub-managed process
`com.r35157.nenjim.npm.NenjimProcessManager` SHALL extend the canonical `NenjimProcess` and SHALL add no public refresh, polling, event, start, stop, or query operation. `NenjimProcessManagerImpl` SHALL require and retain its owning non-null `NenjimHub` as an explicit constructor dependency and SHALL use that Hub as the sole source of displayed process status.
#### Scenario: Process Manager is used through the common lifecycle
- **WHEN** the registered Process Manager is resolved through `NenjimProcess`
- **THEN** its inherited no-argument `start()` is the only public lifecycle operation used by the Hub
#### Scenario: Owning Hub is required
- **WHEN** reference Process Manager construction receives a null Hub
- **THEN** construction fails with `NullPointerException` before startup
#### Scenario: Public API remains minimal
- **WHEN** a consumer examines `NenjimProcessManager`
- **THEN** it exposes no Process Manager-specific refresh, polling, event, stop, status, or query method
### Requirement: Process Manager polls immediately on one non-blocking worker
On its accepted startup attempt, the Process Manager SHALL start or reuse the existing JavaFX runtime mechanism, request the existing Process Manager window, and start exactly one dedicated background polling loop. The loop SHALL request `NenjimHub.getStartedProcessIds()` immediately and again after each normal three-second interval. Polling work SHALL not execute on the JavaFX application thread, and the worker SHALL be a daemon that does not prevent JVM termination.
#### Scenario: First snapshot is immediate
- **WHEN** Process Manager startup schedules its JavaFX window and starts its polling worker
- **THEN** the worker requests a Hub snapshot before waiting for the first three-second interval
#### Scenario: Polling continues at the required interval
- **WHEN** one polling attempt completes without interruption
- **THEN** the same worker waits three seconds and performs the next attempt
#### Scenario: Polling is not JavaFX work
- **WHEN** the Hub snapshot is queried and mapped
- **THEN** that work runs on the dedicated background polling thread rather than the JavaFX application thread
#### Scenario: Worker does not own JVM lifetime
- **WHEN** only the Process Manager polling worker would otherwise keep the JVM alive
- **THEN** its daemon status permits normal JVM termination
### Requirement: Successful snapshots replace the JavaFX list exactly
Each successful poll SHALL map every returned `NenjimComponentId` to its exact `value()` string in Hub-provided order and SHALL publish that complete point-in-time list through the existing JavaFX runtime integration. Every JavaFX control mutation SHALL execute on the JavaFX application thread, and publication SHALL replace the complete displayed list rather than infer additions or removals. The existing title, dimensions, and multiple-selection behavior MAY remain unchanged, and no hardcoded process rows SHALL remain.
#### Scenario: Ordered ID values are displayed
- **WHEN** the Hub returns an ordered snapshot of component IDs
- **THEN** the JavaFX list is replaced with the corresponding `value()` strings in the same order
#### Scenario: A later snapshot removes an ID
- **WHEN** a successful later snapshot omits an ID present in the previously displayed snapshot
- **THEN** complete replacement removes that string without the Process Manager maintaining authoritative lifecycle state
#### Scenario: Process Manager eventually displays itself
- **WHEN** the Hub records `nenjim.process-manager.default` after its `start()` returns successfully
- **THEN** a later successful poll displays that ID in the position supplied by the Hub
#### Scenario: JavaFX controls remain thread-confined
- **WHEN** the window is created or a successful snapshot is published
- **THEN** all JavaFX control creation and mutation executes through the existing JavaFX runtime on the JavaFX application thread
### Requirement: Polling failures preserve the last successful view
A polling failure SHALL be logged clearly, SHALL enqueue no invented or partial replacement, SHALL not terminate the JavaFX application thread, and SHALL leave the last successfully displayed snapshot visible. The background loop SHALL retry after the next normal three-second interval. If background waiting is interrupted, the worker SHALL restore its interrupted status and terminate the polling loop cleanly. Synchronous failures from the Process Manager's `start()` SHALL remain ordinary fail-fast Hub process-start failures.
#### Scenario: Hub polling fails
- **WHEN** obtaining or mapping a Hub snapshot throws a runtime failure
- **THEN** the failure is logged, the displayed list is unchanged, and the worker retries after the normal interval
#### Scenario: JavaFX publication fails
- **WHEN** publishing a successfully polled snapshot fails on the JavaFX application thread
- **THEN** the failure is logged without deliberately terminating that application thread or publishing invented fallback data
#### Scenario: Polling worker is interrupted
- **WHEN** the background worker is interrupted while waiting for its next poll
- **THEN** it restores its interrupted status and exits without another polling attempt
#### Scenario: Initial process start fails
- **WHEN** a synchronous failure prevents `NenjimProcessManager.start()` from completing
- **THEN** the failure propagates through the Hub, the Process Manager ID is not recorded, and no later Hub process is started
### Requirement: Displayed IDs are startup milestones only
The Process Manager list SHALL mean exactly the component IDs in the latest successfully published `NenjimHub.getStartedProcessIds()` snapshot. It SHALL make no claim about current health, worker activity, or continued liveness and SHALL add no names, aliases, metadata, timestamps, controls, icons, colours, history, or persistence. Polling SHALL be treated as a temporary internal observation mechanism without specifying Hub events as delivered functionality.
#### Scenario: Consumer interprets a displayed ID
- **WHEN** a component ID appears in the Process Manager list
- **THEN** it means that the Hub recorded a successful return from that process's `start()` and does not guarantee current health or liveness
#### Scenario: Presentation remains limited to IDs
- **WHEN** the Process Manager renders a Hub snapshot
- **THEN** every row contains only the exact component-ID value with no translated name or additional process metadata
#### Scenario: Event functionality remains absent
- **WHEN** the delivered Process Manager implementation is inspected
- **THEN** it observes the Hub by polling and exposes no event type, subscription, or push-update contract
@@ -28,6 +28,8 @@ import com.r35157.libs.objcache.impl.ref.ObjectCacheImpl;
import com.r35157.libs.solana.impl.cached.CachedSolanaBlockChain;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.nenjim.hubd.NenjimHub;
import com.r35157.nenjim.npm.NenjimProcessManager;
import com.r35157.nenjim.npm.impl.ref.NenjimProcessManagerImpl;
import com.r35157.nenjim.process.NenjimProcess;
import com.r35157.nenjim.service.journal.JournalService;
import com.r35157.nenjim.service.journal.JournalServiceManager;
@@ -94,6 +96,7 @@ public final class NenjimHubImpl implements NenjimHub {
startProcess(registryService, EVELYN_SERVICE_TEST_ID);
startProcess(registryService, EVELYN_IOU_BURNER_PROD_ID);
startProcess(registryService, EVELYN_MISSION_CONTROL_ID);
startProcess(registryService, PROCESS_MANAGER_ID);
System.out.println("Done - Now online!");
@@ -278,6 +281,11 @@ public final class NenjimHubImpl implements NenjimHub {
registryServiceManager.registerComponent(
EVELYN_MISSION_CONTROL_ID,
missionControl);
processManager = new NenjimProcessManagerImpl(this);
registryServiceManager.registerComponent(
PROCESS_MANAGER_ID,
processManager);
}
private void startProcess(
@@ -356,6 +364,8 @@ public final class NenjimHubImpl implements NenjimHub {
new NenjimComponentId("evelyn.iou-burner.prod");
private static final NenjimComponentId EVELYN_MISSION_CONTROL_ID =
new NenjimComponentId("evelyn.mission-control.default");
private static final NenjimComponentId PROCESS_MANAGER_ID =
new NenjimComponentId("nenjim.process-manager.default");
private static final ΩRaydiumLiquidityPoolIdΩ EVE_USDT_RAYDIUM_POOL_ID =
"8rN4BTEzbogQosEQYgsEu18XwfKS5Yoxqwit8zEVFwEe";
private final CountDownLatch shutdownLatch = new CountDownLatch(1);
@@ -395,4 +405,5 @@ public final class NenjimHubImpl implements NenjimHub {
// Application components
private EvelynMissionControl missionControl;
private NenjimProcessManager processManager;
}
@@ -1,5 +1,7 @@
package com.r35157.nenjim.npm;
public interface NenjimProcessManager {
void start();
import com.r35157.nenjim.process.NenjimProcess;
/** Hub-managed process that presents the runtime's started-process snapshot. */
public interface NenjimProcessManager extends NenjimProcess {
}
@@ -1,31 +1,99 @@
package com.r35157.nenjim.npm.impl.ref;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.nenjim.hubd.NenjimHub;
import com.r35157.nenjim.npm.NenjimProcessManager;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.stage.Stage;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
public final class NenjimProcessManagerImpl implements NenjimProcessManager {
public NenjimProcessManagerImpl(@NotNull NenjimHub nenjimHub) {
this.nenjimHub = Objects.requireNonNull(nenjimHub, "nenjimHub");
}
@Override
public void start() {
if (!startAttempted.compareAndSet(false, true)) {
throw new IllegalStateException(
"Nenjim Process Manager start has already been attempted");
}
JavaFxRuntime.runLater(this::showWindow);
Thread pollingWorker = new Thread(
this::pollStartedProcesses,
"nenjim-process-manager-poller");
pollingWorker.setDaemon(true);
pollingWorker.start();
}
private void showWindow() {
var processes = new ListView<String>();
processes.getItems().addAll(
"Nenjim Composer",
"Nenjim Process Manager (this)",
"Evelyn Mission Control"
);
processes.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
processList = new ListView<>();
processList.getSelectionModel().setSelectionMode(
SelectionMode.MULTIPLE);
var window = new Stage();
window.setTitle("Nenjim Process Manager");
window.setScene(new Scene(processes, 500, 400));
window.setScene(new Scene(processList, 500, 400));
window.show();
}
private void pollStartedProcesses() {
while (!Thread.currentThread().isInterrupted()) {
publishLatestSnapshot();
try {
Thread.sleep(POLL_INTERVAL);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
log.info("Nenjim Process Manager polling was interrupted");
return;
}
}
}
private void publishLatestSnapshot() {
try {
List<String> processIds = nenjimHub.getStartedProcessIds()
.stream()
.map(componentId -> componentId.value())
.toList();
JavaFxRuntime.runLater(
() -> replaceDisplayedSnapshot(processIds));
} catch (RuntimeException failure) {
log.error(
"Failed to poll Nenjim Hub started-process snapshot; "
+ "the last displayed snapshot is retained",
failure);
}
}
private void replaceDisplayedSnapshot(List<String> processIds) {
try {
processList.getItems().setAll(processIds);
} catch (RuntimeException failure) {
log.error(
"Failed to display the latest Nenjim Hub "
+ "started-process snapshot",
failure);
}
}
private static final Logger log = LoggerFactory.getLogger(
NenjimProcessManagerImpl.class);
private static final Duration POLL_INTERVAL = Duration.ofSeconds(3);
private final NenjimHub nenjimHub;
private final AtomicBoolean startAttempted = new AtomicBoolean();
private ListView<String> processList;
}