## Context See `proposal.md` for motivation and the `assetaz-ticker-service` delta for behavior. Nenjim's planned component model constructs every component independently in a context, leaves it initialized but unstarted, and later lets components query that context for peer plugins by interface. Issue #61 therefore requires plugin-style acquisition without constructor-time coupling between a price source and the ticker, while retaining the existing typed ValueTypes, durable append format, and temporary Cauldron placement. ## Goals / Non-Goals **Goals:** Keep acquisition resources owned by independently constructible sources; make the ticker the source-discovery owner, lifecycle coordinator, and durable sink; isolate histories by stable source identity; preserve greatest-timestamp selection and persist-before-publish under concurrent callbacks; align dependencies with Nenjim's future component context. **Non-Goals:** Implementing Nenjim's final context-query API in this change, dynamic source changes after ticker startup, automatic migration, source health/retry policy, Raydium or streaming integrations, public history queries, consumers, or automated tests. ## Decisions - Add API-package `PriceSource` and functional `PriceSink` interfaces. `PriceSource.start(PriceSink)` performs lifecycle-time wiring, so a source constructor needs neither the ticker nor a sink. `TickerService` exposes `start`, `stop`, and latest-price queries but no `addPriceSource`; plugin management is an implementation responsibility rather than a client capability. `TickerService` does not extend `PriceSink`, while `TickerServiceImpl` implements both contracts. - In the final Nenjim model, `TickerServiceImpl` queries its own context for `PriceSource` implementations after all components have been independently constructed. It does not query for sinks: the ticker is the sink and passes itself to each active source when calling `priceSource.start(this)`. The current public varargs constructor `TickerServiceImpl(PriceSource...)` is temporary Cauldron wiring that supplies exactly the instances a future context lookup will return; it is not public plugin-management API or the final discovery mechanism. - Add `PriceSourceName` beneath `String -> Name` in `conf/detag.conf`, as explicitly authorized by issue #61. Store source identity in `PriceObservation` but not in history lines, because the source directory is the persistent identity and repeated line metadata would create disagreement risks. - Represent each context-provided source internally by the source object, its `(TradingPair, sourceName)` key, derived path, activation state, and a per-history lock. Reject duplicate keys and unsafe names while capturing the constructor-provided/discovered set before startup. Compare callbacks by exact source object identity as well as stable metadata so an unknown impersonating instance cannot address another source's history. - Centralize paths under the ticker data root using canonical base UUID, canonical quote UUID, validated source name, and sanitized symbols in the final filename. Source names are rejected rather than rewritten because they are technical persistent identity. Symbols are non-identity display text and are converted character-by-character to safe filename components, rejecting an empty result. - During `start()`, validate and derive every obtained source, check exact file existence without scanning or creating, load all active histories into local state, compute per-pair maximum timestamps, then commit startup state and start active sources with `source.start(this)`. If loading or starting fails, stop every source already started and leave the ticker stopped rather than expose a partially started lifecycle. - During `stop()`, prevent new callbacks, stop every started source, and aggregate/report lifecycle failures while ensuring every source receives a stop attempt. The obtained source set remains owned by the ticker across a subsequent restart; changing the context-provided set dynamically while starting, started, or stopping is outside this design. - Move the hardcoded price, clock, one-minute delay, and scheduled executor into `HardcodedPriceSource`. Its public constructor has no sink or ticker dependency. `start(PriceSink)` first rejects an already-started call, then stores the supplied sink, announces immediately, and schedules subsequent attempts with `scheduleWithFixedDelay`; checking first prevents a repeated start from replacing the live sink. `stop()` shuts down its executor. The ticker contains none of those acquisition constants or resources. - On `announce`, resolve the exact active registration, validate non-null price and timestamp, construct `AssetPrice` and `PriceObservation`, lock that source history, append the complete UTF-8 line with `FileChannel`, and call `force(true)`. Only afterward atomically update the pair's latest value when the new timestamp is greater. Per-history locks allow independent sources to persist concurrently without interleaving, while atomic per-pair maximum updates preserve cross-source ordering. - Keep strict UTC parsing and the existing comment behavior. Loaded observations receive trading pair and source name from the obtained source context. The old flat history path is deliberately ignored; operator-controlled manual movement is the only migration. - Temporary NenjimHub wiring constructs an unstarted `HardcodedPriceSource` independently, then constructs `TickerServiceImpl` with that source through the varargs Cauldron adapter and starts the ticker. The ticker performs sink wiring and lifecycle. This simulates future context lookup without transferring AssetAZ ownership or adding consumers. ## Risks / Trade-offs - [A source throws during startup after earlier sources started] → Stop all sources that were successfully started, suppress secondary stop failures, and fail ticker startup clearly. - [A source callback races with stop] → Guard lifecycle and active-registration checks so callbacks are accepted only while the ticker is started; source stop is responsible for terminating its own producer resources. - [Multiple source callbacks contend for one trading pair] → Use per-history serialization for files and an atomic greatest-timestamp update for the shared pair view. - [Symbol sanitization can make filenames less recognizable] → Preserve UUIDs as technical identity and use a deterministic safe replacement only for the human-readable leaf filename. - [Manual migration can temporarily deactivate the hardcoded source] → Log the exact expected new path and never create or infer content, making operator action explicit and reversible. ## Migration Plan Before deployment, stop the old service and move `data/assetaz/ticker/EVE_USDC.prices` to `data/assetaz/ticker/019c3f9f-41d1-7a73-b1df-d4c11c7ff301/019c3f9f-41d1-7a73-b1df-d4c11c7ff302/Hardcoded/EVE_USDC.prices`, creating parent directories only as an operator action. Deploy the API, ticker, hardcoded source, Detag configuration, and wiring together. Rollback restores the previous code and moves the file back to the legacy flat path; no automatic data transformation is involved.