88: Rename Evelyn to EvelynStatusService and expose the calculated reference price

This commit is contained in:
2026-10-04 19:42:21 +02:00
parent 035a01c48c
commit 9c636bc93d
21 changed files with 788 additions and 111 deletions
+14
View File
@@ -49,6 +49,20 @@ Nenjim Hub reads only `conf/NenjimHub.conf`; copy
`conf/NenjimHub.conf.example` before first use and edit the ordered process `conf/NenjimHub.conf.example` before first use and edit the ordered process
list for the deployment role. The example is never loaded as runtime input. list for the deployment role. The example is never loaded as runtime input.
The Evelyn status processes use `evelyn.status.prod` and `evelyn.status.test`.
When upgrading an existing deployment, replace `evelyn.service.prod` and
`evelyn.service.test` with those IDs in private `conf/NenjimHub.conf`, preserving
their order and enabled/commented selections. The instance names `Production`
and `Test`, their `data/evelyn/<instance-name>/status.log` files, and history
format version 1 remain unchanged.
`com.r35157.evelyn.service.status.EvelynStatusService` exposes immutable status
history and `AssetPrice getReferencePrice(Instant timestamp)`. The reference
price is calculated on demand for the supplied instant and carries the canonical
EVE/USDT trading pair. It works before startup and while sampling is stopped,
without accessing Ticker or history storage. See [Evelyn](docs/Evelyn.md) for the
curve and service contract.
## Release log: ## Release log:
| Date | Version | Description | | Date | Version | Description |
|:-----------------|:--------------:|----------------------| |:-----------------|:--------------:|----------------------|
+33
View File
@@ -28,6 +28,7 @@
* [8. Risici, fail-safes og antifragilitet](#8-risici-fail-safes-og-antifragilitet) * [8. Risici, fail-safes og antifragilitet](#8-risici-fail-safes-og-antifragilitet)
* [9. Udvikling, YouTrack-integration og offentlig gennemsigtighed](#9-udvikling-youtrack-integration-og-offentlig-gennemsigtighed) * [9. Udvikling, YouTrack-integration og offentlig gennemsigtighed](#9-udvikling-youtrack-integration-og-offentlig-gennemsigtighed)
* [10. Fremtidige emner / TODO](#10-fremtidige-emner--todo) * [10. Fremtidige emner / TODO](#10-fremtidige-emner--todo)
* [11. Statusservice og referencepris](#11-statusservice-og-referencepris)
<!-- TOC --> <!-- TOC -->
## 1. Introduktion formål og vision ## 1. Introduktion formål og vision
@@ -301,3 +302,35 @@ kapitalallokering over tid (AI-drevet tuning).
ekstreme markedsbevægelser. ekstreme markedsbevægelser.
- Open API for ekstern integration: Gør det muligt at tilføje eksterne datafeeds, udveksle signaler med andre - Open API for ekstern integration: Gør det muligt at tilføje eksterne datafeeds, udveksle signaler med andre
systemer, eller automatisere funding-opgaver. systemer, eller automatisere funding-opgaver.
## 11. Statusservice og referencepris
Evelyn er navnet på det samlede system. `com.r35157.evelyn.service.status.EvelynStatusService` indsamler og
persisterer statusindeks for én navngiven instans og giver consumers adgang til den samme referencepris, som
Evelyn Price Index bruger. Referenceimplementeringen er `EvelynStatusServiceImpl` under
`com.r35157.evelyn.service.status.impl.ref`. Hub'en ejer fortsat startup af de enkelte processer.
`AssetPrice getReferencePrice(Instant timestamp)` beregner prisen for det angivne tidspunkt:
```text
Start: 2026-07-31T22:00:00Z
Startpris: 15.00 USDT pr. EVE
Årlig faktor: 1.20
Årslængde: 31,536,000 sekunder
referencePrice = 15.00 × 1.20^(elapsedSeconds / 31,536,000)
```
Resultatet indeholder det kanoniske EVE/USDT-tradingpar, så quote-valutaen er eksplicit. Prisen er 15 ved start og
18 efter ét 365-dages år. Statusindekset bruger `((actualPrice / referencePrice) - 1) × 10` med samme samplingtidspunkt
og samme beregning og konstanter. Den eksisterende konvention om at behandle USDT som USD i indekset bevares.
Referenceprisen beregnes ved hvert kald uden tickeropslag, historikadgang, workers eller ændring af historikken.
Kaldet virker før sampling starter og mens den er stoppet, også uden provisionerede historikfiler. `null` afvises
med `NullPointerException`, og tidspunkter før kurvens start afvises med `IllegalArgumentException`.
Production og Test bevarer separate navne, historikker, livscyklusser og Mission Control-charts. Historikfilerne
ligger fortsat i `data/evelyn/<instance-name>/status.log` med formatversion 1. Sampling sker fortsat med cirka ét
minuts fast forsinkelse, og et punkt persisteres før publicering i et immutable snapshot. `stop()` rydder kun
hukommelsen og beholder navnereservationen, restart genindlæser filen, og `close()` frigiver navnet efter sikker
terminering. `EvelynStatusIndexPoint` bliver i sin eksisterende package med alle seks indeksfelter; kun Evelyn
Price Index beregnes og vises, mens de øvrige fem felter fortsat er nul. Legacy-rebalancering er fortsat inaktiv.
+9 -2
View File
@@ -268,14 +268,21 @@ nenjim.registry.service
nenjim.journal.service nenjim.journal.service
assetaz.ticker.default assetaz.ticker.default
jupiter-perps-alarm.default jupiter-perps-alarm.default
evelyn.service.prod evelyn.status.prod
evelyn.service.test evelyn.status.test
evelyn.iou-burner.prod evelyn.iou-burner.prod
nenjim.javafx-runtime.service nenjim.javafx-runtime.service
evelyn.mission-control.default evelyn.mission-control.default
nenjim.process-manager.default nenjim.process-manager.default
``` ```
Ved opgradering erstattes `evelyn.service.prod` med `evelyn.status.prod` og
`evelyn.service.test` med `evelyn.status.test` i deploymentets private `conf/NenjimHub.conf`, hvor de forekommer.
Bevar rækkefølge og aktive/udkommenterede valg. De gamle ID'er accepteres ikke længere. Hub'en konstruerer samme
navngivne `Production`- og `Test`-instanser gennem `EvelynStatusServiceImpl` og injicerer deres
`EvelynStatusService`-views i Mission Control. Process Manager viser de nye ID'er fra Hub'ens startup-snapshot.
Historik ligger fortsat i `data/evelyn/<instance-name>/status.log` med formatversion 1 og kræver ingen migration.
Efter validering konstruerer og starter Hub'en Registry, registrerer det som `nenjim.registry.service` og registrerer Efter validering konstruerer og starter Hub'en Registry, registrerer det som `nenjim.registry.service` og registrerer
startup-milepælen. Derefter konstrueres og registreres hele den nuværende passive graph uden at starte workers eller startup-milepælen. Derefter konstrueres og registreres hele den nuværende passive graph uden at starte workers eller
processer. Først herefter behandles resten af `[AUTO_START]` i den angivne rækkefølge, begyndende med Journal. Hver processer. Først herefter behandles resten af `[AUTO_START]` i den angivne rækkefølge, begyndende med Journal. Hver
+1
View File
@@ -48,6 +48,7 @@
| JavaFxRuntimeService | Registrerbart service-view med kun non-null `runLater(Runnable)`. Samme referenceimplementation er en Hub-styret `NenjimProcess`; service-consumers får den gennem constructor injection, men får ingen lifecycle-, status-, configuration-, window-, event-, stop- eller restart-operationer. | | JavaFxRuntimeService | Registrerbart service-view med kun non-null `runLater(Runnable)`. Samme referenceimplementation er en Hub-styret `NenjimProcess`; service-consumers får den gennem constructor injection, men får ingen lifecycle-, status-, configuration-, window-, event-, stop- eller restart-operationer. |
| TickerService | Lifecycle-frit domain/query-view med kun latest-price lookup. Samme `TickerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; det ejer `PriceSource`-startup internt og stopper allerede startede sources som rollback, hvis Ticker-start fejler, men eksponerer ingen almindelig start- eller stop-operation. | | TickerService | Lifecycle-frit domain/query-view med kun latest-price lookup. Samme `TickerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; det ejer `PriceSource`-startup internt og stopper allerede startede sources som rollback, hvis Ticker-start fejler, men eksponerer ingen almindelig start- eller stop-operation. |
| EvelynIOUBurnerService | Tomt, lifecycle-frit domain-service-view, som bevarer typed Registry- og dependency-identitet, indtil eksterne domain-operationer findes. Samme `EvelynIOUBurnerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; service-consumers får ingen start-, stop- eller restart-operationer. | | EvelynIOUBurnerService | Tomt, lifecycle-frit domain-service-view, som bevarer typed Registry- og dependency-identitet, indtil eksterne domain-operationer findes. Samme `EvelynIOUBurnerServiceImpl`-objekt er separat en Hub-styret `NenjimProcess`; service-consumers får ingen start-, stop- eller restart-operationer. |
| EvelynStatusService | Service for én navngiven Evelyn-instans, som indsamler og persisterer statushistorik og udleverer den beregnede EVE-referencepris med eksplicit USDT-quote. Den bevarer sit eksisterende start/stop/close-contract; samme `EvelynStatusServiceImpl`-objekt startes af Hub'en som `NenjimProcess` under `evelyn.status.prod` eller `evelyn.status.test`. Evelyn er fortsat navnet på det samlede system. |
| Context | Fremtidig beskrivelse af ønskede/resolverede artifactvalg og isoleret runtime; ikke implementeret af Journal-laget eller første Registry-version. | | Context | Fremtidig beskrivelse af ønskede/resolverede artifactvalg og isoleret runtime; ikke implementeret af Journal-laget eller første Registry-version. |
| Resolver | Fremtidig komponent for dependency graph og release-selection; Journal-laget repræsenterer reglerne, men vælger ikke en graf. | | Resolver | Fremtidig komponent for dependency graph og release-selection; Journal-laget repræsenterer reglerne, men vælger ikke en graf. |
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-04
@@ -0,0 +1,30 @@
# Design
## Context
See `proposal.md` for motivation. Status sampling already calls a package-private `EvelynPriceIndexCalculator`, which holds the curve constants and decimal arithmetic. The implementation receives its canonical EVE/USDT pair from the Hub and validates storage only at startup. Constructor name reservations, file restoration and durable append behavior must survive the package move.
## Goals / Non-Goals
**Goals:** Keep one calculation behind both the public reference-price operation and status sampling; preserve pure on-demand reads and separate Production/Test state while making their status-service identity explicit.
**Non-Goals:** No additional reference-data service, new ValueTags, curve configuration, lifecycle redesign, package-wide cleanup, trading activation, or storage provisioning/migration.
## Decisions
1. Move only the API, implementation and calculator. Import `EvelynStatusIndexPoint` from its existing package. Retain the implementation's existing constructor shapes and lifecycle methods; a compatibility wrapper would leave two competing names and is unnecessary for the requested breaking rename.
2. Implement `getReferencePrice(timestamp)` as a direct call to the calculator's existing expected-price method, wrapped in `AssetPrice` with the constructor-injected trading pair. Sampling continues to call `calculateIndex(actualPrice, samplingInstant)`, which delegates to that same expected-price method. Sharing the helper preserves identical constants and arithmetic without allocating a second service or changing index calculation.
3. Give the public operation no lifecycle gate, synchronization, clock read, ticker lookup or storage access. The immutable trading pair and pure calculator suffice before startup and during stop. Null and pre-start rejection are inherited from the calculator.
4. Rename Hub factory references, dependency fields, constants and process IDs together, and update Mission Control's parameter/field types. Process Manager already displays IDs from Hub snapshots and needs no new hardcoded mapping. Keep instance names `Production` and `Test` and the `data/evelyn` root.
5. Document the private configuration substitutions without editing the deployment configuration. The versioned example currently selects only Registry and Journal, so its selected processes stay unchanged.
## Risks / Trade-offs
- [Consumers using old packages or IDs fail] → Update all versioned consumers and document exact deployment substitutions; intentionally provide no alias.
- [Fractional exponentiation uses floating-point transcendental arithmetic] → Preserve the existing calculator unchanged and check the start, annual and representative fractional values.
- [Verification could trigger operational side effects] → Compile normally and use offline deterministic checks with fake Ticker and temporary storage; never run the Hub or financial processes.
- [No existing status/calculator verification source is present in this checkout] → Add only the deterministic acceptance checks explicitly required by #88, covering the public API and regression behavior rather than the rename itself.
## Migration Plan
Deploy the renamed code with `evelyn.service.prod` replaced by `evelyn.status.prod` and `evelyn.service.test` by `evelyn.status.test` wherever present in private `conf/NenjimHub.conf`. Preserve line order and enabled/commented selection. Retain existing status files byte-for-byte; the service reads the same paths and format. Rollback restores the old code and old IDs without a data migration.
@@ -0,0 +1,28 @@
# Proposal
## Why
The current `Evelyn` status-history service is named as if it owns the entire Evelyn system, and its existing reference-price calculation is unavailable to other services. Issue [#88](https://git.r35157.com/r35157/com_r35157_nenjim-hubd-impl_ref/issues/88) gives Evelyn consumers one authoritative reference price through the existing status service.
## What Changes
- **BREAKING**: Move and rename the public API to `com.r35157.evelyn.service.status.EvelynStatusService` and its implementation to `com.r35157.evelyn.service.status.impl.ref.EvelynStatusServiceImpl`; move the package-private calculator alongside it.
- Add `AssetPrice getReferencePrice(Instant timestamp)`, calculating on demand through the existing curve and returning the service's canonical EVE/USDT trading pair independently of sampling and storage.
- **BREAKING**: Replace `evelyn.service.prod` / `evelyn.service.test` with `evelyn.status.prod` / `evelyn.status.test` in Hub wiring and recognized process IDs. Document the corresponding private deployment configuration edit.
- Preserve named Production/Test storage, format/version, history, sampling, lifecycle, Mission Control charts, and inactive legacy rebalancing.
## Capabilities
### New Capabilities
None.
### Modified Capabilities
- `evelyn-status-index-history`: Identify the status-service API and expose the shared reference-price calculation while preserving history and lifecycle behavior.
- `nenjim-hub-configuration`: Recognize the renamed status process IDs and preserve the configured selection and dependency order.
- `nenjim-component-registry`: Use the renamed IDs in the canonical component-ID contract.
## Impact
Affected source is limited to the status API, implementation, calculator, Hub composition, Mission Control's typed dependencies, and directly related documentation/specifications and verification. Other Evelyn packages and `EvelynStatusIndexPoint` stay in place. Existing deployments must replace only the two old IDs where present in their private `NenjimHub.conf`, retaining enabled/commented selections and order. No history migration, dependency addition, live financial operation, or commit is required.
@@ -0,0 +1,188 @@
# Spec Delta
## MODIFIED Requirements
### Requirement: Evelyn calculates the live price index
`EvelynStatusService` SHALL obtain the actual EVE/USDT price from the latest AssetAZ Ticker observation for the canonical EVE/USDT trading pair. It SHALL treat USDT as USD for this calculation and SHALL NOT access Raydium directly. At sampling instant t, on or after 2026-07-31T22:00:00Z, the reference price SHALL be 15 × 1.20^(elapsedSeconds / 31,536,000) and the Evelyn Price Index SHALL be ((actualPrice / referencePrice) - 1) × 10, calculated with adequate decimal precision and without arbitrary history rounding. Sampling and the public reference-price operation SHALL use the same reference calculation and constants at the same sampling instant.
#### Scenario: Calculate expected price at the configured start
- **WHEN** the sampling instant is 2026-07-31T22:00:00Z
- **THEN** the expected EVE price is exactly 15 USD
#### Scenario: Calculate expected price after one year
- **WHEN** exactly 31,536,000 seconds have elapsed since the configured start
- **THEN** the expected EVE price is exactly 18 USD
#### Scenario: Calculate representative fractional-year growth
- **WHEN** 18 days, 3 hours, 8 minutes, and 14 seconds have elapsed
- **THEN** the expected EVE price is approximately 15.136464436276 USD
#### Scenario: Calculate index from actual price
- **WHEN** actual price equals expected price, is 10 percent above, is 10 percent below, is double, or is half the expected price
- **THEN** the respective index is 0, 1, -1, 10, or -5
### Requirement: Evelyn instances have safe unique persistent names
Every `EvelynStatusService` instance SHALL have an NFC-normalized Unicode name whose preserved spelling and capitalization selects the direct child directory data/evelyn/<name>. A name SHALL be rejected if it is empty or whitespace-only, has leading or trailing whitespace, is . or .., ends with a period, contains a control character or any of < > : " / \ | ? *, is a case-insensitive Windows-reserved filename including a reserved base with an extension, or normalizes as a path to anything other than one direct child of data/evelyn. Within one JVM, normalized names SHALL be reserved atomically and compared case-insensitively using locale-independent rules.
#### Scenario: A portable Unicode name is accepted
- **WHEN** an instance is constructed with a valid Unicode name containing Danish characters
- **THEN** NFC normalization is applied and the normalized spelling and capitalization identify its direct child data directory
#### Scenario: An unsafe name is rejected without reservation
- **WHEN** construction receives an invalid name or any other invalid constructor argument
- **THEN** construction fails before reserving a persistent identity
#### Scenario: Concurrent equivalent names conflict
- **WHEN** two constructor calls in one JVM concurrently request names that are equal after NFC normalization and locale-independent case folding
- **THEN** exactly one reserves the persistent identity and the other fails
### Requirement: Evelyn distinguishes restartable stop from permanent close
Stopping `EvelynStatusService` SHALL terminate sampling, clear memory, preserve its persistent file, and retain its name reservation so the same object can restart. `EvelynStatusService` SHALL support permanent, idempotent close; closing an active instance SHALL first stop it safely, a successfully closed instance SHALL never start again, and its name SHALL be released only after its sampling thread has definitely terminated. A termination failure SHALL be reported and SHALL retain the reservation.
#### Scenario: A stopped instance restarts
- **WHEN** a stopped but not closed `EvelynStatusService` instance is started again
- **THEN** it retains exclusive ownership of the same name and reloads the complete persisted history before sampling
#### Scenario: An active instance closes successfully
- **WHEN** close is called on an active instance and its sampling thread terminates
- **THEN** memory is cleared, the name reservation is released, and later start calls fail
#### Scenario: Close cannot confirm termination
- **WHEN** close cannot confirm that the sampling thread terminated
- **THEN** close reports failure and retains the name reservation
#### Scenario: Close is repeated
- **WHEN** close is called after the instance was successfully closed
- **THEN** it completes without changing state or failing
### Requirement: Evelyn uses externally provisioned per-instance storage
Each named instance SHALL use the existing regular file data/evelyn/<normalized-name>/status.log. Before every start, `EvelynStatusService` SHALL require the direct instance path to be an existing readable and writable directory and status.log to be an existing readable and writable regular file. `EvelynStatusService` SHALL NOT create, replace, repair, truncate, rename, or otherwise provision either path. Any validation or actual open failure SHALL identify the affected path, leave memory unpublished, and prevent sampling from starting.
#### Scenario: Required storage is absent or unsuitable
- **WHEN** the instance directory or status file is missing, the path has the wrong type, or required access is unavailable
- **THEN** start fails with the affected path and reason without modifying storage, publishing history, or starting sampling
#### Scenario: Storage changes while stopped
- **WHEN** storage becomes invalid after stop and the same instance is started again
- **THEN** the complete startup validation is repeated and restart fails without modifying storage
### Requirement: Evelyn validates status history format strictly
The UTF-8 status.log format SHALL have supported version 1, declared by FORMAT_VERSION=1 as the first actual entry; blank lines and full-line comments MAY precede it when loading, while an externally provisioned new file SHALL place it on the physical first line. Missing, duplicate, malformed, misplaced, or unsupported declarations SHALL fail loading before record parsing. After the declaration, blank lines and comments SHALL be ignored, inline comments MAY follow data, and every other entry SHALL be a valid record with an explicit stable numeric type code.
Type 1 SHALL encode <UTC uuuuMMddHHmmssSSS'Z' timestamp>:1:<plain BigDecimal Evelyn Price Index>. It SHALL have exactly three fields. Unknown types, invalid timestamps or decimals, missing or surplus fields, and decreasing timestamps SHALL fail the complete load; equal timestamps SHALL be accepted. A valid declaration with no records SHALL represent empty history. Applicable failures SHALL identify the file path, one-based line number, and original line, and `EvelynStatusService` SHALL neither skip, sort, partially publish, nor repair invalid content.
#### Scenario: A valid versioned history loads
- **WHEN** the file has a valid first actual version declaration and valid type-1 records in non-decreasing timestamp order
- **THEN** the status service restores every record in file order with zero values for the five unimplemented indexes
#### Scenario: The format declaration is invalid
- **WHEN** the declaration is missing, duplicated, malformed, misplaced, or not version 1
- **THEN** startup fails before parsing records and reports useful file diagnostics
#### Scenario: A record is invalid
- **WHEN** a record has an unknown type, malformed field, wrong field count, or timestamp earlier than its predecessor
- **THEN** startup fails with path, line number, and original line without exposing partial history or altering the file
### Requirement: Evelyn samples without overlapping or catch-up attempts
Each started `EvelynStatusService` instance SHALL own its sampling executor, attempt its first sample only after complete storage validation and history restoration, and make subsequent attempts approximately one minute after the preceding attempt completes. Sampling SHALL remain active for the status-service lifecycle independently of whether Mission Control is open. `EvelynStatusService` SHALL add exactly one timestamped point per successful attempt and SHALL isolate a failed or temporarily unavailable Ticker lookup or persistence operation without adding a fake or unpersisted point, terminating the status service, or cancelling the next attempt. Stopping `EvelynStatusService` SHALL terminate its sampling executor.
#### Scenario: Initial Ticker data is unavailable
- **WHEN** the immediate sampling attempt cannot obtain a latest EVE/USDT price
- **THEN** no point is persisted or published, diagnostic context is logged, and the next fixed-delay attempt remains scheduled
#### Scenario: A later sample succeeds
- **WHEN** a later attempt obtains a latest EVE/USDT price whose millisecond timestamp is not earlier than the last persisted timestamp
- **THEN** exactly one type-1 record is durably appended before the corresponding point becomes visible in memory
#### Scenario: Persistence fails for one sample
- **WHEN** appending or durably flushing a calculated point fails
- **THEN** the point is not published, instance and path context are logged, and the next fixed-delay attempt remains scheduled
#### Scenario: A sampled timestamp goes backwards
- **WHEN** a newly sampled timestamp is earlier than the last persisted timestamp
- **THEN** no record or point is added, the failure is logged, and later attempts remain scheduled
### Requirement: Evelyn provides historical status-index measurements
Each named `EvelynStatusService` instance SHALL publish a safe, non-null, oldest-to-newest snapshot only after its complete persisted history has been validated and loaded. Each type-1 point SHALL contain its persisted or sampled timestamp, the Evelyn Price Index, and BigDecimal.ZERO placeholders for EVE_SYRUP Pool Depth Index, EVE_SYRUP Pool Balance Index, AAZDKK_USDT Pool Balance Index, AAZDKK_USDT Pool Price Index, and AAZDKK_USDT Pool Depth Index. Production and Test SHALL use independently named files, history collections, reservations, and lifecycles. A sampled point SHALL be durably appended using robust line-boundary handling before publication. Stop SHALL clear only memory; restart SHALL restore the complete file, and close SHALL preserve the file.
#### Scenario: Status-index history is requested during sampling
- **WHEN** a caller requests a status-service instance's status-index history while persisted points may be appended
- **THEN** it receives a safe snapshot containing only completely persisted points and all six typed fields
#### Scenario: A process starts two named Evelyn environments
- **WHEN** Production and Test `EvelynStatusService` instances start with independently provisioned files
- **THEN** each restores and extends only its own complete history even if calculated values are identical
#### Scenario: Existing file lacks a final line separator
- **WHEN** a valid existing status file ends with a record but no line separator
- **THEN** the next durable append first supplies a line boundary and preserves both records as distinct entries
#### Scenario: An Evelyn instance stops and restarts
- **WHEN** a named instance is stopped and later restarted
- **THEN** its memory is cleared at stop and its complete unchanged persistent history is revalidated and restored before sampling resumes
### Requirement: Evelyn Mission Control visualizes status-index measurements
Evelyn Mission Control SHALL display separate live Production and Test Overview charts exclusively from their corresponding authoritative `EvelynStatusService` snapshots and SHALL never read or manage persistence directly. Each chart SHALL contain one enabled series named Evelyn Price Index; the five future series and their typed fields SHALL remain available as disabled source scaffolding. Opening or reopening EMC SHALL immediately reconstruct each chart from the complete restored current snapshot before periodically refreshing it. Successful points SHALL be added to chart objects only on the JavaFX Application Thread while the window remains open. Timestamp bounds SHALL expand as points arrive, and only enabled Evelyn Price Index values SHALL determine a dynamic Y-axis range that is symmetric around and always displays zero, using a reasonable default range for empty or all-zero history. Hiding or closing EMC SHALL release only EMC's JavaFX refresh resources and SHALL NOT start, stop, close, persist, or clear either status-service instance.
#### Scenario: Production and Test Overviews show restored history
- **WHEN** Mission Control opens after two named `EvelynStatusService` instances have restored their histories
- **THEN** it displays separate one-series charts immediately populated from the corresponding complete snapshots
#### Scenario: A successful point appears while EMC is open
- **WHEN** a status-service history publishes a newly persisted point
- **THEN** the corresponding chart adds it on the JavaFX Application Thread and updates its timestamp and symmetric price-index bounds without reopening EMC
#### Scenario: EMC reopens after collecting hidden-window samples
- **WHEN** the same EMC instance is reopened after the status services persisted additional points while its window was hidden
- **THEN** each chart is reconstructed immediately from its environment's complete current history without missing points because of stale rendering state
#### Scenario: Future fields contain larger values
- **WHEN** any disabled future-index field has a greater absolute value than Evelyn Price Index
- **THEN** that disabled value does not affect the visible Y-axis bounds
## ADDED Requirements
### Requirement: Status service has an explicit public boundary
The system SHALL expose `com.r35157.evelyn.service.status.EvelynStatusService` as the status-history and reference-price API. Its process IDs SHALL be `evelyn.status.prod` and `evelyn.status.test`. The service rename SHALL preserve the Production/Test storage identities, existing lifecycle operations, `EvelynStatusIndexPoint` and its six fields. Evelyn SHALL remain the overall system name, with NenjimHub owning individual process startup.
#### Scenario: Consumer uses the renamed status API
- **WHEN** a consumer requests status history or a reference price
- **THEN** it uses `EvelynStatusService` without accessing an implementation helper or a new service container
#### Scenario: Existing Production and Test storage survives the rename
- **WHEN** the renamed services are constructed and started with the existing Production and Test identities
- **THEN** they use the unchanged `data/evelyn/Production/status.log` and `data/evelyn/Test/status.log` files and format version 1 without migration or provisioning
### Requirement: Reference price is calculated on demand with an explicit quote currency
`EvelynStatusService` SHALL expose `AssetPrice getReferencePrice(Instant timestamp)`. For each supplied instant on or after 2026-07-31T22:00:00Z it SHALL calculate 15.00 × 1.20^(elapsedSeconds / 31,536,000), returning the canonical EVE/USDT trading pair. It SHALL preserve the existing USDT-as-USD index convention without conversion, duplicate constants, separate configuration, cached historical measurements, or dependence on a sampling interval.
#### Scenario: Public reference price at the start
- **WHEN** a consumer requests the reference price for 2026-07-31T22:00:00Z
- **THEN** the price is 15 with the canonical EVE base and USDT quote currencies
#### Scenario: Public reference price after one year
- **WHEN** a consumer requests the reference price exactly 31,536,000 seconds after the start
- **THEN** the price is 18 with the same canonical trading pair
#### Scenario: Supplied timestamp determines the price
- **WHEN** a consumer requests a valid instant distinct from the clock or latest historical point
- **THEN** the returned value is calculated for that instant rather than the current clock or historical sample
### Requirement: Reference-price requests are independent of sampling and storage
A reference-price request SHALL succeed before sampling starts and while sampling is stopped, including when history storage has not been provisioned. It SHALL NOT fetch market prices, access history files, start a worker, or mutate status history. A null timestamp SHALL be rejected with `NullPointerException`; an instant before the reference curve starts SHALL be rejected with `IllegalArgumentException`.
#### Scenario: Reference price before startup without storage
- **WHEN** a constructed service with no provisioned history storage receives a valid reference-price request
- **THEN** it returns the calculated value without a ticker lookup, file access, worker startup, or history change
#### Scenario: Reference price while sampling is stopped
- **WHEN** sampling is stopped and a valid reference-price request is made
- **THEN** the service returns the calculated value without restarting sampling or reading, writing, or mutating history
#### Scenario: Reference price during sampling preserves history
- **WHEN** reference-price requests are made while the service holds a status-history snapshot
- **THEN** the requests do not add or alter any status point or file record
#### Scenario: Null or pre-start timestamp is rejected
- **WHEN** a caller supplies null or an instant before 2026-07-31T22:00:00Z
- **THEN** the service rejects it with the respective null or argument exception without side effects
@@ -0,0 +1,18 @@
# Spec Delta
## MODIFIED Requirements
### Requirement: Component IDs are canonical validated values
`NenjimComponentId` SHALL reject null, strip surrounding Unicode whitespace before storage and validation, and accept only lower-case ASCII dot-separated segments whose first character is a letter and whose remaining subparts contain lower-case letters or digits separated by single internal hyphens. Standard record value equality and hashing SHALL define ID equality.
#### Scenario: Surrounding whitespace is canonicalized
- **WHEN** an ID is constructed from ` nenjim.registry.service `
- **THEN** its stored value is `nenjim.registry.service`
#### Scenario: Valid component IDs are accepted
- **WHEN** IDs such as `nenjim.registry.service`, `assetaz.price-source.raydium-pool`, or `evelyn.status.prod` are constructed
- **THEN** construction succeeds
#### Scenario: Invalid component IDs are rejected
- **WHEN** an ID is null or contains uppercase letters, underscores, internal whitespace, an empty segment, a leading or trailing dot, or a malformed hyphen
- **THEN** construction fails with the specified null or argument exception category and identifies the invalid canonicalized value without exposing secrets
@@ -0,0 +1,36 @@
# Spec Delta
## MODIFIED Requirements
### Requirement: Selected processes are constructed and started lazily in exact order
The Hub SHALL support the modern process IDs `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `jupiter-perps-alarm.default`, `evelyn.status.prod`, `evelyn.status.test`, `evelyn.iou-burner.prod`, `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` through implementation-only construction capability that creates no process instance during validation. After passive composition, the Hub SHALL visit every configured entry after Registry in declared order, beginning with Journal. At each entry it SHALL construct only that process and any process-specific passive administration view, register the process under the exact configured ID, resolve it through the canonical `NenjimProcess` contract where applicable, call `start()`, and record the ID only after successful return.
The configuration SHALL be the complete manually ordered process dependency closure. The Hub SHALL not discover, insert, recursively construct, reorder, or automatically start dependencies. A process omitted from `[AUTO_START]` SHALL not be constructed, registered, started, or recorded, even when its implementation is available. Legacy process prototypes outside the modern Hub graph SHALL not be selectable.
#### Scenario: Configured process starts successfully
- **WHEN** a configured process entry is reached and its dependencies are already available
- **THEN** that process is constructed, registered, started, and recorded before the next entry is reached
#### Scenario: Process is omitted
- **WHEN** a known process ID does not appear in `[AUTO_START]`
- **THEN** no instance of that process is constructed or registered and its lifecycle is never started or recorded
#### Scenario: Jupiter Perps Alarm is selected
- **WHEN** `jupiter-perps-alarm.default` is explicitly listed after its required predecessors
- **THEN** the otherwise inactive modern alarm process is constructed, registered, started and recorded at that exact position
#### Scenario: Dependency order is invalid
- **WHEN** a selected process is reached before a process dependency required by its current constructor or startup behavior
- **THEN** construction or startup fails clearly without implicit dependency construction, repair, or reordering
#### Scenario: JavaFX processes are omitted
- **WHEN** a valid headless plan omits `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default`
- **THEN** none of those objects is constructed or registered and the JavaFX toolkit remains uninitialized by the Hub
#### Scenario: Renamed status services retain dependency wiring
- **WHEN** `evelyn.status.prod` and `evelyn.status.test` are listed after Ticker and before Mission Control
- **THEN** the Hub constructs separate Production/Test status services, registers the renamed IDs, and injects the corresponding typed service views into Mission Control in the configured order
#### Scenario: Deployment still contains an old status ID
- **WHEN** a private deployment configuration contains `evelyn.service.prod` or `evelyn.service.test`
- **THEN** validation rejects the unknown ID before startup; deployment migration replaces only that ID with `evelyn.status.prod` or `evelyn.status.test`, preserving selection and order
@@ -0,0 +1,26 @@
# Tasks
## 1. Status API and Calculation
- [x] 1.1 Move/rename the API and implementation and move the package-private calculator; verify only these three Evelyn files change packages and `EvelynStatusIndexPoint` stays unchanged.
- [x] 1.2 Add documented `AssetPrice getReferencePrice(Instant)` delegating to the existing calculator and canonical pair; verify deterministic start/year/fractional values, invalid timestamps, no ticker/clock/storage access, unchanged history and operation before startup/while stopped.
- [x] 1.3 Document the renamed API and reference-price contract; verify the documented curve, units and preserved lifecycle/storage match the code.
## 2. Hub and Mission Control Integration
- [x] 2.1 Rename Hub types, fields, factories and component IDs and Mission Control's typed dependencies; compile main/test sources and inspect Process Manager's use of Hub-provided IDs.
- [x] 2.2 Document private deployment ID substitutions and update affected versioned examples without changing their selections; verify the example still selects only Registry and Journal and private runtime configuration is untouched.
- [x] 2.3 Verify separate Production/Test histories, index calculation, durable append, immutable snapshots and restart behavior with offline temporary storage; review Mission Control chart wiring and retained six-field scaffolding.
## 3. Integration and Review
- [x] 3.1 Update the affected existing OpenSpec capabilities from the deltas and strictly validate this change; verify the renamed API, operation and IDs are represented without archiving the change.
- [x] 3.2 Run the focused acceptance checks and existing offline verification, compile all source sets, run `git diff --check`, and review the diff for generated-file edits, runtime data/config changes, financial activation and commits.
## Verification Result
- Gradle 9.6.1 / JDK 25: `classes testClasses test --offline --console=plain --no-daemon` passed. A temporary writable Gradle home and the existing read-only dependency cache were used in this session.
- Five status-service acceptance checks and four existing offline Solana transaction-construction checks passed: 9 tests, 0 failures, 0 errors.
- Strict OpenSpec validation passed for this change and its three updated main capabilities.
- `git diff --check` passed; generated source, private configuration and runtime data were not edited, the versioned bootstrap example and status-point record are unchanged, and the index calculator differs only in its package.
- No Hub or live financial process was run. The change remains active, all eight tasks are complete, and no files were staged or committed.
@@ -7,7 +7,7 @@ Provide typed process-local Evelyn status measurements and visualize the current
## Requirements ## Requirements
### Requirement: Evelyn calculates the live price index ### Requirement: Evelyn calculates the live price index
Evelyn SHALL obtain the actual EVE/USDT price from the latest AssetAZ Ticker observation for the canonical EVE/USDT trading pair. It SHALL treat USDT as USD for this calculation and SHALL NOT access Raydium directly. At sampling instant t, on or after 2026-07-31T22:00:00Z, the expected price SHALL be 15 × 1.20^(elapsedSeconds / 31,536,000) and the Evelyn Price Index SHALL be ((actualPrice / expectedPrice) - 1) × 10, calculated with adequate decimal precision and without arbitrary history rounding. `EvelynStatusService` SHALL obtain the actual EVE/USDT price from the latest AssetAZ Ticker observation for the canonical EVE/USDT trading pair. It SHALL treat USDT as USD for this calculation and SHALL NOT access Raydium directly. At sampling instant t, on or after 2026-07-31T22:00:00Z, the reference price SHALL be 15 × 1.20^(elapsedSeconds / 31,536,000) and the Evelyn Price Index SHALL be ((actualPrice / referencePrice) - 1) × 10, calculated with adequate decimal precision and without arbitrary history rounding. Sampling and the public reference-price operation SHALL use the same reference calculation and constants at the same sampling instant.
#### Scenario: Calculate expected price at the configured start #### Scenario: Calculate expected price at the configured start
- **WHEN** the sampling instant is 2026-07-31T22:00:00Z - **WHEN** the sampling instant is 2026-07-31T22:00:00Z
@@ -26,7 +26,7 @@ Evelyn SHALL obtain the actual EVE/USDT price from the latest AssetAZ Ticker obs
- **THEN** the respective index is 0, 1, -1, 10, or -5 - **THEN** the respective index is 0, 1, -1, 10, or -5
### Requirement: Evelyn instances have safe unique persistent names ### Requirement: Evelyn instances have safe unique persistent names
Every Evelyn instance SHALL have an NFC-normalized Unicode name whose preserved spelling and capitalization selects the direct child directory data/evelyn/<name>. A name SHALL be rejected if it is empty or whitespace-only, has leading or trailing whitespace, is . or .., ends with a period, contains a control character or any of < > : " / \ | ? *, is a case-insensitive Windows-reserved filename including a reserved base with an extension, or normalizes as a path to anything other than one direct child of data/evelyn. Within one JVM, normalized names SHALL be reserved atomically and compared case-insensitively using locale-independent rules. Every `EvelynStatusService` instance SHALL have an NFC-normalized Unicode name whose preserved spelling and capitalization selects the direct child directory data/evelyn/<name>. A name SHALL be rejected if it is empty or whitespace-only, has leading or trailing whitespace, is . or .., ends with a period, contains a control character or any of < > : " / \ | ? *, is a case-insensitive Windows-reserved filename including a reserved base with an extension, or normalizes as a path to anything other than one direct child of data/evelyn. Within one JVM, normalized names SHALL be reserved atomically and compared case-insensitively using locale-independent rules.
#### Scenario: A portable Unicode name is accepted #### Scenario: A portable Unicode name is accepted
- **WHEN** an instance is constructed with a valid Unicode name containing Danish characters - **WHEN** an instance is constructed with a valid Unicode name containing Danish characters
@@ -41,10 +41,10 @@ Every Evelyn instance SHALL have an NFC-normalized Unicode name whose preserved
- **THEN** exactly one reserves the persistent identity and the other fails - **THEN** exactly one reserves the persistent identity and the other fails
### Requirement: Evelyn distinguishes restartable stop from permanent close ### Requirement: Evelyn distinguishes restartable stop from permanent close
Stopping Evelyn SHALL terminate sampling, clear memory, preserve its persistent file, and retain its name reservation so the same object can restart. Evelyn SHALL support permanent, idempotent close; closing an active instance SHALL first stop it safely, a successfully closed instance SHALL never start again, and its name SHALL be released only after its sampling thread has definitely terminated. A termination failure SHALL be reported and SHALL retain the reservation. Stopping `EvelynStatusService` SHALL terminate sampling, clear memory, preserve its persistent file, and retain its name reservation so the same object can restart. `EvelynStatusService` SHALL support permanent, idempotent close; closing an active instance SHALL first stop it safely, a successfully closed instance SHALL never start again, and its name SHALL be released only after its sampling thread has definitely terminated. A termination failure SHALL be reported and SHALL retain the reservation.
#### Scenario: A stopped instance restarts #### Scenario: A stopped instance restarts
- **WHEN** a stopped but not closed Evelyn instance is started again - **WHEN** a stopped but not closed `EvelynStatusService` instance is started again
- **THEN** it retains exclusive ownership of the same name and reloads the complete persisted history before sampling - **THEN** it retains exclusive ownership of the same name and reloads the complete persisted history before sampling
#### Scenario: An active instance closes successfully #### Scenario: An active instance closes successfully
@@ -60,7 +60,7 @@ Stopping Evelyn SHALL terminate sampling, clear memory, preserve its persistent
- **THEN** it completes without changing state or failing - **THEN** it completes without changing state or failing
### Requirement: Evelyn uses externally provisioned per-instance storage ### Requirement: Evelyn uses externally provisioned per-instance storage
Each named instance SHALL use the existing regular file data/evelyn/<normalized-name>/status.log. Before every start, Evelyn SHALL require the direct instance path to be an existing readable and writable directory and status.log to be an existing readable and writable regular file. Evelyn SHALL NOT create, replace, repair, truncate, rename, or otherwise provision either path. Any validation or actual open failure SHALL identify the affected path, leave memory unpublished, and prevent sampling from starting. Each named instance SHALL use the existing regular file data/evelyn/<normalized-name>/status.log. Before every start, `EvelynStatusService` SHALL require the direct instance path to be an existing readable and writable directory and status.log to be an existing readable and writable regular file. `EvelynStatusService` SHALL NOT create, replace, repair, truncate, rename, or otherwise provision either path. Any validation or actual open failure SHALL identify the affected path, leave memory unpublished, and prevent sampling from starting.
#### Scenario: Required storage is absent or unsuitable #### Scenario: Required storage is absent or unsuitable
- **WHEN** the instance directory or status file is missing, the path has the wrong type, or required access is unavailable - **WHEN** the instance directory or status file is missing, the path has the wrong type, or required access is unavailable
@@ -73,11 +73,11 @@ Each named instance SHALL use the existing regular file data/evelyn/<normalized-
### Requirement: Evelyn validates status history format strictly ### Requirement: Evelyn validates status history format strictly
The UTF-8 status.log format SHALL have supported version 1, declared by FORMAT_VERSION=1 as the first actual entry; blank lines and full-line comments MAY precede it when loading, while an externally provisioned new file SHALL place it on the physical first line. Missing, duplicate, malformed, misplaced, or unsupported declarations SHALL fail loading before record parsing. After the declaration, blank lines and comments SHALL be ignored, inline comments MAY follow data, and every other entry SHALL be a valid record with an explicit stable numeric type code. The UTF-8 status.log format SHALL have supported version 1, declared by FORMAT_VERSION=1 as the first actual entry; blank lines and full-line comments MAY precede it when loading, while an externally provisioned new file SHALL place it on the physical first line. Missing, duplicate, malformed, misplaced, or unsupported declarations SHALL fail loading before record parsing. After the declaration, blank lines and comments SHALL be ignored, inline comments MAY follow data, and every other entry SHALL be a valid record with an explicit stable numeric type code.
Type 1 SHALL encode <UTC uuuuMMddHHmmssSSS'Z' timestamp>:1:<plain BigDecimal Evelyn Price Index>. It SHALL have exactly three fields. Unknown types, invalid timestamps or decimals, missing or surplus fields, and decreasing timestamps SHALL fail the complete load; equal timestamps SHALL be accepted. A valid declaration with no records SHALL represent empty history. Applicable failures SHALL identify the file path, one-based line number, and original line, and Evelyn SHALL neither skip, sort, partially publish, nor repair invalid content. Type 1 SHALL encode <UTC uuuuMMddHHmmssSSS'Z' timestamp>:1:<plain BigDecimal Evelyn Price Index>. It SHALL have exactly three fields. Unknown types, invalid timestamps or decimals, missing or surplus fields, and decreasing timestamps SHALL fail the complete load; equal timestamps SHALL be accepted. A valid declaration with no records SHALL represent empty history. Applicable failures SHALL identify the file path, one-based line number, and original line, and `EvelynStatusService` SHALL neither skip, sort, partially publish, nor repair invalid content.
#### Scenario: A valid versioned history loads #### Scenario: A valid versioned history loads
- **WHEN** the file has a valid first actual version declaration and valid type-1 records in non-decreasing timestamp order - **WHEN** the file has a valid first actual version declaration and valid type-1 records in non-decreasing timestamp order
- **THEN** Evelyn restores every record in file order with zero values for the five unimplemented indexes - **THEN** the status service restores every record in file order with zero values for the five unimplemented indexes
#### Scenario: The format declaration is invalid #### Scenario: The format declaration is invalid
- **WHEN** the declaration is missing, duplicated, malformed, misplaced, or not version 1 - **WHEN** the declaration is missing, duplicated, malformed, misplaced, or not version 1
@@ -88,7 +88,7 @@ Type 1 SHALL encode <UTC uuuuMMddHHmmssSSS'Z' timestamp>:1:<plain BigDecimal Eve
- **THEN** startup fails with path, line number, and original line without exposing partial history or altering the file - **THEN** startup fails with path, line number, and original line without exposing partial history or altering the file
### Requirement: Evelyn samples without overlapping or catch-up attempts ### Requirement: Evelyn samples without overlapping or catch-up attempts
Each started Evelyn environment SHALL own its sampling executor, attempt its first sample only after complete storage validation and history restoration, and make subsequent attempts approximately one minute after the preceding attempt completes. Sampling SHALL remain active for the Evelyn lifecycle independently of whether Mission Control is open. Evelyn SHALL add exactly one timestamped point per successful attempt and SHALL isolate a failed or temporarily unavailable Ticker lookup or persistence operation without adding a fake or unpersisted point, terminating Evelyn, or cancelling the next attempt. Stopping Evelyn SHALL terminate its sampling executor. Each started `EvelynStatusService` instance SHALL own its sampling executor, attempt its first sample only after complete storage validation and history restoration, and make subsequent attempts approximately one minute after the preceding attempt completes. Sampling SHALL remain active for the status-service lifecycle independently of whether Mission Control is open. `EvelynStatusService` SHALL add exactly one timestamped point per successful attempt and SHALL isolate a failed or temporarily unavailable Ticker lookup or persistence operation without adding a fake or unpersisted point, terminating the status service, or cancelling the next attempt. Stopping `EvelynStatusService` SHALL terminate its sampling executor.
#### Scenario: Initial Ticker data is unavailable #### Scenario: Initial Ticker data is unavailable
- **WHEN** the immediate sampling attempt cannot obtain a latest EVE/USDT price - **WHEN** the immediate sampling attempt cannot obtain a latest EVE/USDT price
@@ -107,14 +107,14 @@ Each started Evelyn environment SHALL own its sampling executor, attempt its fir
- **THEN** no record or point is added, the failure is logged, and later attempts remain scheduled - **THEN** no record or point is added, the failure is logged, and later attempts remain scheduled
### Requirement: Evelyn provides historical status-index measurements ### Requirement: Evelyn provides historical status-index measurements
Each named Evelyn instance SHALL publish a safe, non-null, oldest-to-newest snapshot only after its complete persisted history has been validated and loaded. Each type-1 point SHALL contain its persisted or sampled timestamp, the Evelyn Price Index, and BigDecimal.ZERO placeholders for EVE_SYRUP Pool Depth Index, EVE_SYRUP Pool Balance Index, AAZDKK_USDT Pool Balance Index, AAZDKK_USDT Pool Price Index, and AAZDKK_USDT Pool Depth Index. Production and Test SHALL use independently named files, history collections, reservations, and lifecycles. A sampled point SHALL be durably appended using robust line-boundary handling before publication. Stop SHALL clear only memory; restart SHALL restore the complete file, and close SHALL preserve the file. Each named `EvelynStatusService` instance SHALL publish a safe, non-null, oldest-to-newest snapshot only after its complete persisted history has been validated and loaded. Each type-1 point SHALL contain its persisted or sampled timestamp, the Evelyn Price Index, and BigDecimal.ZERO placeholders for EVE_SYRUP Pool Depth Index, EVE_SYRUP Pool Balance Index, AAZDKK_USDT Pool Balance Index, AAZDKK_USDT Pool Price Index, and AAZDKK_USDT Pool Depth Index. Production and Test SHALL use independently named files, history collections, reservations, and lifecycles. A sampled point SHALL be durably appended using robust line-boundary handling before publication. Stop SHALL clear only memory; restart SHALL restore the complete file, and close SHALL preserve the file.
#### Scenario: Status-index history is requested during sampling #### Scenario: Status-index history is requested during sampling
- **WHEN** a caller requests an Evelyn environment's status-index history while persisted points may be appended - **WHEN** a caller requests a status-service instance's status-index history while persisted points may be appended
- **THEN** it receives a safe snapshot containing only completely persisted points and all six typed fields - **THEN** it receives a safe snapshot containing only completely persisted points and all six typed fields
#### Scenario: A process starts two named Evelyn environments #### Scenario: A process starts two named Evelyn environments
- **WHEN** Production and Test Evelyn instances start with independently provisioned files - **WHEN** Production and Test `EvelynStatusService` instances start with independently provisioned files
- **THEN** each restores and extends only its own complete history even if calculated values are identical - **THEN** each restores and extends only its own complete history even if calculated values are identical
#### Scenario: Existing file lacks a final line separator #### Scenario: Existing file lacks a final line separator
@@ -126,20 +126,65 @@ Each named Evelyn instance SHALL publish a safe, non-null, oldest-to-newest snap
- **THEN** its memory is cleared at stop and its complete unchanged persistent history is revalidated and restored before sampling resumes - **THEN** its memory is cleared at stop and its complete unchanged persistent history is revalidated and restored before sampling resumes
### Requirement: Evelyn Mission Control visualizes status-index measurements ### Requirement: Evelyn Mission Control visualizes status-index measurements
Evelyn Mission Control SHALL display separate live Production and Test Overview charts exclusively from their corresponding authoritative Evelyn snapshots and SHALL never read or manage persistence directly. Each chart SHALL contain one enabled series named Evelyn Price Index; the five future series and their typed fields SHALL remain available as disabled source scaffolding. Opening or reopening EMC SHALL immediately reconstruct each chart from the complete restored current snapshot before periodically refreshing it. Successful points SHALL be added to chart objects only on the JavaFX Application Thread while the window remains open. Timestamp bounds SHALL expand as points arrive, and only enabled Evelyn Price Index values SHALL determine a dynamic Y-axis range that is symmetric around and always displays zero, using a reasonable default range for empty or all-zero history. Hiding or closing EMC SHALL release only EMC's JavaFX refresh resources and SHALL NOT start, stop, close, persist, or clear either Evelyn environment. Evelyn Mission Control SHALL display separate live Production and Test Overview charts exclusively from their corresponding authoritative `EvelynStatusService` snapshots and SHALL never read or manage persistence directly. Each chart SHALL contain one enabled series named Evelyn Price Index; the five future series and their typed fields SHALL remain available as disabled source scaffolding. Opening or reopening EMC SHALL immediately reconstruct each chart from the complete restored current snapshot before periodically refreshing it. Successful points SHALL be added to chart objects only on the JavaFX Application Thread while the window remains open. Timestamp bounds SHALL expand as points arrive, and only enabled Evelyn Price Index values SHALL determine a dynamic Y-axis range that is symmetric around and always displays zero, using a reasonable default range for empty or all-zero history. Hiding or closing EMC SHALL release only EMC's JavaFX refresh resources and SHALL NOT start, stop, close, persist, or clear either status-service instance.
#### Scenario: Production and Test Overviews show restored history #### Scenario: Production and Test Overviews show restored history
- **WHEN** Mission Control opens after two named Evelyn environments have restored their histories - **WHEN** Mission Control opens after two named `EvelynStatusService` instances have restored their histories
- **THEN** it displays separate one-series charts immediately populated from the corresponding complete snapshots - **THEN** it displays separate one-series charts immediately populated from the corresponding complete snapshots
#### Scenario: A successful point appears while EMC is open #### Scenario: A successful point appears while EMC is open
- **WHEN** an Evelyn history publishes a newly persisted point - **WHEN** a status-service history publishes a newly persisted point
- **THEN** the corresponding chart adds it on the JavaFX Application Thread and updates its timestamp and symmetric price-index bounds without reopening EMC - **THEN** the corresponding chart adds it on the JavaFX Application Thread and updates its timestamp and symmetric price-index bounds without reopening EMC
#### Scenario: EMC reopens after collecting hidden-window samples #### Scenario: EMC reopens after collecting hidden-window samples
- **WHEN** the same EMC instance is reopened after Evelyn persisted additional points while its window was hidden - **WHEN** the same EMC instance is reopened after the status services persisted additional points while its window was hidden
- **THEN** each chart is reconstructed immediately from its environment's complete current history without missing points because of stale rendering state - **THEN** each chart is reconstructed immediately from its environment's complete current history without missing points because of stale rendering state
#### Scenario: Future fields contain larger values #### Scenario: Future fields contain larger values
- **WHEN** any disabled future-index field has a greater absolute value than Evelyn Price Index - **WHEN** any disabled future-index field has a greater absolute value than Evelyn Price Index
- **THEN** that disabled value does not affect the visible Y-axis bounds - **THEN** that disabled value does not affect the visible Y-axis bounds
### Requirement: Status service has an explicit public boundary
The system SHALL expose `com.r35157.evelyn.service.status.EvelynStatusService` as the status-history and reference-price API. Its process IDs SHALL be `evelyn.status.prod` and `evelyn.status.test`. The service rename SHALL preserve the Production/Test storage identities, existing lifecycle operations, `EvelynStatusIndexPoint` and its six fields. Evelyn SHALL remain the overall system name, with NenjimHub owning individual process startup.
#### Scenario: Consumer uses the renamed status API
- **WHEN** a consumer requests status history or a reference price
- **THEN** it uses `EvelynStatusService` without accessing an implementation helper or a new service container
#### Scenario: Existing Production and Test storage survives the rename
- **WHEN** the renamed services are constructed and started with the existing Production and Test identities
- **THEN** they use the unchanged `data/evelyn/Production/status.log` and `data/evelyn/Test/status.log` files and format version 1 without migration or provisioning
### Requirement: Reference price is calculated on demand with an explicit quote currency
`EvelynStatusService` SHALL expose `AssetPrice getReferencePrice(Instant timestamp)`. For each supplied instant on or after 2026-07-31T22:00:00Z it SHALL calculate 15.00 × 1.20^(elapsedSeconds / 31,536,000), returning the canonical EVE/USDT trading pair. It SHALL preserve the existing USDT-as-USD index convention without conversion, duplicate constants, separate configuration, cached historical measurements, or dependence on a sampling interval.
#### Scenario: Public reference price at the start
- **WHEN** a consumer requests the reference price for 2026-07-31T22:00:00Z
- **THEN** the price is 15 with the canonical EVE base and USDT quote currencies
#### Scenario: Public reference price after one year
- **WHEN** a consumer requests the reference price exactly 31,536,000 seconds after the start
- **THEN** the price is 18 with the same canonical trading pair
#### Scenario: Supplied timestamp determines the price
- **WHEN** a consumer requests a valid instant distinct from the clock or latest historical point
- **THEN** the returned value is calculated for that instant rather than the current clock or historical sample
### Requirement: Reference-price requests are independent of sampling and storage
A reference-price request SHALL succeed before sampling starts and while sampling is stopped, including when history storage has not been provisioned. It SHALL NOT fetch market prices, access history files, start a worker, or mutate status history. A null timestamp SHALL be rejected with `NullPointerException`; an instant before the reference curve starts SHALL be rejected with `IllegalArgumentException`.
#### Scenario: Reference price before startup without storage
- **WHEN** a constructed service with no provisioned history storage receives a valid reference-price request
- **THEN** it returns the calculated value without a ticker lookup, file access, worker startup, or history change
#### Scenario: Reference price while sampling is stopped
- **WHEN** sampling is stopped and a valid reference-price request is made
- **THEN** the service returns the calculated value without restarting sampling or reading, writing, or mutating history
#### Scenario: Reference price during sampling preserves history
- **WHEN** reference-price requests are made while the service holds a status-history snapshot
- **THEN** the requests do not add or alter any status point or file record
#### Scenario: Null or pre-start timestamp is rejected
- **WHEN** a caller supplies null or an instant before 2026-07-31T22:00:00Z
- **THEN** the service rejects it with the respective null or argument exception without side effects
@@ -12,7 +12,7 @@ Provide one typed, read-only catalogue of constructed Nenjim components and a li
- **THEN** its stored value is `nenjim.registry.service` - **THEN** its stored value is `nenjim.registry.service`
#### Scenario: Valid component IDs are accepted #### Scenario: Valid component IDs are accepted
- **WHEN** IDs such as `nenjim.registry.service`, `assetaz.price-source.raydium-pool`, or `evelyn.service.prod` are constructed - **WHEN** IDs such as `nenjim.registry.service`, `assetaz.price-source.raydium-pool`, or `evelyn.status.prod` are constructed
- **THEN** construction succeeds - **THEN** construction succeeds
#### Scenario: Invalid component IDs are rejected #### Scenario: Invalid component IDs are rejected
@@ -85,7 +85,7 @@ After validating the entire plan, the Hub SHALL construct the configured Registr
- **THEN** startup stops immediately with Registry remaining recorded and no configured process after Registry constructed or started - **THEN** startup stops immediately with Registry remaining recorded and no configured process after Registry constructed or started
### Requirement: Selected processes are constructed and started lazily in exact order ### Requirement: Selected processes are constructed and started lazily in exact order
The Hub SHALL support the modern process IDs `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `jupiter-perps-alarm.default`, `evelyn.service.prod`, `evelyn.service.test`, `evelyn.iou-burner.prod`, `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` through implementation-only construction capability that creates no process instance during validation. After passive composition, the Hub SHALL visit every configured entry after Registry in declared order, beginning with Journal. At each entry it SHALL construct only that process and any process-specific passive administration view, register the process under the exact configured ID, resolve it through the canonical `NenjimProcess` contract where applicable, call `start()`, and record the ID only after successful return. The Hub SHALL support the modern process IDs `nenjim.registry.service`, `nenjim.journal.service`, `assetaz.ticker.default`, `jupiter-perps-alarm.default`, `evelyn.status.prod`, `evelyn.status.test`, `evelyn.iou-burner.prod`, `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` through implementation-only construction capability that creates no process instance during validation. After passive composition, the Hub SHALL visit every configured entry after Registry in declared order, beginning with Journal. At each entry it SHALL construct only that process and any process-specific passive administration view, register the process under the exact configured ID, resolve it through the canonical `NenjimProcess` contract where applicable, call `start()`, and record the ID only after successful return.
The configuration SHALL be the complete manually ordered process dependency closure. The Hub SHALL not discover, insert, recursively construct, reorder, or automatically start dependencies. A process omitted from `[AUTO_START]` SHALL not be constructed, registered, started, or recorded, even when its implementation is available. Legacy process prototypes outside the modern Hub graph SHALL not be selectable. The configuration SHALL be the complete manually ordered process dependency closure. The Hub SHALL not discover, insert, recursively construct, reorder, or automatically start dependencies. A process omitted from `[AUTO_START]` SHALL not be constructed, registered, started, or recorded, even when its implementation is available. Legacy process prototypes outside the modern Hub graph SHALL not be selectable.
@@ -109,6 +109,14 @@ The configuration SHALL be the complete manually ordered process dependency clos
- **WHEN** a valid headless plan omits `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default` - **WHEN** a valid headless plan omits `nenjim.javafx-runtime.service`, `evelyn.mission-control.default`, and `nenjim.process-manager.default`
- **THEN** none of those objects is constructed or registered and the JavaFX toolkit remains uninitialized by the Hub - **THEN** none of those objects is constructed or registered and the JavaFX toolkit remains uninitialized by the Hub
#### Scenario: Renamed status services retain dependency wiring
- **WHEN** `evelyn.status.prod` and `evelyn.status.test` are listed after Ticker and before Mission Control
- **THEN** the Hub constructs separate Production/Test status services, registers the renamed IDs, and injects the corresponding typed service views into Mission Control in the configured order
#### Scenario: Deployment still contains an old status ID
- **WHEN** a private deployment configuration contains `evelyn.service.prod` or `evelyn.service.test`
- **THEN** validation rejects the unknown ID before startup; deployment migration replaces only that ID with `evelyn.status.prod` or `evelyn.status.test`, preserving selection and order
### Requirement: Configured startup is fail-fast and reports actual milestones ### Requirement: Configured startup is fail-fast and reports actual milestones
Registry and every selected process SHALL be registered before its `start()` call and appended to the Hub's immutable ordered startup snapshot only after `start()` returns successfully. Construction failure SHALL leave the selected process unregistered. Registration or startup failure SHALL stop the plan before any later entry is constructed or started; a startup failure MAY leave that process registered but SHALL not record it. Earlier successful processes SHALL remain in snapshots, no rollback or retry SHALL occur, and the Hub SHALL announce its existing online point and enter its blocking runtime wait only after every configured process succeeds. Registry and every selected process SHALL be registered before its `start()` call and appended to the Hub's immutable ordered startup snapshot only after `start()` returns successfully. Construction failure SHALL leave the selected process unregistered. Registration or startup failure SHALL stop the plan before any later entry is constructed or started; a startup failure MAY leave that process registered but SHALL not record it. Earlier successful processes SHALL remain in snapshots, no rollback or retry SHALL occur, and the Hub SHALL announce its existing online point and enter its blocking runtime wait only after every configured process succeeds.
@@ -1,41 +0,0 @@
package com.fanitas.evelyn.core;
import com.r35157.nenjim.component.NenjimComponent;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Collects and exposes the status-index history for one named Evelyn instance.
*/
public interface Evelyn extends NenjimComponent, AutoCloseable {
/**
* Executes the legacy Evelyn service operation.
*
* @throws Exception when service execution fails
*/
void executeService() throws Exception;
/**
* Restores persisted history and starts status-index sampling.
*/
void start();
/**
* Stops sampling and clears memory while retaining persistent ownership.
*/
void stop();
/**
* Permanently closes this instance and releases its persistent name.
*/
@Override
void close();
/**
* Returns an immutable snapshot containing only durably persisted points.
*
* @return complete current history ordered from oldest to newest
*/
@NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory();
}
@@ -1,6 +1,6 @@
package com.r35157.evelyn.emc.impl.ref; package com.r35157.evelyn.emc.impl.ref;
import com.fanitas.evelyn.core.Evelyn; import com.r35157.evelyn.service.status.EvelynStatusService;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint; import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.evelyn.emc.EvelynMissionControl; import com.r35157.evelyn.emc.EvelynMissionControl;
import com.r35157.nenjim.process.NenjimProcess; import com.r35157.nenjim.process.NenjimProcess;
@@ -33,12 +33,12 @@ public final class EvelynMissionControlImpl implements
NenjimProcess { NenjimProcess {
public EvelynMissionControlImpl( public EvelynMissionControlImpl(
@NotNull Evelyn evelynServiceProd, @NotNull EvelynStatusService evelynStatusServiceProd,
@NotNull Evelyn evelynServiceTest, @NotNull EvelynStatusService evelynStatusServiceTest,
@NotNull JavaFxRuntimeService javaFxRuntimeService @NotNull JavaFxRuntimeService javaFxRuntimeService
) { ) {
this.evelynProd = Objects.requireNonNull(evelynServiceProd, "evelynServiceProd"); this.evelynStatusProd = Objects.requireNonNull(evelynStatusServiceProd, "evelynStatusServiceProd");
this.evelynTest = Objects.requireNonNull(evelynServiceTest, "evelynServiceTest"); this.evelynStatusTest = Objects.requireNonNull(evelynStatusServiceTest, "evelynStatusServiceTest");
this.javaFxRuntimeService = Objects.requireNonNull( this.javaFxRuntimeService = Objects.requireNonNull(
javaFxRuntimeService, javaFxRuntimeService,
"javaFxRuntimeService"); "javaFxRuntimeService");
@@ -253,8 +253,8 @@ public final class EvelynMissionControlImpl implements
} }
private void refreshCharts() { private void refreshCharts() {
refreshChart(productionChart, evelynProd.getStatusIndexHistory()); refreshChart(productionChart, evelynStatusProd.getStatusIndexHistory());
refreshChart(testChart, evelynTest.getStatusIndexHistory()); refreshChart(testChart, evelynStatusTest.getStatusIndexHistory());
} }
static final class StatusIndexChart { static final class StatusIndexChart {
@@ -313,8 +313,8 @@ public final class EvelynMissionControlImpl implements
} }
}; };
private final Evelyn evelynProd; private final EvelynStatusService evelynStatusProd;
private final Evelyn evelynTest; private final EvelynStatusService evelynStatusTest;
private final JavaFxRuntimeService javaFxRuntimeService; private final JavaFxRuntimeService javaFxRuntimeService;
private final Timeline refreshTimeline = new Timeline(new KeyFrame( private final Timeline refreshTimeline = new Timeline(new KeyFrame(
Duration.seconds(CHART_REFRESH_SECONDS), Duration.seconds(CHART_REFRESH_SECONDS),
@@ -0,0 +1,59 @@
package com.r35157.evelyn.service.status;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.libs.valuetypes.basic.AssetPrice;
import com.r35157.nenjim.component.NenjimComponent;
import org.jetbrains.annotations.NotNull;
import java.time.Instant;
import java.util.List;
/**
* Collects status-index history and exposes the calculated reference price
* for one named Evelyn instance.
*/
public interface EvelynStatusService extends NenjimComponent, AutoCloseable {
/**
* Executes the legacy Evelyn service operation.
*
* @throws Exception when service execution fails
*/
void executeService() throws Exception;
/**
* Restores persisted history and starts status-index sampling.
*/
void start();
/**
* Stops sampling and clears memory while retaining persistent ownership.
*/
void stop();
/**
* Permanently closes this instance and releases its persistent name.
*/
@Override
void close();
/**
* Returns an immutable snapshot containing only durably persisted points.
*
* @return complete current history ordered from oldest to newest
*/
@NotNull List<EvelynStatusIndexPoint> getStatusIndexHistory();
/**
* Calculates the EVE reference price for the supplied instant, quoted in
* the service's canonical EVE/USDT trading pair. The curve starts at 15.00
* on 2026-07-31T22:00:00Z and grows by a factor of 1.20 per 365-day year.
* This operation works before sampling starts and while sampling is stopped;
* it does not access market prices or history storage or mutate history.
*
* @param timestamp instant for which to calculate the reference price
* @return calculated reference price with its explicit trading pair
* @throws NullPointerException if the timestamp is null
* @throws IllegalArgumentException if the timestamp precedes the curve start
*/
@NotNull AssetPrice getReferencePrice(@NotNull Instant timestamp);
}
@@ -1,4 +1,4 @@
package com.fanitas.evelyn.core.impl.ref; package com.r35157.evelyn.service.status.impl.ref;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -1,8 +1,9 @@
package com.fanitas.evelyn.core.impl.ref; package com.r35157.evelyn.service.status.impl.ref;
import com.fanitas.evelyn.core.*; import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated; import com.fanitas.evelyn.raydium.RaydiumLiquidityPoolPositionConcentrated;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.libs.basic.Pair; import com.r35157.libs.basic.Pair;
import com.r35157.libs.basic.Triblet; import com.r35157.libs.basic.Triblet;
import com.r35157.libs.raydium.Raydium; import com.r35157.libs.raydium.Raydium;
@@ -46,9 +47,9 @@ import java.util.concurrent.ConcurrentHashMap;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM; import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM; import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
public class EvelynImpl implements Evelyn, NenjimProcess { public class EvelynStatusServiceImpl implements EvelynStatusService, NenjimProcess {
public EvelynImpl( public EvelynStatusServiceImpl(
@NotNull String instanceName, @NotNull String instanceName,
@NotNull TickerService tickerService, @NotNull TickerService tickerService,
@NotNull TradingPair eveUsdtTradingPair, @NotNull TradingPair eveUsdtTradingPair,
@@ -65,7 +66,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
); );
} }
EvelynImpl( EvelynStatusServiceImpl(
@NotNull String instanceName, @NotNull String instanceName,
@NotNull TickerService tickerService, @NotNull TickerService tickerService,
@NotNull TradingPair eveUsdtTradingPair, @NotNull TradingPair eveUsdtTradingPair,
@@ -97,7 +98,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
.normalize(); .normalize();
if (!validatedDataRoot.equals(validatedInstancePath.getParent())) { if (!validatedDataRoot.equals(validatedInstancePath.getParent())) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Evelyn instance name must resolve to a direct child of " "EvelynStatusService instance name must resolve to a direct child of "
+ validatedDataRoot + ": " + normalizedInstanceName + validatedDataRoot + ": " + normalizedInstanceName
); );
} }
@@ -108,7 +109,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
if (!RESERVED_INSTANCE_NAMES.add(reservationKey)) { if (!RESERVED_INSTANCE_NAMES.add(reservationKey)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Evelyn instance name is already reserved in this JVM: " "EvelynStatusService instance name is already reserved in this JVM: "
+ normalizedInstanceName + normalizedInstanceName
); );
} }
@@ -135,13 +136,13 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
public synchronized void start() { public synchronized void start() {
if (closeRequested || lifecycleState == LifecycleState.CLOSED) { if (closeRequested || lifecycleState == LifecycleState.CLOSED) {
throw new IllegalStateException( throw new IllegalStateException(
"Closing or closed Evelyn instance cannot be started: " "Closing or closed EvelynStatusService instance cannot be started: "
+ instanceName + instanceName
); );
} }
if (lifecycleState != LifecycleState.STOPPED) { if (lifecycleState != LifecycleState.STOPPED) {
throw new IllegalStateException( throw new IllegalStateException(
"Evelyn instance cannot start while in state " "EvelynStatusService instance cannot start while in state "
+ lifecycleState + ": " + instanceName + lifecycleState + ": " + instanceName
); );
} }
@@ -154,7 +155,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
clearStatusIndexHistory(); clearStatusIndexHistory();
lifecycleState = LifecycleState.STOPPED; lifecycleState = LifecycleState.STOPPED;
throw new IllegalStateException( throw new IllegalStateException(
"Could not start Evelyn instance '" + instanceName "Could not start EvelynStatusService instance '" + instanceName
+ "' from " + statusHistoryPath + ": " + "' from " + statusHistoryPath + ": "
+ exception.getMessage(), + exception.getMessage(),
exception exception
@@ -177,7 +178,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
clearStatusIndexHistory(); clearStatusIndexHistory();
lifecycleState = LifecycleState.STOPPED; lifecycleState = LifecycleState.STOPPED;
throw new IllegalStateException( throw new IllegalStateException(
"Could not create Evelyn sampling executor for instance '" "Could not create EvelynStatusService sampling executor for instance '"
+ instanceName + "'", + instanceName + "'",
exception exception
); );
@@ -203,7 +204,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
clearStatusIndexHistory(); clearStatusIndexHistory();
lifecycleState = LifecycleState.STOPPED; lifecycleState = LifecycleState.STOPPED;
throw new IllegalStateException( throw new IllegalStateException(
"Could not start Evelyn sampling for instance '" "Could not start EvelynStatusService sampling for instance '"
+ instanceName + "'", + instanceName + "'",
exception exception
); );
@@ -222,7 +223,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
if (lifecycleState != LifecycleState.STARTED if (lifecycleState != LifecycleState.STARTED
&& lifecycleState != LifecycleState.TERMINATION_FAILED) { && lifecycleState != LifecycleState.TERMINATION_FAILED) {
throw new IllegalStateException( throw new IllegalStateException(
"Evelyn instance cannot stop while in state " "EvelynStatusService instance cannot stop while in state "
+ lifecycleState + ": " + instanceName + lifecycleState + ": " + instanceName
); );
} }
@@ -244,7 +245,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
lifecycleState = LifecycleState.TERMINATION_FAILED; lifecycleState = LifecycleState.TERMINATION_FAILED;
} }
throw new IllegalStateException( throw new IllegalStateException(
"Interrupted while stopping Evelyn instance '" "Interrupted while stopping EvelynStatusService instance '"
+ instanceName + "'; name reservation retained", + instanceName + "'; name reservation retained",
exception exception
); );
@@ -255,7 +256,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
lifecycleState = LifecycleState.TERMINATION_FAILED; lifecycleState = LifecycleState.TERMINATION_FAILED;
} }
throw new IllegalStateException( throw new IllegalStateException(
"Evelyn sampling thread did not terminate for instance '" "EvelynStatusService sampling thread did not terminate for instance '"
+ instanceName + "'; name reservation retained" + instanceName + "'; name reservation retained"
); );
} }
@@ -282,7 +283,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
if (lifecycleState != LifecycleState.STOPPED if (lifecycleState != LifecycleState.STOPPED
|| statusIndexScheduler != null) { || statusIndexScheduler != null) {
throw new IllegalStateException( throw new IllegalStateException(
"Cannot close Evelyn instance while sampling termination " "Cannot close EvelynStatusService instance while sampling termination "
+ "is unconfirmed: " + instanceName + "is unconfirmed: " + instanceName
); );
} }
@@ -298,6 +299,14 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
} }
} }
@Override
public @NotNull AssetPrice getReferencePrice(@NotNull Instant timestamp) {
return new AssetPrice(
EvelynPriceIndexCalculator.calculateExpectedPrice(timestamp),
eveUsdtTradingPair
);
}
private void clearStatusIndexHistory() { private void clearStatusIndexHistory() {
synchronized (statusIndexHistory) { synchronized (statusIndexHistory) {
statusIndexHistory.clear(); statusIndexHistory.clear();
@@ -686,7 +695,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
private static String validateInstanceName(String instanceName) { private static String validateInstanceName(String instanceName) {
Objects.requireNonNull(instanceName, "instanceName"); Objects.requireNonNull(instanceName, "instanceName");
String normalizedName = Normalizer.normalize(instanceName, Normalizer.Form.NFC); String normalizedName = Normalizer.normalize(instanceName, Normalizer.Form.NFC);
if (normalizedName.codePoints().allMatch(EvelynImpl::isWhitespace) if (normalizedName.codePoints().allMatch(EvelynStatusServiceImpl::isWhitespace)
|| hasWhitespaceAtBoundary(normalizedName) || hasWhitespaceAtBoundary(normalizedName)
|| normalizedName.equals(".") || normalizedName.equals(".")
|| normalizedName.equals("..") || normalizedName.equals("..")
@@ -698,7 +707,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
) )
|| isWindowsReservedFilename(normalizedName)) { || isWindowsReservedFilename(normalizedName)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Unsafe Evelyn instance name: " + normalizedName "Unsafe EvelynStatusService instance name: " + normalizedName
); );
} }
return normalizedName; return normalizedName;
@@ -734,7 +743,7 @@ public class EvelynImpl implements Evelyn, NenjimProcess {
return false; return false;
} }
private static final Logger log = LoggerFactory.getLogger(EvelynImpl.class); private static final Logger log = LoggerFactory.getLogger(EvelynStatusServiceImpl.class);
private static final Path DATA_ROOT = Path.of("data", "evelyn"); private static final Path DATA_ROOT = Path.of("data", "evelyn");
private static final String STATUS_HISTORY_FILENAME = "status.log"; private static final String STATUS_HISTORY_FILENAME = "status.log";
private static final String FORMAT_VERSION_KEY = "FORMAT_VERSION"; private static final String FORMAT_VERSION_KEY = "FORMAT_VERSION";
@@ -3,8 +3,8 @@ package com.r35157.nenjim.hubd.impl.ref;
import com.r35157.assetaz.services.journal.impl.ref.AssetAZJournalServiceImpl; import com.r35157.assetaz.services.journal.impl.ref.AssetAZJournalServiceImpl;
import com.r35157.assetaz.services.journal.codec.impl.ref.AssetOwnerRegisteredCodec; import com.r35157.assetaz.services.journal.codec.impl.ref.AssetOwnerRegisteredCodec;
import com.r35157.assetaz.services.journal.codec.impl.ref.JournalFormatVersionChangedCodec; import com.r35157.assetaz.services.journal.codec.impl.ref.JournalFormatVersionChangedCodec;
import com.fanitas.evelyn.core.Evelyn; import com.r35157.evelyn.service.status.EvelynStatusService;
import com.fanitas.evelyn.core.impl.ref.EvelynImpl; import com.r35157.evelyn.service.status.impl.ref.EvelynStatusServiceImpl;
import com.fanitas.evelyn.service.burner.EvelynIOUBurnerService; import com.fanitas.evelyn.service.burner.EvelynIOUBurnerService;
import com.fanitas.evelyn.service.burner.impl.ref.EvelynIOUBurnerServiceImpl; import com.fanitas.evelyn.service.burner.impl.ref.EvelynIOUBurnerServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -274,29 +274,29 @@ public final class NenjimHubImpl implements NenjimHub {
return process; return process;
} }
private NenjimProcess constructEvelynProdProcess() { private NenjimProcess constructEvelynStatusProdProcess() {
EvelynImpl process = new EvelynImpl( EvelynStatusServiceImpl process = new EvelynStatusServiceImpl(
"Production", "Production",
requireProcessDependency( requireProcessDependency(
tickerService, tickerService,
EVELYN_SERVICE_PROD_ID, EVELYN_STATUS_PROD_ID,
TICKER_SERVICE_ID), TICKER_SERVICE_ID),
eveUSDT, eveUSDT,
clockUTC); clockUTC);
evelynServiceProd = process; evelynStatusServiceProd = process;
return process; return process;
} }
private NenjimProcess constructEvelynTestProcess() { private NenjimProcess constructEvelynStatusTestProcess() {
EvelynImpl process = new EvelynImpl( EvelynStatusServiceImpl process = new EvelynStatusServiceImpl(
"Test", "Test",
requireProcessDependency( requireProcessDependency(
tickerService, tickerService,
EVELYN_SERVICE_TEST_ID, EVELYN_STATUS_TEST_ID,
TICKER_SERVICE_ID), TICKER_SERVICE_ID),
eveUSDT, eveUSDT,
clockUTC); clockUTC);
evelynServiceTest = process; evelynStatusServiceTest = process;
return process; return process;
} }
@@ -335,13 +335,13 @@ public final class NenjimHubImpl implements NenjimHub {
private NenjimProcess constructMissionControlProcess() { private NenjimProcess constructMissionControlProcess() {
EvelynMissionControlImpl process = new EvelynMissionControlImpl( EvelynMissionControlImpl process = new EvelynMissionControlImpl(
requireProcessDependency( requireProcessDependency(
evelynServiceProd, evelynStatusServiceProd,
EVELYN_MISSION_CONTROL_ID, EVELYN_MISSION_CONTROL_ID,
EVELYN_SERVICE_PROD_ID), EVELYN_STATUS_PROD_ID),
requireProcessDependency( requireProcessDependency(
evelynServiceTest, evelynStatusServiceTest,
EVELYN_MISSION_CONTROL_ID, EVELYN_MISSION_CONTROL_ID,
EVELYN_SERVICE_TEST_ID), EVELYN_STATUS_TEST_ID),
requireProcessDependency( requireProcessDependency(
javaFxRuntimeService, javaFxRuntimeService,
EVELYN_MISSION_CONTROL_ID, EVELYN_MISSION_CONTROL_ID,
@@ -385,8 +385,8 @@ public final class NenjimHubImpl implements NenjimHub {
registryService, Path.of("data/assetaz/journal/events.pde"))), registryService, Path.of("data/assetaz/journal/events.pde"))),
Map.entry(TICKER_SERVICE_ID, this::constructTickerProcess), Map.entry(TICKER_SERVICE_ID, this::constructTickerProcess),
Map.entry(JUPITER_PERPS_ALARM_ID, this::constructJupiterPerpsAlarmProcess), Map.entry(JUPITER_PERPS_ALARM_ID, this::constructJupiterPerpsAlarmProcess),
Map.entry(EVELYN_SERVICE_PROD_ID, this::constructEvelynProdProcess), Map.entry(EVELYN_STATUS_PROD_ID, this::constructEvelynStatusProdProcess),
Map.entry(EVELYN_SERVICE_TEST_ID, this::constructEvelynTestProcess), Map.entry(EVELYN_STATUS_TEST_ID, this::constructEvelynStatusTestProcess),
Map.entry(EVELYN_IOU_BURNER_PROD_ID, this::constructEvelynIOUBurnerProdProcess), Map.entry(EVELYN_IOU_BURNER_PROD_ID, this::constructEvelynIOUBurnerProdProcess),
Map.entry(JAVAFX_RUNTIME_SERVICE_ID, this::constructJavaFxRuntimeProcess), Map.entry(JAVAFX_RUNTIME_SERVICE_ID, this::constructJavaFxRuntimeProcess),
Map.entry(EVELYN_MISSION_CONTROL_ID, this::constructMissionControlProcess), Map.entry(EVELYN_MISSION_CONTROL_ID, this::constructMissionControlProcess),
@@ -491,10 +491,10 @@ public final class NenjimHubImpl implements NenjimHub {
new NenjimComponentId("jupiter.perps.evelyn-test"); new NenjimComponentId("jupiter.perps.evelyn-test");
private static final NenjimComponentId JUPITER_PERPS_ALARM_ID = private static final NenjimComponentId JUPITER_PERPS_ALARM_ID =
new NenjimComponentId("jupiter-perps-alarm.default"); new NenjimComponentId("jupiter-perps-alarm.default");
private static final NenjimComponentId EVELYN_SERVICE_PROD_ID = private static final NenjimComponentId EVELYN_STATUS_PROD_ID =
new NenjimComponentId("evelyn.service.prod"); new NenjimComponentId("evelyn.status.prod");
private static final NenjimComponentId EVELYN_SERVICE_TEST_ID = private static final NenjimComponentId EVELYN_STATUS_TEST_ID =
new NenjimComponentId("evelyn.service.test"); new NenjimComponentId("evelyn.status.test");
private static final NenjimComponentId JUPITER_SWAP_EVELYN_BURNER_PROD_ID = private static final NenjimComponentId JUPITER_SWAP_EVELYN_BURNER_PROD_ID =
new NenjimComponentId("jupiter.swap.evelyn-burner-prod"); new NenjimComponentId("jupiter.swap.evelyn-burner-prod");
private static final NenjimComponentId DISCORD_ASSETAZ_TOKEN_NOTIFICATION_ID = private static final NenjimComponentId DISCORD_ASSETAZ_TOKEN_NOTIFICATION_ID =
@@ -539,8 +539,8 @@ public final class NenjimHubImpl implements NenjimHub {
// Services // Services
private TickerService tickerService; private TickerService tickerService;
private JupiterPerpsAlarm jupiterPerpsAlarm; private JupiterPerpsAlarm jupiterPerpsAlarm;
private Evelyn evelynServiceProd; private EvelynStatusService evelynStatusServiceProd;
private Evelyn evelynServiceTest; private EvelynStatusService evelynStatusServiceTest;
private JupiterSwapService jupiterSwapService; private JupiterSwapService jupiterSwapService;
private BoundNotificationService discordAssetAZTokenNotificationService; private BoundNotificationService discordAssetAZTokenNotificationService;
private EvelynIOUBurnerService evelynIOUBurnerServiceProd; private EvelynIOUBurnerService evelynIOUBurnerServiceProd;
@@ -0,0 +1,214 @@
package com.r35157.evelyn.service.status.impl.ref;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.assetaz.services.cis.impl.hc.HardcodedCurrencyIdentityService;
import com.r35157.assetaz.services.ticker.PriceObservation;
import com.r35157.assetaz.services.ticker.TickerService;
import com.r35157.evelyn.service.status.EvelynStatusService;
import com.r35157.libs.valuetypes.basic.AssetPrice;
import com.r35157.libs.valuetypes.basic.TradingPair;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.EVE_ID;
import static com.r35157.assetaz.services.cis.CurrencyTypeIds.USDT_ID;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EvelynStatusServiceImplTest {
@Test
void publicApiCalculatesRequestedCurveWithCanonicalTradingPair(@TempDir Path root) {
try (EvelynStatusService service = service("Curve", UNUSED_TICKER, UNUSED_CLOCK, root)) {
assertPrice("15", service.getReferencePrice(START));
assertPrice("18", service.getReferencePrice(START.plusSeconds(31_536_000)));
AssetPrice fractional = service.getReferencePrice(START.plusSeconds(1_566_494));
assertTrue(fractional.price().subtract(new BigDecimal("15.136464436276"))
.abs().compareTo(new BigDecimal("0.000000000001")) < 0);
assertSame(EVE_USDT, fractional.tradingPair());
assertTrue(service.getStatusIndexHistory().isEmpty());
assertTrue(Files.notExists(root.resolve("Curve")));
}
}
@Test
void publicApiRejectsNullAndPreStartInstantsWithoutSideEffects(@TempDir Path root) {
try (EvelynStatusService service = service("Invalid", UNUSED_TICKER, UNUSED_CLOCK, root)) {
assertThrows(NullPointerException.class, () -> service.getReferencePrice(null));
assertThrows(IllegalArgumentException.class,
() -> service.getReferencePrice(START.minusNanos(1)));
assertTrue(service.getStatusIndexHistory().isEmpty());
assertTrue(Files.notExists(root.resolve("Invalid")));
}
}
@Test
void referencePriceWorksWhileStoppedWithoutProvisionedStorage(@TempDir Path root) {
try (EvelynStatusService service = service("Stopped", UNUSED_TICKER, UNUSED_CLOCK, root)) {
service.stop();
assertPrice("18", service.getReferencePrice(START.plusSeconds(31_536_000)));
assertTrue(service.getStatusIndexHistory().isEmpty());
assertTrue(Files.notExists(root.resolve("Stopped")));
}
}
@Test
void samplingPersistsSharedIndexAndRestoresHistoryAcrossRestart(@TempDir Path root)
throws Exception {
Path history = provision(root, "History", "2");
String original = Files.readString(history);
try (EvelynStatusService service = service("History", ticker("16.5"), FIXED_CLOCK, root)) {
service.start();
List<EvelynStatusIndexPoint> snapshot = awaitHistory(service, 2);
assertDecimal("1", snapshot.get(1).evelynPriceIndex());
assertEquals(START, snapshot.get(1).timestamp());
assertZeroPlaceholders(snapshot.get(1));
assertThrows(UnsupportedOperationException.class, () -> snapshot.add(snapshot.getFirst()));
// Observing a published point must also observe its durable record.
String persisted = Files.readString(history);
assertTrue(persisted.startsWith(original + "\n"));
assertEquals(3, persisted.lines().count());
byte[] beforeRequest = Files.readAllBytes(history);
assertPrice("18", service.getReferencePrice(START.plusSeconds(31_536_000)));
assertEquals(snapshot, service.getStatusIndexHistory());
assertArrayEquals(beforeRequest, Files.readAllBytes(history));
service.stop();
assertTrue(service.getStatusIndexHistory().isEmpty());
assertEquals(2, snapshot.size());
assertArrayEquals(beforeRequest, Files.readAllBytes(history));
Files.delete(history);
assertPrice("15", service.getReferencePrice(START));
assertTrue(Files.notExists(history));
Files.write(history, beforeRequest);
service.start();
List<EvelynStatusIndexPoint> restored = awaitHistory(service, 3);
assertEquals(snapshot, restored.subList(0, 2));
assertDecimal("1", restored.get(2).evelynPriceIndex());
service.stop();
byte[] beforeClose = Files.readAllBytes(history);
service.close();
assertArrayEquals(beforeClose, Files.readAllBytes(history));
assertThrows(IllegalStateException.class, service::start);
}
}
@Test
void productionAndTestKeepIndependentNamedHistories(@TempDir Path root) throws Exception {
Path productionFile = provision(root, "Production", "2");
Path testFile = provision(root, "Test", "-2");
try (EvelynStatusService production = service("Production", ticker("16.5"), FIXED_CLOCK, root);
EvelynStatusService test = service("Test", ticker("13.5"), FIXED_CLOCK, root)) {
production.start();
test.start();
List<EvelynStatusIndexPoint> productionHistory = awaitHistory(production, 2);
List<EvelynStatusIndexPoint> testHistory = awaitHistory(test, 2);
assertDecimal("2", productionHistory.getFirst().evelynPriceIndex());
assertDecimal("-2", testHistory.getFirst().evelynPriceIndex());
assertDecimal("1", productionHistory.get(1).evelynPriceIndex());
assertDecimal("-1", testHistory.get(1).evelynPriceIndex());
assertPrice("15", production.getReferencePrice(START));
assertPrice("15", test.getReferencePrice(START));
byte[] testBytes = Files.readAllBytes(testFile);
production.stop();
assertTrue(production.getStatusIndexHistory().isEmpty());
assertEquals(testHistory, test.getStatusIndexHistory());
assertArrayEquals(testBytes, Files.readAllBytes(testFile));
assertEquals(3, Files.readString(productionFile).lines().count());
}
}
private static EvelynStatusService service(String name, TickerService ticker, Clock clock, Path root) {
return new EvelynStatusServiceImpl(name, ticker, EVE_USDT, clock, 1, TimeUnit.DAYS, root);
}
private static TickerService ticker(String actualPrice) {
return pair -> {
assertSame(EVE_USDT, pair);
return new PriceObservation(new AssetPrice(new BigDecimal(actualPrice), pair), START, "offline");
};
}
private static Path provision(Path root, String name, String existingIndex) throws Exception {
Path history = Files.createDirectory(root.resolve(name)).resolve("status.log");
Files.writeString(history, "FORMAT_VERSION=1\n20260731215959000Z:1:" + existingIndex);
return history;
}
private static List<EvelynStatusIndexPoint> awaitHistory(EvelynStatusService service, int size)
throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
List<EvelynStatusIndexPoint> history;
do {
history = service.getStatusIndexHistory();
if (history.size() == size) {
return history;
}
Thread.sleep(10);
} while (System.nanoTime() < deadline);
assertEquals(size, history.size(), "Expected persisted sample was not published");
return history;
}
private static void assertDecimal(String expected, BigDecimal actual) {
assertEquals(0, new BigDecimal(expected).compareTo(actual));
}
private static void assertPrice(String expected, AssetPrice actual) {
assertDecimal(expected, actual.price());
assertSame(EVE_USDT, actual.tradingPair());
assertEquals(EVE_ID, actual.tradingPair().base().id());
assertEquals(USDT_ID, actual.tradingPair().quote().id());
}
private static void assertZeroPlaceholders(EvelynStatusIndexPoint point) {
assertEquals(BigDecimal.ZERO, point.eveSyrupPoolDepthIndex());
assertEquals(BigDecimal.ZERO, point.eveSyrupPoolBalanceIndex());
assertEquals(BigDecimal.ZERO, point.aazdkkUsdtPoolBalanceIndex());
assertEquals(BigDecimal.ZERO, point.aazdkkUsdtPoolPriceIndex());
assertEquals(BigDecimal.ZERO, point.aazdkkUsdtPoolDepthIndex());
}
private static final Instant START = Instant.parse("2026-07-31T22:00:00Z");
private static final Clock FIXED_CLOCK = Clock.fixed(START, ZoneOffset.UTC);
private static final TradingPair EVE_USDT;
static {
HardcodedCurrencyIdentityService currencies = new HardcodedCurrencyIdentityService();
EVE_USDT = new TradingPair(currencies.resolve(EVE_ID), currencies.resolve(USDT_ID));
}
private static final TickerService UNUSED_TICKER = pair -> {
throw new AssertionError("Reference-price requests must not fetch a market price");
};
private static final Clock UNUSED_CLOCK = new Clock() {
@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}
@Override
public Clock withZone(ZoneId zone) {
return this;
}
@Override
public Instant instant() {
throw new AssertionError("Reference-price requests must use the supplied instant");
}
};
}