9.3 KiB
Context
See proposal.md for motivation and the two delta specs for observable behavior. Commit 7eeedcc contains the format, immutable domain behavior, parser, chain validation, queries, examples, and documentation, but as an intermediate API under com.r35157.nenjim.journal: a public mutable JournalManager owns chain state and a concrete filesystem JournalService owns refresh. The updated issue supersedes that package and responsibility split. Human-written Java remains in .tjava; Detag runs through the normal Gradle build.
Goals / Non-Goals
Goals:
- Make
JournalServicethe filesystem-independent read-only API andJournalServiceManagerthe lifecycle/data-root/refresh API. - Keep exactly those two interfaces directly in
com.r35157.nenjim.service.journal, with composed models, value types, exceptions, and reference implementation in their prescribed subpackages. - Preserve the already implemented format, immutable construction invariants, chain semantics, queries, exact-text identity, CID canonicality, examples, and diagnostics.
- Keep parser, mutable ingestion, chain storage, and filesystem traversal out of public signatures.
- Ensure a failed initial start never exposes a partially loaded service and a failed individual ingestion never mutates previously accepted state.
Non-Goals:
- No compatibility adapters for
com.r35157.nenjim.journal, no publicJournalManager, and no deprecated or forwarding aliases. - No Context, resolver, selection, classloader, retrieval, publishing, synchronization, watching, signing, UI/CLI, URI interpretation, or license filtering.
- No wiring into NenjimHub startup and no changes to CIS or any unrelated service.
- No automated test sources or test-configuration changes; strict OpenSpec/build checks and temporary runtime probes provide verification.
- No transactional refresh across a whole directory tree. Each ingestion is atomic; a refresh can have accepted earlier valid files before a later file fails.
Decisions
Public packages follow semantic API roles
The package layout is:
com.r35157.nenjim.service.journal: onlyJournalServiceandJournalServiceManager..model:Journal,JournalMetadata,JournalPolicy,ArtifactRelease,ArtifactDependency, andPolicyRule..valuetypes:ArtifactCoordinate,JournalVersion,ContentDigest,DependencyTarget,VersionExpression, andVersionExpressionSet..exception:InvalidJournalException..impl.ref:JournalServiceImpl,JournalServiceManagerImpl,JournalTextParser, and internal helpers.
Composed records belong in model even when they are records; only small values fully defined by their value belong in valuetypes. This explicit split makes the logical module extractable later and prevents the current repository location from becoming accidental API ownership. The old package is deleted rather than wrapped because the issue explicitly replaces the intermediate API.
The service exposes queries while ingestion stays package-private
JournalService contains only coordinate, revision, latest, exact, as-of, and selected-release queries. JournalServiceImpl implements those operations over synchronized in-memory chain state. Its constructor, non-mutating parse helper, mutable text-ingestion method, stored exact text, digest-owner index, and stored-revision type remain package-private or private implementation details.
Internal ingestion parses and validates the complete string before acquiring the state lock, calculates SHA-256 over the exact UTF-8 text, and then checks idempotency, conflicts, genesis/head identity, predecessor ownership, and strictly increasing Journal time before replacing one immutable chain snapshot. Parse or chain failure therefore leaves all existing state unchanged. Query results remain immutable copies or optionals.
The earlier public parser contract is removed. JournalTextParser is a package-private reference helper under .impl.ref; it still owns textual syntax, quoting, ordering, comments, duplicate detection, source/line diagnostics, and conversion. Public models and value types continue to protect their own construction invariants. In particular, JournalPolicy requires every recommended rule to contain exactly one exact stable version, and Journal requires that version to exist in its own release map. The parser deliberately repeats both checks so invalid text retains precise source- and line-level diagnostics even though direct model construction is protected independently.
The service manager owns lifecycle and filesystem access
JournalServiceManagerImpl normalizes and stores the configured root. On start, it creates a fresh candidate JournalServiceImpl, validates the root, reads every runtime file in artifact-directory and Journal-version order, validates placement with the internal parse helper, and ingests each complete text. Only after all initial files succeed is the candidate published through journalService() and the manager considered started. A failed start discards the candidate, so no partially loaded service is exposed.
journalService() and refresh() require a successfully started manager. stop() ends the manager lifecycle and stops exposing its owned service without touching Journal files. A service reference already handed to a caller remains a safe read-only snapshot holder; the manager cannot revoke an ordinary Java reference, but it will not return that instance after stop and a later start builds a fresh instance.
Refresh reuses the active service and repeats chronological loading. Exact-text idempotency makes unchanged files harmless and newly appended successors become visible immediately. File-by-file refresh is retained because the issue requires atomic ingestion, not an all-directory transaction.
Filesystem placement is validated before mutation
The manager implementation accepts only direct artifact-coordinate directories and direct .journal runtime files, ignores .journal.example, rejects unexpected entries, and never creates or writes the root. It parses each file without mutation to compare metadata coordinate and JOURNAL_VERSION with directory and filename, then invokes internal ingestion. Parsing twice is deliberate: placement must fail before mutation, while the ingestion boundary must independently accept and validate a complete text rather than trusting filesystem preparation.
Existing format and model algorithms migrate unchanged in meaning
The line-oriented parser, canonical half-open version intervals, immutable collection copies, optional URI handling, free-text license handling, dependency/policy validation, exact timestamp rules, and CIDv1 canonical Base32/minimal varint checks keep their behavior while imports and packages move. This minimizes semantic regression during an API-only architectural migration.
Documentation uses Service terminology consistently
docs/Terminologi.md defines general Service and ServiceManager concepts before the Journal-specific entries. docs/Nenjim.md and docs/Nenjim-public.html describe the public query service, lifecycle manager, package boundary, start/refresh behavior, and future-work boundary without carrying forward the public JournalManager or filesystem-facing JournalService model.
Risks / Trade-offs
- [Moving every public type is a source- and binary-incompatible change] → The intermediate API is explicitly superseded; delete it completely and update all repository references in one change without aliases.
- [Initial loading may ingest several files before a later one fails] → It happens in an unexposed candidate service that is discarded on start failure.
- [A refresh is not atomic across the complete tree] → Each ingestion remains atomic and files are immutable; the failing path is reported and can be corrected before another refresh.
- [A previously returned service reference cannot be revoked on stop] → It exposes only immutable read queries and no lifecycle, refresh, filesystem, or mutation capability.
- [Strict parsing can reject hand-written content] → Diagnostics retain logical source, one-based line, and a concrete reason; version-controlled examples document accepted syntax.
- [Synchronized service methods serialize ingestion and queries] → Journal sets are expected to be small and refresh is explicit; correctness is preferred until profiling demonstrates contention.
Migration Plan
- Reconcile proposal, specs, design, and tasks with the updated authoritative issue.
- Move public composed models, value types, and the exception into their required packages; add the two central interfaces and remove the public parser/manager surface.
- Refactor the old chain manager into
JournalServiceImpland the old filesystem façade into lifecycle-awareJournalServiceManagerImpl; moveJournalTextParserand all helpers behind the implementation boundary. - Update imports and all three required documentation files, then verify the old package and public
JournalManagerare absent. - Strictly validate the active OpenSpec change, compile through Detag, parse every example, probe lifecycle/refresh/atomicity/idempotency/conflicts/forks/history, and inspect the final diff without syncing, archiving, committing, or pushing.
Rollback before publication is the normal source-control reversal of these uncommitted changes. There is no runtime data migration or persisted-state mutation in this change.