75: Implement the Nenjim Journal model, text parser, manager and filesystem service
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-19
|
||||
@@ -0,0 +1,82 @@
|
||||
## 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 `JournalService` the filesystem-independent read-only API and `JournalServiceManager` the 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 public `JournalManager`, 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`: only `JournalService` and `JournalServiceManager`.
|
||||
- `.model`: `Journal`, `JournalMetadata`, `JournalPolicy`, `ArtifactRelease`, `ArtifactDependency`, and `PolicyRule`.
|
||||
- `.valuetypes`: `ArtifactCoordinate`, `JournalVersion`, `ContentDigest`, `DependencyTarget`, `VersionExpression`, and `VersionExpressionSet`.
|
||||
- `.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
|
||||
|
||||
1. Reconcile proposal, specs, design, and tasks with the updated authoritative issue.
|
||||
2. 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.
|
||||
3. Refactor the old chain manager into `JournalServiceImpl` and the old filesystem façade into lifecycle-aware `JournalServiceManagerImpl`; move `JournalTextParser` and all helpers behind the implementation boundary.
|
||||
4. Update imports and all three required documentation files, then verify the old package and public `JournalManager` are absent.
|
||||
5. 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.
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
Nenjim needs a stable, executable Journal foundation before Context and classloader work can continue. The Journal API must also follow Nenjim's service architecture so ordinary consumers receive a filesystem-independent read-only service while lifecycle and data-source administration remain with a separate service manager.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add an immutable public Journal domain model for one artifact per Journal, exact releases, content identity, dependencies, policy, and normalized version expressions.
|
||||
- Add strict format-version-1 parsing and whole-document validation, including quoted metadata/license text, timestamps, digests, dependency constraints, policy, and precise source/line diagnostics.
|
||||
- Add a filesystem-independent, read-only `JournalService` query interface and a reference `JournalServiceImpl` that maintains independent immutable revision chains behind an internal ingestion operation.
|
||||
- Add a lifecycle and administrative `JournalServiceManager` interface and filesystem-facing `JournalServiceManagerImpl` that owns the data root, performs initial chronological loading and explicit refresh, and exposes the service only after successful start.
|
||||
- Separate the two central service interfaces, composed models, value types, exceptions, and reference implementation into the mandated public and implementation packages.
|
||||
- **BREAKING**: Remove the intermediate `com.r35157.nenjim.journal` API, its public `JournalManager`, and the older `hubd.journal`, `hubd.module`, `JournalManagerImpl`, and `JournalId` skeletons without compatibility aliases.
|
||||
- Move the three example Journals into artifact-coordinate revision directories and rewrite them as valid format-version-1 snapshots.
|
||||
- Rewrite Nenjim documentation and terminology around artifact-scoped immutable snapshots, chain history, dependency/policy semantics, content/license metadata, and the general `Service`/`ServiceManager` responsibility split.
|
||||
- Keep Contexts, resolution, release selection, classloading, retrieval, signatures, publishing, synchronization, watching, UI/CLI, URI interpretation, and license filtering outside this change.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `nenjim-journal-format`: Defines the immutable Journal model, format-version-1 grammar, version-expression semantics, and complete structural and semantic validation contract.
|
||||
- `nenjim-journal-management`: Defines the read-only query service, lifecycle manager, internal atomic Journal-chain ingestion, package boundary, and filesystem refresh/layout behavior.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
None.
|
||||
|
||||
## Impact
|
||||
|
||||
- The only types directly under `com.r35157.nenjim.service.journal` are `JournalService` and `JournalServiceManager`; public models, value types, and exceptions use their specified subpackages.
|
||||
- Reference parsing, mutable ingestion, chain state, and filesystem loading live under `com.r35157.nenjim.service.journal.impl.ref` and do not leak through public signatures.
|
||||
- Existing obsolete Journal/module source files and references are removed or rewritten.
|
||||
- Journal examples under `data/nenjim/journals` and `docs/Nenjim.md`, `docs/Terminologi.md`, and `docs/Nenjim-public.html` change substantially.
|
||||
- No new external dependency or runtime integration with Context/classloading is introduced.
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
## Purpose
|
||||
|
||||
Defines the observable format-version-1 contract and immutable domain meaning of a complete, artifact-scoped Nenjim Journal snapshot.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Artifact-scoped complete snapshots
|
||||
The Journal format SHALL describe exactly one artifact identified by the coordinate `<GROUP>-<MODULE>-<ARTIFACT>`, SHALL contain no `JOURNAL_ID` or artifact `TYPE`, and SHALL represent a complete immutable worldview rather than a delta from its predecessor.
|
||||
|
||||
#### Scenario: Valid artifact identity
|
||||
- **WHEN** metadata contains non-empty `GROUP=com_r35157_nenjim`, `MODULE=hubd`, and `ARTIFACT=api`
|
||||
- **THEN** the parsed coordinate is `com_r35157_nenjim-hubd-api`
|
||||
|
||||
#### Scenario: Obsolete identity fields
|
||||
- **WHEN** a Journal contains `JOURNAL_ID` or `TYPE`
|
||||
- **THEN** parsing fails as an unknown or misplaced field without silently ignoring it
|
||||
|
||||
#### Scenario: No-op semantic revision
|
||||
- **WHEN** a later valid snapshot differs from its predecessor only by `JOURNAL_VERSION` and `PREVIOUS_JOURNAL_DIGEST`
|
||||
- **THEN** the snapshot remains valid because revisions are complete worldviews and need not change their semantic content
|
||||
|
||||
### Requirement: Strict top-level structure and comments
|
||||
The parser SHALL require the logical order `FORMAT_VERSION=1`, `JOURNAL_VERSION`, optional `PREVIOUS_JOURNAL_DIGEST`, `[METADATA]`, optional `[POLICY]`, and one or more `[RELEASE <version>]` sections. Blank lines and full-line or trailing `#` comments SHALL be accepted, except that a `#` inside a quoted value is data.
|
||||
|
||||
#### Scenario: Comments before format entry
|
||||
- **WHEN** blank lines and full-line comments precede `FORMAT_VERSION=1`
|
||||
- **THEN** the format entry is treated as the first actual configuration entry and parsing proceeds
|
||||
|
||||
#### Scenario: Hash inside quoted text
|
||||
- **WHEN** `DESCRIPTION='Project #1' # trailing comment` is parsed
|
||||
- **THEN** the decoded description is `Project #1` and the trailing comment is discarded
|
||||
|
||||
#### Scenario: Unsupported format
|
||||
- **WHEN** the first actual entry is `FORMAT_VERSION=2`
|
||||
- **THEN** parsing fails explicitly because the format version is unsupported
|
||||
|
||||
#### Scenario: Misordered or duplicate structure
|
||||
- **WHEN** a required entry or section is missing, duplicated, malformed, unknown, or placed outside its defined order
|
||||
- **THEN** parsing fails and no unknown content is ignored
|
||||
|
||||
### Requirement: Journal and publication timestamps
|
||||
`JOURNAL_VERSION` and `PUBLISHED_AT` SHALL each use a strict UTC `uuuuMMddHHmmssSSS'Z'` timestamp with millisecond precision and SHALL retain distinct revision-time and release-time meanings.
|
||||
|
||||
#### Scenario: Valid timestamp
|
||||
- **WHEN** `20260819121530842Z` is supplied as a Journal or publication timestamp
|
||||
- **THEN** it is represented as the corresponding immutable UTC instant/value
|
||||
|
||||
#### Scenario: Invalid timestamp
|
||||
- **WHEN** a timestamp has missing milliseconds, a non-UTC suffix, an impossible calendar value, or trailing characters
|
||||
- **THEN** parsing fails
|
||||
|
||||
### Requirement: Quoted metadata values
|
||||
`[METADATA]` SHALL contain exactly one non-empty `GROUP`, `MODULE`, `ARTIFACT`, and single-quoted `DESCRIPTION`, and MAY contain at most one single-quoted `INFORMATION_URI`. The parser SHALL decode `\'` and `\\`, reject other malformed escaping and empty quoted values, and expose information URI as `Optional<URI>` only when the decoded value is a syntactically valid non-empty absolute URI, without restricting its scheme.
|
||||
|
||||
#### Scenario: Valid quoted metadata
|
||||
- **WHEN** metadata contains `DESCRIPTION='Nenjim\'s reference hub'` and `INFORMATION_URI='ftp://ftp.domain.com/components/123.txt'`
|
||||
- **THEN** the model exposes description `Nenjim's reference hub` and the exact absolute FTP URI
|
||||
|
||||
#### Scenario: Optional information URI absent
|
||||
- **WHEN** valid metadata omits `INFORMATION_URI`
|
||||
- **THEN** the model exposes an empty optional URI
|
||||
|
||||
#### Scenario: Scheme-independent URI
|
||||
- **WHEN** a syntactically valid absolute `https:`, `ftp:`, or `ipfs:` URI is supplied
|
||||
- **THEN** parsing accepts it without fetching, opening, or scheme-specific interpretation
|
||||
|
||||
#### Scenario: Invalid metadata text or URI
|
||||
- **WHEN** a required quoted value is unquoted, empty, unterminated, badly escaped, or the information URI is relative or syntactically invalid
|
||||
- **THEN** parsing fails at that field
|
||||
|
||||
### Requirement: Exact artifact releases and content
|
||||
Every release section SHALL identify a unique exact `major.minor.patch` stable semantic version and SHALL contain exactly one non-empty quoted `LICENSE`, exactly one valid `PUBLISHED_AT`, one or more supported unique `CONTENT_DIGEST` values, and exactly one non-negative long `CONTENT_SIZE`.
|
||||
|
||||
#### Scenario: Valid release content
|
||||
- **WHEN** release `1.0.3` contains decoded license text, a valid publication timestamp, a SHA-256 digest, an optional CIDv1 digest, and a non-negative content size
|
||||
- **THEN** the immutable release model exposes all values without normalizing or interpreting the license
|
||||
|
||||
#### Scenario: Invalid release version
|
||||
- **WHEN** a release heading uses `1`, `1.0`, `1.0.0-beta`, or `1.0.0+build.42`
|
||||
- **THEN** parsing fails rather than normalizing it to another exact release
|
||||
|
||||
#### Scenario: Duplicate release
|
||||
- **WHEN** two release sections identify the same exact version
|
||||
- **THEN** parsing fails
|
||||
|
||||
#### Scenario: Invalid license
|
||||
- **WHEN** a release omits `LICENSE`, repeats it, or supplies an unquoted, malformed, or empty value
|
||||
- **THEN** parsing fails
|
||||
|
||||
#### Scenario: Digest validation
|
||||
- **WHEN** content digests use valid `sha256:<64 lowercase hexadecimal characters>` and/or `cid1:<CIDv1 base32 text>` values whose payload is the exact canonical unpadded lowercase Base32 encoding of a valid CIDv1 envelope, with at most one digest of each scheme
|
||||
- **THEN** all supplied content identities remain inspectable in the release model
|
||||
|
||||
#### Scenario: Canonical CIDv1 envelope
|
||||
- **WHEN** a CIDv1 content digest is supplied
|
||||
- **THEN** its version, multicodec, multihash code, and digest-length fields use their shortest unsigned varint encoding within the supported `long` range, and the declared digest length matches the payload
|
||||
|
||||
#### Scenario: Non-canonical CIDv1
|
||||
- **WHEN** a CIDv1 spelling has non-zero unused Base32 padding bits, a non-minimal varint, or a varint that overflows the supported `long` range in any envelope field
|
||||
- **THEN** parsing fails even if a permissive decoder could produce the same CID bytes or a truncated numeric value
|
||||
|
||||
#### Scenario: Invalid content identity
|
||||
- **WHEN** no content digest exists, a digest scheme is unknown, a digest syntax is invalid, a scheme repeats, or content size is negative or outside the long range
|
||||
- **THEN** parsing fails
|
||||
|
||||
### Requirement: Normalized version expressions
|
||||
The model SHALL distinguish exact artifact versions from partial version boundaries, normalize semantically equivalent selector syntax, support membership and set containment, and reject unsupported or empty expressions.
|
||||
|
||||
#### Scenario: Equivalent exact forms
|
||||
- **WHEN** `1.0.3` and `[1.0.3]` are parsed
|
||||
- **THEN** both produce the same exact version-expression representation
|
||||
|
||||
#### Scenario: Minor series
|
||||
- **WHEN** `[1.0]` is parsed
|
||||
- **THEN** it contains every stable `1.0.x` release from `1.0.0` through the release immediately before `1.1.0` and is not normalized to exact `1.0.0`
|
||||
|
||||
#### Scenario: Inclusive and exclusive ranges
|
||||
- **WHEN** `[1.0.3-->1.0.7]` and `[1.0.3-->1.0.7)` are parsed
|
||||
- **THEN** the first contains both endpoints and the second excludes `1.0.7`
|
||||
|
||||
#### Scenario: Partial range boundaries
|
||||
- **WHEN** `[1.0-->1.2]` or `[1.0-->1.2)` is parsed
|
||||
- **THEN** the inclusive form contains the entire `1.2.x` series and the exclusive form excludes the entire `1.2.x` series
|
||||
|
||||
#### Scenario: Explicit cross-major range
|
||||
- **WHEN** `[1.0-->3.0)` is parsed
|
||||
- **THEN** it contains stable versions in major lines 1 and 2 but none in major line 3
|
||||
|
||||
#### Scenario: Open upper range
|
||||
- **WHEN** `[1.0-->`, `(1.0-->`, or `[1.0.3-->` is parsed
|
||||
- **THEN** its implicit exclusive upper boundary is `2.0.0`, while its lower boundary respectively starts at `1.0.0`, `1.1.0`, or `1.0.3`
|
||||
|
||||
#### Scenario: Expression union
|
||||
- **WHEN** `[1.0.0-->1.0.3], [1.0.7], 1.1.1` is parsed for a repeatable/list-valued rule
|
||||
- **THEN** the model represents the normalized union and accepts whitespace around commas and `-->`
|
||||
|
||||
#### Scenario: Invalid expression
|
||||
- **WHEN** an expression uses a major-only boundary, a bare incomplete exact version, prerelease/build syntax, mathematical comparison syntax, whitespace inside a numeric version, an empty list element, reversed boundaries, or an empty range
|
||||
- **THEN** parsing fails
|
||||
|
||||
### Requirement: Nenjim dependencies
|
||||
Each artifact release SHALL model each `nenjim:<artifact-coordinate>` dependency target with exactly one `DEPENDS_ON` base expression set, zero or more described `EXCLUDES` entries, and zero or more described `PREFERRED` entries. Unknown schemes SHALL fail explicitly, exclusions and preferences SHALL require a corresponding base dependency, and the complete preferred set SHALL be contained by `DEPENDS_ON - EXCLUDES`.
|
||||
|
||||
#### Scenario: Valid dependency relationship
|
||||
- **WHEN** a release depends on `nenjim:com_r35157_nenjim-hubd-api=[1.0-->`, excludes `1.0.4` with a description, and prefers `1.0.3`
|
||||
- **THEN** the model preserves the target, base constraint, and individual described exclusion/preference rules
|
||||
|
||||
#### Scenario: Repeated exclusion and preference rules
|
||||
- **WHEN** multiple `EXCLUDES` or `PREFERRED` lines refer to one declared target
|
||||
- **THEN** their expression sets form unions while every line and optional description remains individually inspectable
|
||||
|
||||
#### Scenario: Duplicate base dependency
|
||||
- **WHEN** the same dependency target has two `DEPENDS_ON` entries
|
||||
- **THEN** parsing fails
|
||||
|
||||
#### Scenario: Orphan relationship rule
|
||||
- **WHEN** an `EXCLUDES` or `PREFERRED` target has no corresponding `DEPENDS_ON`
|
||||
- **THEN** parsing fails
|
||||
|
||||
#### Scenario: Invalid preferred subset
|
||||
- **WHEN** any preferred version lies outside the base dependency set or inside an exclusion
|
||||
- **THEN** parsing fails because preference cannot make an invalid version valid
|
||||
|
||||
#### Scenario: Unsupported dependency scheme
|
||||
- **WHEN** a dependency uses `maven:` or another unsupported scheme in format version 1
|
||||
- **THEN** parsing fails clearly rather than ignoring or resolving it
|
||||
|
||||
### Requirement: Artifact policy
|
||||
An optional `[POLICY]` section SHALL preserve individual `BLACKLIST`, `DISCOURAGED`, and `RECOMMENDED` rules and descriptions. Blacklist SHALL be a hard constraint, discouraged and recommended SHALL be hints, overlaps SHALL be valid, and each recommended rule SHALL identify exactly one exact release present in the same snapshot. These recommendation invariants SHALL be enforced both while parsing and when the public model types are constructed directly without the parser.
|
||||
|
||||
#### Scenario: Valid overlapping policy
|
||||
- **WHEN** a broad discouraged range overlaps a blacklisted exact release and an existing exact release is recommended
|
||||
- **THEN** all individual rules and descriptions remain inspectable and the blacklist has the documented stronger meaning without flattening the rules
|
||||
|
||||
#### Scenario: Repeated policy entries
|
||||
- **WHEN** policy contains several blacklist, discouraged, or recommended lines
|
||||
- **THEN** all lines are retained as immutable individual rules
|
||||
|
||||
#### Scenario: Invalid recommendation
|
||||
- **WHEN** `RECOMMENDED` contains a non-exact range or identifies a release absent from the snapshot
|
||||
- **THEN** parsing fails
|
||||
|
||||
#### Scenario: Direct policy construction rejects a non-exact recommendation
|
||||
- **WHEN** a caller constructs `JournalPolicy` directly with a recommended rule containing a range, a minor series, multiple versions, or any other non-exact `VersionExpressionSet`
|
||||
- **THEN** construction fails because one recommended rule must identify exactly one exact `major.minor.patch` version
|
||||
|
||||
#### Scenario: Direct Journal construction rejects an absent recommendation
|
||||
- **WHEN** a caller constructs `Journal` directly with an exact recommended version that is absent from that Journal's release map
|
||||
- **THEN** construction fails because recommendations refer only to releases in the same snapshot
|
||||
|
||||
#### Scenario: Direct construction preserves valid policy overlaps
|
||||
- **WHEN** a caller constructs a Journal with an existing exact release recommended while that version also belongs to discouraged or blacklist rules
|
||||
- **THEN** construction succeeds and all individual rules remain inspectable because overlaps are valid
|
||||
|
||||
#### Scenario: Optional description decoding
|
||||
- **WHEN** a repeatable policy or dependency rule ends with `:'Known \'issue\' on C:\\tmp'`
|
||||
- **THEN** its decoded non-empty description is `Known 'issue' on C:\tmp`
|
||||
|
||||
#### Scenario: Invalid optional description
|
||||
- **WHEN** an explicitly supplied description is empty, unquoted, unterminated, or has malformed escaping
|
||||
- **THEN** parsing fails
|
||||
|
||||
### Requirement: Complete diagnostics and immutable results
|
||||
Parsing SHALL validate the entire document before it is accepted, SHALL report the known logical source, one-based line number, and reason for every parse failure, and SHALL return immutable defensively copied objects and collections without using `null` for optional values.
|
||||
|
||||
#### Scenario: Source-aware failure
|
||||
- **WHEN** line 12 of logical source `example.journal` contains an invalid field
|
||||
- **THEN** the parse exception identifies `example.journal`, line 12, and the validation reason
|
||||
|
||||
#### Scenario: Immutable model
|
||||
- **WHEN** callers obtain metadata, releases, policies, dependencies, digests, or expression collections
|
||||
- **THEN** callers cannot mutate the Journal snapshot through those returned values
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
## Purpose
|
||||
|
||||
Defines the public Journal query service, its lifecycle manager, internal atomic chain ingestion, historical query behavior, and explicit filesystem loading of immutable Journal revisions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Strict Journal API package boundary
|
||||
The public Journal API SHALL begin at `com.r35157.nenjim.service.journal`, where the only directly contained Java types SHALL be the public interfaces `JournalService` and `JournalServiceManager`. Public composed domain objects SHALL be under `.model`, public value types SHALL be under `.valuetypes`, public Journal-specific exceptions SHALL be under `.exception`, and the reference implementation and its parser, ingestion, chain-state, and filesystem helpers SHALL be under `.impl.ref` or its subpackages without leaking implementation types through public signatures.
|
||||
|
||||
#### Scenario: Public package contents
|
||||
- **WHEN** the compiled Journal API packages are inspected
|
||||
- **THEN** only `JournalService` and `JournalServiceManager` are directly in `com.r35157.nenjim.service.journal`, while composed models, value types, and exceptions are in their respective public subpackages
|
||||
|
||||
#### Scenario: Reference implementation boundary
|
||||
- **WHEN** the public service and model signatures are inspected
|
||||
- **THEN** they expose no parser, mutable ingestion API, filesystem loader, chain store, or reference-implementation type
|
||||
|
||||
#### Scenario: Superseded API removed
|
||||
- **WHEN** the source and compiled Journal API are inspected
|
||||
- **THEN** no public `JournalManager`, deprecated alias, forwarding wrapper, or compatibility type remains under `com.r35157.nenjim.journal` or another superseded package
|
||||
|
||||
### Requirement: Filesystem-independent read-only Journal service
|
||||
`JournalService` SHALL be the ordinary application-facing, filesystem-independent, read-only API. It SHALL list known artifact coordinates and chronological revisions; obtain latest, exact, and newest-at-or-before revisions; obtain an exact artifact release from a selected revision; return immutable results without `null`; and expose no lifecycle, path, refresh, parser, configuration, or mutation operation.
|
||||
|
||||
#### Scenario: Chronological and latest queries
|
||||
- **WHEN** an artifact has several accepted revisions
|
||||
- **THEN** the service lists all complete snapshots in ascending Journal-version order and returns the chain head as latest
|
||||
|
||||
#### Scenario: Exact and as-of historical queries
|
||||
- **WHEN** a caller requests an exact revision or the newest revision at or before a Journal version
|
||||
- **THEN** the service returns the complete worldview from that chain position rather than filtering a newer snapshot by release publication time
|
||||
|
||||
#### Scenario: Release query from selected worldview
|
||||
- **WHEN** a caller requests an exact artifact version from a selected revision
|
||||
- **THEN** the release, including license, content, dependencies, and policy context in that snapshot, is returned only if that revision knows it
|
||||
|
||||
#### Scenario: Missing query result
|
||||
- **WHEN** a coordinate, revision, as-of revision, or artifact release is unknown
|
||||
- **THEN** the service returns an empty optional or immutable empty collection rather than `null`
|
||||
|
||||
#### Scenario: Read-only boundary
|
||||
- **WHEN** an ordinary consumer receives `JournalService`
|
||||
- **THEN** the interface provides no start, stop, filesystem root, refresh, parsing, update, ingestion, or other mutation operation
|
||||
|
||||
### Requirement: Internal atomic text ingestion and chain identity
|
||||
The reference service implementation SHALL keep mutable ingestion non-public and filesystem-independent. One ingestion SHALL reject null, empty, or blank input; parse and validate the complete text; compute SHA-256 over its exact UTF-8 bytes including whitespace and line endings; validate its chain relationship; and atomically expose the immutable Journal through `JournalService`. A failed ingestion SHALL leave every previously accepted revision unchanged for that operation.
|
||||
|
||||
#### Scenario: Valid internal ingestion
|
||||
- **WHEN** a complete valid genesis Journal is supplied internally
|
||||
- **THEN** it becomes immediately queryable through the read-only service under its metadata-derived coordinate
|
||||
|
||||
#### Scenario: Exact text digest
|
||||
- **WHEN** a Journal text is accepted
|
||||
- **THEN** its revision digest is calculated from exactly the supplied UTF-8 text, including whitespace and line endings
|
||||
|
||||
#### Scenario: Failed ingestion is atomic
|
||||
- **WHEN** an ingestion is null, empty, blank, structurally invalid, semantically invalid, or chain-invalid
|
||||
- **THEN** the operation fails and every previously accepted chain and revision remains unchanged
|
||||
|
||||
### Requirement: Linear per-artifact Journal chains
|
||||
Each artifact coordinate SHALL have exactly one genesis revision and one linear head. A non-genesis revision SHALL reference the current predecessor's exact SHA-256 digest and have a strictly greater Journal version. Missing, cross-artifact, stale-head, duplicate-genesis, or fork relationships SHALL fail.
|
||||
|
||||
#### Scenario: Valid successor
|
||||
- **WHEN** a later revision for the same coordinate references the current head's exact text digest and has a strictly greater Journal version
|
||||
- **THEN** it becomes the new head while the predecessor remains historically queryable
|
||||
|
||||
#### Scenario: Missing or cross-artifact predecessor
|
||||
- **WHEN** a non-genesis revision references an unknown digest or a digest owned by another artifact coordinate
|
||||
- **THEN** ingestion fails without changing either chain
|
||||
|
||||
#### Scenario: Fork or stale predecessor
|
||||
- **WHEN** a revision references a known non-head predecessor or attempts another genesis for an existing coordinate
|
||||
- **THEN** ingestion fails rather than creating multiple heads
|
||||
|
||||
#### Scenario: Non-increasing revision time
|
||||
- **WHEN** a successor Journal version is equal to or earlier than its predecessor's version
|
||||
- **THEN** ingestion fails
|
||||
|
||||
### Requirement: Idempotency and conflicts
|
||||
Supplying the same coordinate, Journal version, and exact complete text more than once SHALL be idempotent. Supplying different text for an existing coordinate and Journal version SHALL fail as a conflict even if the semantic model would be equal.
|
||||
|
||||
#### Scenario: Repeated identical text
|
||||
- **WHEN** an already accepted complete text is supplied again
|
||||
- **THEN** the existing immutable revision is returned without changing the chain
|
||||
|
||||
#### Scenario: Conflicting text
|
||||
- **WHEN** different text is supplied for an already stored coordinate and Journal version
|
||||
- **THEN** ingestion reports a conflict and does not mutate state
|
||||
|
||||
### Requirement: Journal service lifecycle manager
|
||||
`JournalServiceManager` SHALL be the lifecycle and administrative API. It SHALL start and stop the service, report the normalized configured Journal data root, perform explicit refresh, and expose the corresponding `JournalService` only after successful initialization. It SHALL not duplicate the Journal query API.
|
||||
|
||||
#### Scenario: Successful initial start
|
||||
- **WHEN** the configured root and all runtime Journal data are valid
|
||||
- **THEN** start performs the complete initial chronological load before the manager reports success and exposes its read-only service
|
||||
|
||||
#### Scenario: Failed initial start
|
||||
- **WHEN** the root, placement, filename, text, or chain is invalid during initial loading
|
||||
- **THEN** start fails clearly and the manager does not report a started service or expose a partially loaded service
|
||||
|
||||
#### Scenario: Service access follows lifecycle
|
||||
- **WHEN** service access or refresh is requested before successful start or after stop
|
||||
- **THEN** the manager rejects the operation as unavailable in the current lifecycle state
|
||||
|
||||
#### Scenario: Stop is non-destructive
|
||||
- **WHEN** the manager is stopped
|
||||
- **THEN** service-owned maintenance stops cleanly without creating, rewriting, or deleting Journal files
|
||||
|
||||
#### Scenario: Administrative API does not duplicate queries
|
||||
- **WHEN** `JournalServiceManager` is inspected
|
||||
- **THEN** artifact and revision query operations are available only through its associated `JournalService`
|
||||
|
||||
### Requirement: Filesystem-backed lifecycle loading
|
||||
The reference service manager SHALL own a configured existing readable root, discover runtime files only at `<root>/<artifact-coordinate>/<journal-version>.journal`, ignore `.journal.example`, validate directory and filename placement against parsed content, sort each artifact's files chronologically, read complete UTF-8 text, and supply it to the internal ingestion operation. It SHALL not create, overwrite, delete, publish, download, synchronize, or watch files.
|
||||
|
||||
#### Scenario: Valid filesystem load
|
||||
- **WHEN** an artifact directory contains chronologically named valid genesis and successor `.journal` files whose content identity matches their placement
|
||||
- **THEN** initial start or refresh ingests both complete UTF-8 texts in chronological order and exposes their chain through the service
|
||||
|
||||
#### Scenario: Example files are ignored
|
||||
- **WHEN** a directory contains `<journal-version>.journal.example`
|
||||
- **THEN** loading does not ingest it as a runtime Journal
|
||||
|
||||
#### Scenario: Invalid directory placement
|
||||
- **WHEN** a runtime Journal's metadata-derived coordinate differs from its parent directory name
|
||||
- **THEN** loading fails clearly
|
||||
|
||||
#### Scenario: Invalid filename placement
|
||||
- **WHEN** a runtime filename does not use the strict timestamp syntax or differs from the parsed `JOURNAL_VERSION`
|
||||
- **THEN** loading fails clearly
|
||||
|
||||
#### Scenario: Invalid root
|
||||
- **WHEN** the configured Journal root is missing, not a directory, or unreadable
|
||||
- **THEN** start or refresh fails clearly without creating it
|
||||
|
||||
#### Scenario: Unexpected filesystem entry
|
||||
- **WHEN** the configured root contains a runtime entry outside the artifact-directory/revision-file structure
|
||||
- **THEN** loading fails rather than silently treating it as a Journal
|
||||
|
||||
### Requirement: Explicit idempotent refresh
|
||||
The service manager SHALL support explicit refresh. Repeating refresh for unchanged files SHALL be idempotent, and a later refresh SHALL discover valid newly added successors while preserving existing historical revisions.
|
||||
|
||||
#### Scenario: Unchanged refresh
|
||||
- **WHEN** refresh runs twice without filesystem changes
|
||||
- **THEN** the second run leaves all chains unchanged and succeeds through ingestion idempotency
|
||||
|
||||
#### Scenario: New revision discovered
|
||||
- **WHEN** a valid successor file is added after an earlier successful refresh
|
||||
- **THEN** the next refresh ingests it and the read-only service exposes it as the new head
|
||||
|
||||
### Requirement: Version-controlled examples and migration
|
||||
Version-controlled examples SHALL use `<journal-version>.journal.example` beneath an artifact-coordinate directory and collectively demonstrate quoted description, a scheme-independent information URI, per-release license, dependencies, exclusions, preferences, policy, exact/range syntax, and optional descriptions. Any multi-revision example chain SHALL use the predecessor file's real exact-text SHA-256 digest.
|
||||
|
||||
#### Scenario: Example placement
|
||||
- **WHEN** repository Journal examples are inspected
|
||||
- **THEN** each is placed under `data/nenjim/journals/<artifact-coordinate>/` with a strict Journal-version filename ending in `.journal.example`
|
||||
|
||||
#### Scenario: Example chain integrity
|
||||
- **WHEN** examples include a successor revision
|
||||
- **THEN** its predecessor digest equals the SHA-256 of the predecessor example file's exact UTF-8 bytes
|
||||
@@ -0,0 +1,51 @@
|
||||
## 1. Immutable Journal Domain
|
||||
|
||||
- [x] 1.1 Implement artifact coordinates, Journal versions, content digests, metadata, releases, dependencies, policy rules, and complete Journal snapshots with immutable construction invariants.
|
||||
- [x] 1.2 Implement canonical version expressions and immutable expression-set membership, union, subtraction, and subset operations.
|
||||
|
||||
## 2. Format Version 1 Parsing
|
||||
|
||||
- [x] 2.1 Add the strict internal reference line parser and public Journal exception with source/line diagnostics, comment handling, ordering, duplicates, quoted text, URI, timestamp, release, and digest validation.
|
||||
- [x] 2.2 Add dependency and policy parsing plus cross-field validation for declared targets, preferred subsets, exact recommendations, and preserved descriptions.
|
||||
|
||||
## 3. Read-only Journal Service State
|
||||
|
||||
- [x] 3.1 Implement internal atomic filesystem-independent ingestion, exact-text SHA-256 identity, idempotency/conflicts, and linear per-artifact chain validation.
|
||||
- [x] 3.2 Implement immutable coordinate, chronological, latest, exact, as-of, and selected-release queries for the read-only service.
|
||||
|
||||
## 4. Lifecycle and Filesystem Service Manager
|
||||
|
||||
- [x] 4.1 Implement strict root/directory/filename discovery, UTF-8 chronological loading, placement validation, ignored example files, and explicit idempotent refresh in the service manager.
|
||||
- [x] 4.2 Separate ordinary read-only Journal queries from lifecycle, data-root, initial-loading, and refresh administration.
|
||||
|
||||
## 5. Migration and Documentation
|
||||
|
||||
- [x] 5.1 Move and rewrite the three artifact-scoped example Journals into revision directories with valid format-version-1 content covering the documented features.
|
||||
- [x] 5.2 Rewrite `docs/Nenjim.md`, `docs/Terminologi.md`, and `docs/Nenjim-public.html` for the implemented Journal model, `Service`/`ServiceManager` split, and clearly separated future functionality.
|
||||
- [x] 5.3 Remove the obsolete Journal/module/JournalId skeleton files, dummy behavior, imports, and dead references.
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- [x] 6.1 Run strict OpenSpec validation and verify the change remains active, unsynced, and unarchived.
|
||||
- [x] 6.2 Compile the complete project through Gradle/Detag and run focused temporary probes covering example parsing, chain behavior, version expressions, invalid input diagnostics, and filesystem refresh.
|
||||
- [x] 6.3 Review the final diff for issue scope, out-of-scope exclusions, immutability, and absence of generated-source edits.
|
||||
|
||||
## 7. CIDv1 Canonicality Follow-up
|
||||
|
||||
- [x] 7.1 Require exact canonical unpadded lowercase Base32 round-tripping and minimal, overflow-safe unsigned varints for every CIDv1 envelope field.
|
||||
- [x] 7.2 Remove the obsolete `JournalId` tag from the versioned and active Detag configuration.
|
||||
- [x] 7.3 Run temporary CID canonicality probes, strict OpenSpec validation, `compileJava`, `assemble`, and `git diff --check`, then verify that the change remains active, unsynced, and unarchived.
|
||||
|
||||
## 8. Service Architecture Migration
|
||||
|
||||
- [x] 8.1 Reconcile proposal, design, and delta specs with the updated authoritative issue and remove the old `JournalManager`/filesystem-`JournalService` responsibility split.
|
||||
- [x] 8.2 Move the public composed models, value types, and exception into their mandated packages; add only the two central service interfaces directly under `com.r35157.nenjim.service.journal`; remove the superseded package without aliases.
|
||||
- [x] 8.3 Implement `JournalServiceImpl`, lifecycle-aware `JournalServiceManagerImpl`, and internal `JournalTextParser` with non-public ingestion, complete initial loading, refresh, chain atomicity, and existing query behavior.
|
||||
- [x] 8.4 Update the three required documents and terminology for the general `Service`/`ServiceManager` concepts, Journal responsibilities, package boundary, and current/future scope.
|
||||
- [x] 8.5 Run all required build, example, package-boundary, lifecycle, ingestion, chain/query, OpenSpec, generated-source, and diff verifications while leaving the change active, unsynced, and unarchived.
|
||||
|
||||
## 9. Direct Recommendation Construction Invariants
|
||||
|
||||
- [x] 9.1 Specify that direct `JournalPolicy` construction rejects non-exact recommendations and direct `Journal` construction rejects recommendations for releases absent from the same snapshot.
|
||||
- [x] 9.2 Enforce the recommendation invariants in the public model constructors while retaining parser diagnostics and valid policy overlaps.
|
||||
- [x] 9.3 Run temporary direct-construction and example-parsing probes, strict OpenSpec validation, `compileJava`, `assemble`, `git diff --check`, and generated-source checks while leaving the change active, unsynced, and unarchived.
|
||||
Reference in New Issue
Block a user