78: Make JournalService a Hub-managed Nenjim process and integrate it into startup

This commit is contained in:
2026-09-02 16:30:22 +02:00
parent f5fc5bb3b5
commit a1d1a43556
17 changed files with 773 additions and 212 deletions
@@ -0,0 +1,123 @@
## Context
Issue #75 implemented a strict filesystem-backed Journal and put construction,
initial loading, service access, refresh and stop on `JournalServiceManager`.
Issue #77 subsequently established a common runtime architecture: the Hub
constructs and starts active `NenjimProcess` objects, domain service interfaces
stay lifecycle-free, and service managers are separate passive administrative
handles retained by the Hub.
The Journal format, parser, exact-text digest, chain validation, immutable
queries and explicit refresh behavior are already accepted and must remain
unchanged. Only ownership, lifecycle placement and Hub integration change.
## Goals / Non-Goals
**Goals:**
- Preserve one read-only registrable `JournalService` domain interface.
- Make the same Journal object available as the canonical `NenjimProcess`.
- Publish no queryable initial state until the complete filesystem load
succeeds.
- Keep refresh available only through a distinct passive manager.
- Start and record Journal second, after Registry and before business
composition/startup.
- Keep runtime files read-only and `.journal.example` files ignored.
**Non-Goals:**
- Journal format, model, parser, chain or policy redesign.
- Background watching, polling or remote synchronization.
- Generic stop, restart, health, rollback or recovery.
- Context, resolution, classloading, artifact transfer, configurable autorun or
Process Manager integration.
- New general unit tests or production test hooks.
## Decisions
### One hidden service implementation provides service, process and admin views
`JournalServiceImpl` remains under `.impl.ref` and becomes package-private. It
implements public `JournalService`, public `NenjimProcess`, and a new
package-private `JournalServiceAdmin`. Keeping the concrete type and admin view
non-public prevents ordinary service consumers from naming and casting to the
filesystem administration contract.
`JournalServiceAssembly.create(Path)` constructs one service object, exposes it
through identity-preserving service and process views, and constructs one
distinct `JournalServiceManagerImpl` against its admin view. The assembly owns
no lifecycle and exposes no reverse path from an active service reference to
its manager.
### The service process owns a single startup attempt
The configured root is normalized once during service construction. An atomic
start-attempt guard is consumed before filesystem work, so every second
`start()` call fails with `IllegalStateException`, including after a failed
first attempt. Queries check a volatile availability gate and fail with
`IllegalStateException` until startup has succeeded.
### Initial loading is all-or-nothing
Startup loads every runtime file into a private candidate Journal service using
the existing parser and ingestion rules. Only after the loader has completed
does the active object copy the candidate's immutable chains and digest index
under its state lock and open the availability gate. A root, layout, parse or
chain failure therefore propagates unchanged and leaves the active service with
no queryable partial worldview.
Extracting a separate new mutable chain-store abstraction was rejected because
it would broaden a lifecycle refactor into a Journal-domain redesign. The
candidate uses the already accepted ingestion implementation and is never
published.
### Filesystem loading belongs behind the service admin boundary
The existing strict loader logic moves from the manager into a package-private
`JournalFilesystemLoader`. The Journal process invokes it for initial loading;
the manager delegates refresh through `JournalServiceAdmin`. The loader keeps
the exact root/layout/readability checks, deterministic ordering, placement
validation, `.journal.example` exclusion and no-write behavior.
Post-start refresh loads into the active service and deliberately preserves the
existing per-ingestion atomicity and idempotency. The issue does not strengthen
refresh into whole-filesystem transactional replacement.
### Hub starts Journal as infrastructure before the business graph
After Registry start, registration and successful-history recording,
`NenjimHubImpl` creates the Journal assembly for `data/nenjim/journals`, retains
its manager, registers the service as `nenjim.journal.service`, starts its
process view, and records the ID only after `start()` returns. Business graph
construction and the existing enabled startup sequence occur only afterwards.
Consequently, a Journal failure propagates from the Hub while the Registry is
the sole recorded process. The Hub neither constructs/starts later business
processes nor reaches its online point or blocking wait. No rollback is added
because the common process contract has no stop operation.
## Risks / Trade-offs
- [The hidden implementation was formerly public] → No accepted public API
signature returns it; the assembly exposes only public interfaces and keeps
the documented implementation boundary intact.
- [Loading through a candidate duplicates temporary in-memory state] → Startup
happens once and Journal data is expected to be modest; the temporary cost
buys clear all-or-nothing publication without changing domain semantics.
- [Refresh can accept an earlier new revision before a later file fails] → This
is the already specified per-ingestion atomic behavior and is intentionally
preserved.
- [The Hub's successful path blocks indefinitely] → Verify Journal lifecycle,
identity, boundaries and sample-root loading with temporary probes rather
than invoking production `main()`.
## Migration Plan
1. Change Journal contracts and add the internal admin, loader and assembly
boundary.
2. Move startup into the service process and reduce the manager to passive
delegation.
3. Register/start Journal in the Hub before business composition and startup.
4. Update OpenSpec and directly affected documentation.
5. Run strict OpenSpec validation, the complete Gradle build and safe temporary
boundary/lifecycle probes, then leave this change active for review.