The persistent-twin restoration guide describes restart behavior. Restored shard groups now require complete, ordered, type-consistent descriptors with geometry. Lazy reads report missing slices/files explicitly and never allocate replacement values in a read-only scan.
This document is the central implementation guide for observation storage in k.LAB. It describes
how a quality receives a storage contract, how that contract becomes shards and primitive buffers,
how contextualizers access the buffers, and how data is finalized and persisted. The public API is
defined by Storage, StorageScan, StorageManager, and Data.ShardingStrategy; the main implementation is in
StorageManagerImpl, StorageImpl, and ScannerAdapters.
Storage belongs to a digital twin. A StorageManager owns one Storage per quality observation,
and a Storage owns time-indexed groups of non-overlapping Shard objects. A shard owns one native
primitive buffer. Contextualizers never receive the buffer itself: they access it sequentially
through a typed Storage.Scanner.
See DIGITALTWINS for the owning twin and scheduler lifecycle, KNOWLEDGE_GRAPH for descriptor transactions, and DISTRIBUTED_TWINS for the proposed connected-twin storage facade.
The three layers of the storage contract are:
| Layer | Responsibility |
|---|---|
Data.ShardingStrategy |
Declares primitive type, fill curve, split count, minimum split size, and maximum buffer size. |
Storage and Storage.Shard |
Own the observation data and describe its physical partitioning for an event. |
Storage.Scanner |
Provides sequential, primitive, optionally read-only access to one shard. |
The native strategy is part of Observation.ContextualizationData. Once storage exists, that
strategy describes the bytes in its shards and must not be silently reinterpreted. A setting change
affects storage attributed after the change; it does not migrate existing storage.
Storage.Type maps to scanner and buffer types as follows:
| Native type | Scanner | Primitive buffer width | Normal semantic use |
|---|---|---|---|
DOUBLE |
DoubleScanner |
8 bytes | Numeric quality, full precision |
FLOAT |
FloatScanner |
4 bytes | Numeric quality, reduced precision |
INTEGER |
IntScanner |
4 bytes | Integer state |
LONG |
LongScanner |
8 bytes | Long integer state |
KEYED |
KeyScanner backed by integer codes |
4 bytes | Worldview-bound concept state; zero is missing |
BOOLEAN |
BooleanScanner |
1 byte | Presence or verification state |
The generic Scanner deliberately has no boxed get or add operation. Typed scanners exist so
large contextualizations can execute without per-value allocation or boxing.
CompiledDataflow.harmonizeSharding() attributes storage before the first contextualization of a
new quality:
- Local, model, the producer’s own Java function/adapter declarations, then runtime strategies are merged; later concrete fields win. Numeric primitive types may override one another; incompatible semantic types fail.
- Runtime defaults derive type from semantics (numeric DOUBLE, categorization KEYED, verification BOOLEAN), curve from geometry, and split count from available processors. These concrete fields currently override earlier hints. Runtime sizes are neutral zeroes.
USE_SHORT_FLOAT_REPRESENTATION=trueselects FLOAT instead of DOUBLE.PARALLELIZE_OBSERVATIONS=false, or nondistributed space, unconditionally forces one split and clears both size hints for newly attributed storage. This remains a supported test mode.- The result is recorded on observation contextualization data. Storage creation requires it. Existing positive-ID observations keep their recorded native strategy.
CallDescriptors.shardingStrategy() contributes the producer’s Java declarations. Child strategies
are no longer propagated into the parent’s attribution. Dependency reads use ephemeral requests
against explicit output partitions, so existing inputs retain their own native layouts and types.
The runtime curve is D2_XY for regular 2D space, D3_XYZ for regular 3D space, otherwise
D1_LINEAR. Strategy getters return copies; incomplete native strategies are no longer silently
replaced by scan requests.
These declare layout preferences, not unit conversion or resampling. Registration mappings are:
| Field | @KlabFunction |
@ResourceAdapter |
@Exporter |
|---|---|---|---|
| Suggested partitions | split=-1 |
splits=1 |
No member; descriptor -1 |
| Traversal | fillCurve=UNSPECIFIED |
Same | Same |
| Soft minimum states per shard | minSizeForSplitting=0 |
Same | No member |
| Maximum states per shard | maxSize=0 |
Same | No member |
| Primitive type | Derived from artifact type: NUMBER/CONCEPT/BOOLEAN become DOUBLE/KEYED/BOOLEAN | Unspecified | Unspecified |
-1 means unspecified split preference, 1 means one partition, and positive counts are
suggestions to the geometry splitter. Sizes count states, not bytes; zero means unspecified.
Validation rejects zero or less than -1 splits, negative sizes, and a minimum above a positive
maximum. The adapter default of one partition deliberately remains more conservative than the
function default. Java member names remain unchanged. parallel and reentrant describe execution
and do not replace these fields.
ComponentRegistry validates function strategies and copies all four adapter fields, including
maxSize, into AdapterDescriptor. Its shardingStrategy() has no primitive type. A method's
@KlabFunction supplies its own defaults: it does not inherit class-level sharding fields.
Exporter registration records its curve; invocation applies it to a single read-only traversal
across the observation’s native shards. UNSPECIFIED selects the source’s native curve.
@KlabFunction(name = "sample", description = "Sample a quality",
type = Artifact.Type.NUMBER, split = 4,
fillCurve = Data.FillCurve.D2_YX, minSizeForSplitting = 1024, maxSize = 65536)The native splitter bypasses size hints for positive counts and uses them only to derive an
unspecified count. Planned reads additionally reject native shards exceeding a requested maximum.
Function/adapter layout declarations participate in new producer attribution with the precedence
above; they are preferences, not independent per-input traversal requirements. Every input in one
output task follows that task’s geometry and curve. Dynamic @Splits,
@SplitSize, and adapter @FillCurve methods mentioned in TODOs are not implemented contracts.
DataflowCompiler uses the shared ShardingAnnotations.parse() decoder. Canonical names are
lowercase; positional and named value syntax both work with the parser's normalized _p1 value.
Programmatic beans can use either representation, but supplying both is ambiguous and rejected.
| Annotation | Value | Meaning |
|---|---|---|
@type("float") |
Case-insensitive Storage enum | Primitive type preference |
@split(4) |
Integer: -1 or positive | Suggested partition count |
@maxsize(65536) |
Nonnegative long | Maximum states per shard |
@minsplitsize(1024) |
Nonnegative long | Soft minimum states per shard |
@fillcurve("D2_XInvY") |
Case-insensitive FillCurve enum | Traversal preference |
@split(value=4) and @split(4) are equivalent. Mixed-case enum members such as D2_XInvY
are preserved. Missing, ambiguous, malformed, or out-of-range arguments fail validation. Known
historical camel-case bean names are rejected with a lowercase-name diagnostic; the source grammar
already rejects them. UNSPECIFIED is a neutral declaration, not an executable traversal.
Concept annotations contribute first, model/dependency annotations override them, and explicit
observation definitions have highest precedence. override=true can affect ordinary concept/model
precedence; see ANNOTATIONS.md. Replacement is by name. Model annotations apply
only to the main output. Runtime strategy merging is a separate step with the precedence above.
Parsing/validation and planned conformant reads are implemented; full runtime binding is staged in
STORAGE_PLAN.md.
D1_LINEAR, D2_XY, D2_YX, and D2_XInvY have tested mappings in supported dimensions.
For shape [X,Y], D2_XY varies Y fastest, D2_YX varies X fastest, and D2_XInvY reverses Y
within each X block. D3_XYZ is row-major; D3_ZYX currently aliases it. Hilbert mapping throws.
FillCurve.map accepts int and Mapper.offset narrows to int: these are not proven large-index
mediation APIs. Planned reads reject UNSPECIFIED, D3_ZYX, and Hilbert curves. Planned conformant reads
use a separate checked long-index codec; the legacy helper implementations remain unchanged.
The hosting DigitalTwinImpl owns a StorageManagerImpl, created with its service context.
Focused and peer scopes must not be assumed to own independent storage. The manager indexes
storage by observation ID within the twin:
createStorage(observation)creates it lazily from the observation's attributed strategy;getStorage(observation)returns an existing entry or reconstructs one from contextualization data and knowledge-graph shard descriptors;finalizeStorage(temporaryId, finalId)rekeys storage when transaction commit assigns the persistent observation ID;clear()anddeleteStorage(...)close mapped buffers and remove the context's persisted files.
Exiting a client or the IDE disconnects local scope peers only. It must not call runtime
releaseContext or releaseSession: those operations close server scopes and can delete
context graphs and storage. Runtime persistence and timeout policies continue to govern
unattended twins; explicit deletion remains a separate operation.
Calling getStorage without an existing or reconstructable contract is an error. Storage creation
is not the place to infer semantics or consult current settings: attribution must already have
happened during dataflow compilation.
Initialization shard groups use timestamp zero. Temporal reads resolve exact event-keyed shards,
a covering revision, or an allowed committed baseline and reject missing state. Other moving
dimensions are not fully generalized. LocalTemporalWriteSet stages temporal data;
stageTemporal() exposes committed buffers/descriptors only through transaction callbacks and
cleans them on rollback. Ordinary AbstractExecutor still rejects temporal quality outputs.
First native allocation splits event-local geometry. Physical shard descriptors record geometry, index/count, timestamp, strategy, persistence policy, and native type. Splitting uses plain geometry rather than reconstructing service-local scales. No scanner mediation is implied by native splitting.
ShardStorage allocates an ojAlgo mapped BufferArray of the exact primitive width. Scratch buffer
files live in the storage manager workspace. They are closed when the manager closes; on Windows,
ojAlgo may retain mappings until garbage collection, so deterministic deletion of every scratch
file is not currently guaranteed.
Storage.scan(event, request, scannerClass, readOnly) is the access boundary. For a request whose
strategy equals the native strategy, it opens one scanner per native shard.
Write scanners reset the shard histogram only after pending persistence has been flushed; read-only
scanners reject add(...).
The requested scanner class is a real contract, not a hint. The storage layer either returns an instance assignable to that class, supplies a compatible primitive adapter, or fails explicitly. It must never return a scanner of another type and defer failure to reflection.
During a quality contextualization, AbstractExecutor previews native output partitions and plans
read-only dependency sessions against those exact partitions before opening writable output scanners.
It constructs one task per output shard and binds
component method parameters by the declared input/output name. Parameters may request:
- the
Observationitself; StorageScan.Viewfor consumer geometry, traversal and source metadata;- a physical
Storage.Shardonly when it describes the bound input without mediation; - generic
Storage.Scanner; - the matching typed scanner;
DoubleScannerover native float storage, orFloatScannerover native double storage.
The last two cases use ScannerAdapters. These are primitive, write-through decorators. A
float-to-double read uses primitive widening; a double-to-float write uses primitive narrowing and
therefore has exactly the precision loss requested by USE_SHORT_FLOAT_REPRESENTATION. The reverse
adapter supports a component explicitly requesting floats over double-native storage. The adapter
delegates shard(), size(), hasNext(), and nextLong() to the native scanner, so cursor,
geometry, histogram, mutability, and persistence identity are unchanged. It allocates one small
adapter per scanner, never one object per value and never a converted buffer.
No conversion is permitted between floating-point scanners and boolean or keyed scanners. Such a declaration is a component contract error and fails during binding with the native and requested types in the message.
Legacy get() and nextLong() both advance; peek() does not. Legacy scanner exhaustion checks
are not the strict planned-session contract below. Legacy scan() remains the native compatibility
entry point. Runtime dependency and exporter bindings use the planned path described under S3;
ScannerAdapters.mergeScanners() is no longer used to acquire observation-wide export inputs.
Component contextualizers should follow these rules:
- Declare
DoubleScannerwhen the algorithm operates in double precision. It remains compatible with short-float runtime storage through primitive mediation. - Declare
FloatScanneronly when float arithmetic is intentional. It can consume double-native storage, but reads narrow to float. - Declare generic
Scanneronly when the implementation does not call typed value methods or dispatches explicitly by scanner type; for planned scans, usescanner.view().valueType(). The physical shard type may differ from the consumer value type. - Mark input and output parameters accurately. Inputs receive read-only scanners; writing through them is always an error.
- Do not retain a scanner beyond the contextualizer invocation or share it with another shard task. A scanner owns mutable cursor state.
- Do not assume one shard. Parallelism settings and geometry can produce several independent invocations.
Components do not need recompilation when USE_SHORT_FLOAT_REPRESENTATION changes. Scanner
interfaces remain the API contract, and the runtime mediates the compatible floating-point
representation at invocation time.
After a shard task succeeds, Storage.finalizeRun(nativeScanner) snapshots the shard histogram and,
when the digital-twin persistence policy survives shutdown, schedules the native buffer for
persistence. Finalization deliberately receives the native output scanner rather than a component
adapter.
Persistence runs through a single maintenance executor. Before a writable scanner resets data or histogram state, pending persistence is flushed so an asynchronous writer cannot observe a new run's mutations. Each shard is written to a unique temporary file and moved into place atomically when supported by the filesystem.
The current file format contains a magic number, format version, native type, element width, value
count, and a big-endian primitive payload. Reads validate the header and exact file length. A
backward-compatible reader accepts the original headerless native-endian format when its length is
exactly valid. Knowledge-graph HAS_DATA relationships retain shard descriptors; the files retain
the primitive payload.
Initialization flushes payloads before descriptor publication, but graph and filesystem operations are not one atomic transaction. A graph failure may leave orphan files; rekeying IDs before graph commit is not proof of durable publication. Temporal writes use detached versioned buffers and transaction callbacks; their committed visibility does not provide general initialization rollback. See PERSISTENT_TWINS.md and DIGITALTWINS.md for restoration and ownership/closure behavior. Garbage collection of unreferenced files remains separate work.
Current managers use local observation IDs and local files. They cannot directly serve a union of independent twins with overlapping numeric IDs. A connected twin needs an origin-aware read-only provider for imported observations and ordinary local storage for its own outputs. Cache keys must include source twin/generation, observation, revision, time slice, shard, and representation.
Read source shards through authenticated, versioned data requests; do not send filesystem paths or primitive buffers as graph-change messages. Pin data versions to the source revisions recorded in output provenance. If the requested version has expired, fail explicitly rather than substitute current bytes. Geometry, units, fill curves, and keyed dictionaries still require supported mediation. These facilities are planned in DISTRIBUTED_TWINS, not implemented by the existing storage manager.
Different shard scanners may execute concurrently on virtual threads. A scanner and its cursor are
task-local. StorageImpl uses concurrent maps for shard groups and backing state, while shard
creation is synchronized to prevent duplicate allocation.
Scanner contract failures must be reported before reflective contextualizer invocation. Execution
retains the first concrete task failure as the executor cause; the generic Execution failed
exception is used only when no more specific cause was recorded. This distinction is important
because collective contextualization otherwise reports only the parent failure.
Storage.plan(StorageScan.Request<T>) validates metadata without opening buffers or resetting
histograms. Storage.open(plan) acquires a read session. Providers without these additive
capabilities reject the methods; the legacy scan(...) signature remains available.
var request = StorageScan.Request.nativeRead(
event, storage.getNativeShardingStrategy(), Storage.DoubleScanner.class);
var plan = storage.plan(request);
try (var session = storage.open(plan)) {
for (var scanner : session.scanners()) {
while (scanner.hasNext()) {
boolean valid = scanner.isValid();
double value = scanner.get();
// Consume the primitive value and validity.
}
}
}Requests snapshot mutable strategies and events into immutable Layout and Slice records.
Semantic definitions either match the native source or require a supported S4 conversion. An exact
native request keeps its native partitions and fast path; other layouts use the S2 conformant planner
described below. Empty
consumer partitions mean automatic partitioning, not an empty selection. Explicit partitions have
unique identities and must cover the same cells without overlap or gaps. Positive maximum size
limits consumer partitions, not the underlying physical buffers.
Sources must be finalized initialization shards or restored/committed data. Unsupported layouts,
semantics, coverage/sampling policies, writes, keyed types, and curves fail before buffer access.
Plans belong to their issuing storage instance. Description records contain immutable
source descriptors, event, semantic definitions, layouts, partitions, precision, budgets and
operation metadata. JSON round trips preserve structural equality; inconsistent descriptions and
unknown versions are rejected. A description is not an executable plan or permission to open data.
fingerprint() is SHA-256 over length-prefixed UTF-8 fields in record/list order. An opaque source
revision token incorporates local storage identity and write generation, so changing initialization
values changes the fingerprint. This is not a durable content hash or a cross-provider cache key.
Temporal source URNs/timestamps identify concrete committed shards. S4 persists binding intent on
AFFECTS and executed descriptions in Activity metadata; executable cursors, converters and buffers
are never serialized.
Opening rechecks generation, observation geometry, semantics and source descriptors. Stale plans fail and require explicit replanning. An open session leases its source, blocking initialization writes (including previously obtained writers), writer resets and storage closure. Concurrent sessions have independent task-local cursors. Close/cancel is idempotent and invalidates scanners, closes reader handles and releases the lease. Storage owns mapped buffers. Partial opening failure closes acquired handles and newly restored buffers before releasing the lease. Always use try-with-resources.
shard() identifies physical native storage when the view touches exactly one physical shard.
For a view spanning several shards it throws UnsupportedOperationException; use
view().sources() for the physical source descriptors and view().partition() for consumer
geometry. No synthetic or arbitrarily selected physical shard is returned. view() also describes
consumer type, semantics, slice and traversal. Views are never persisted as HAS_DATA shards. View histograms are
explicitly unavailable; native histograms are not converted-view statistics.
position() is the next offset, equal to size() at exhaustion. get() and nextLong() each
consume one position; peek(), isValid() and location() do not advance. Value/location access
after exhaustion throws NoSuchElementException; access after close throws IllegalStateException.
Writes fail without advancing. Location contains partition, curve, slice and long offset; public spatial-locator
construction and cell metrics remain future work (S2 uses internal grid-coordinate decoding). Legacy cursor behavior is unchanged.
Default LOSSLESS supports native DOUBLE/FLOAT/INTEGER/LONG/BOOLEAN and FLOAT to DOUBLE.
DOUBLE to FLOAT requires ALLOW_FLOAT_NARROWING, using Java IEEE narrowing including overflow to
infinity. Integers never pass through double; long precision is retained. Other casts, including
boolean arithmetic, are rejected. Floating NaN is invalid; infinity is valid. Integer/long/boolean
storage has no missing-value bitmap, so zero and false are valid.
Numeric cursors, validity checks and indexed reads use primitive accessors without per-value boxing,
streams or locator allocation. Metadata and sessions allocate per request/partition; locations
allocate only on explicit demand. Block reads fill caller-owned primitive arrays with one
reader-local lock per block. Independent shards do not share a read lock; scalar reads coordinate
with cancellation on their own reader. No full-dataset copy or per-cell index array is constructed.
Budget limits metadata partitions (default 65536) and values per block (default 65536), not total
dataset size. This does not guarantee that downstream consumer code is allocation-free.
Exporters and individual-value API text responses are required consumers of this same contract. Their shared binding is implemented in S3 below; ordinary semantic conversion remains S4 and contextual conversion S5. Text formatting follows location selection, validity and primitive mediation.
The local provider advertises CONFORMANT_READ. Its planned-read path supports one-to-many,
many-to-one and many-to-many layouts for all five native primitive types. Requested partitions
may cross physical shard boundaries or arrive in a different order. Values are located from
persisted shard geometry, not list position, equal-size assumptions, or transient parent metadata.
The original native strategy, buffers, descriptors and histograms remain unchanged. The legacy
scan(...) overload still rejects a different strategy; runtime/export/text consumers use planned sessions.
var viewStrategy = storage.getNativeShardingStrategy(); // a defensive copy
viewStrategy.setSuggestedSplits(1); // merge into one consumer traversal
viewStrategy.setCurve(Data.FillCurve.D2_YX);
var request = StorageScan.Request.nativeRead(event, viewStrategy, Storage.DoubleScanner.class);
try (var session = storage.open(storage.plan(request))) {
var scanner = session.scanners().getFirst();
// scanner.view() describes the merged traversal; scanner.shard() may be unavailable.
}Remapping requires nonempty, axis-aligned, rectangular regular spatial grids with one to three axes, a known matching CRS, equal cell resolution and aligned cell edges. Source partitions and consumer partitions must each be disjoint and cover the observation's rectangular spatial support exactly, including its outer boundaries. Missing bounds, masks/nonrectangular support, changed CRS/resolution, fractional cell offsets, coverage gaps/overlaps and unresolved distributed non-spatial dimensions fail during planning. Bounding-box overlap alone is never treated as conformance.
CRS identifiers are compared literally; this path does not infer equivalence or reproject.
Persisted native splits that omit a CRS inherit their owning observation's CRS. A spatial-only
consumer geometry inherits the selected slice's non-spatial context. When supplied explicitly,
non-spatial geometry must match the located source; the unchanged full observation geometry may
also describe coverage, while the request's Slice selects the event. It does not request a
second temporal resampling. Explicit partition geometry should use spatial-only or event-local
extents, not a different temporal interval.
Cell-size ratios allow relative error up to 1e-9. Cell edges must be within max(1e-8, eight ULPs) of an integer lattice coordinate; requests requiring a tolerance greater than 1e-4 cells are rejected as numerically ambiguous. This accommodates serialization round-off, not subcell shifts. All index products, volumes and endpoints use checked long arithmetic. Large spatial coordinates whose floating-point bounds cannot identify cells reliably are rejected. Local physical buffers retain their existing size limits; long total indexing does not remove those backend limits.
| Curve | Meaning in the planned reader |
|---|---|
D1_LINEAR |
Row-major mixed-radix order over the located spatial axes |
D2_XY |
Y varies fastest |
D2_YX |
X varies fastest |
D2_XInvY |
Y varies fastest in reverse within each X position |
D3_XYZ |
Z varies fastest, then Y, then X |
Dimensional curve mismatches are rejected. D3_ZYX remains rejected because legacy storage
currently aliases it to XYZ; S2 does not reinterpret existing bytes under a different meaning.
Hilbert and UNSPECIFIED remain unsupported. Traversal applies within each consumer partition;
a single globally ordered traversal requires one partition. Float widening/narrowing and validity
compose with remapping under the S1 precision rules, preserving exact integer/long/boolean data.
Exact native scalar/empty reads retain their fast path and strict cursor behavior; remapping an
empty or non-spatial grid is not required to fabricate partitions or source cells.
An empty partition list asks the planner to derive the requested layout. It repeatedly divides the
largest box along its longest axis, producing deterministic integer-cell partitions, including
uneven ones. A positive split count is a preference, bounded by available cells and the soft
minimum. With split=-1, a positive minimum suggests total/minimum partitions; otherwise one is
preferred. A positive maximum takes priority and may increase the count beyond the preference.
An explicit partition list defines the exact consumer order and boundaries; its sizes must obey
the maximum. The minimum does not reject unavoidable small partitions. PARALLELIZE_OBSERVATIONS
continues to control native output attribution; this readonly view operation does not launch tasks.
Version 1 descriptions remain native-identity descriptions. Version 2 permits different source
and consumer partitions and records INDEX_REMAP before the primitive value operation. JSON and
fingerprint round trips include the requested layout and explicit consumer order. The structural
record validates the version/pipeline; compiling a usable plan also validates geometric coverage.
Null optional layout types use a reserved null marker in fingerprints. WKT commas are preserved
when decoding geometry text, including strings produced by the existing geometry encoder.
The compiled plan contains a balanced bounding-volume directory and partition metadata, never an index per cell. Each mapped reader reuses a fixed coordinate array and its last source lookup. Block reads transfer contiguous native spans directly into caller-owned primitive arrays; other traversals use bounded primitive gathers. No converted buffer, boxed value or per-cell locator is created. Reader-local synchronization protects scratch coordinates/cancellation; source handles remain session-owned and leased as in S1.
Both source and consumer counts must fit Budget.maxPartitions. Source-to-consumer references
are capped at eight times that budget, preventing cross-cutting layouts from creating unbounded
metadata. Violating a budget fails before opening readers. The local metadata-only LRU cache has
at most 16 entries and admits only plans with at most 1024 combined source, target and reference
items each. Keys include source generation/descriptors, observation geometry/identity, semantics,
slice, partitions, type, precision and budgets. Large plans remain usable but bypass the cache.
Caches hold no payload buffers. Stale plans still fail the open-time checks.
Ordinary unit, range and pinned-currency conversions are supported by planned reads, as specified
below. Unit algebra and locator-dependent contextualization remain incomplete. AbstractMediator
can execute supplied dimension-factor operations, but no complete scanner compiler supplies the
required per-cell operations/locators. UnitImpl.aggregatedDimensions is metadata, not evidence of
working contextual conversion. ShapeImpl.getStandardizedArea() computes a metered area, not square
degrees; per-cell geographic area and calendar duration policies remain S5 work. KEYED support is
specified in the S7 section below.
The following remain explicit implementation boundaries:
- spatial mediation outside the S6 support matrix below, and mediation through the legacy scan overload;
- contextual units requiring cell area/volume/duration, nonlinear conversions, and integer-valued semantic conversion;
- temporary scanners from
StorageManager.getTemporaryScanner(...); - complete generalized indexing for moving dimensions other than time;
- cleanup of orphan persisted shards and fully deterministic mapped-file unmapping;
- applying partial-quality actuator coverage to execution storage.
Callers must not silently fall back to native scanners when one of these operations is requested. An explicit unsupported-operation failure protects buffer order, type safety, and data integrity.
Focused scanner mediation tests are in ScannerAdaptersTest. Storage width and durable buffer
round-trip tests are in StorageManagerImplTest; broader persistence and contextualization tests
exercise storage through the runtime module. The minimum focused verification is:
.\mvnw.cmd -pl klab.core.services -am "-Dtest=ScannerAdaptersTest,StorageManagerImplTest" "-Dsurefire.failIfNoSpecifiedTests=false" testChanges to attribution or executor binding should additionally compile and test
klab.services.runtime because that module owns sharding harmonization and component invocation.
Storage.writeLayout(event) previews the ordered native output partitions without allocating
buffers, creating scanners or resetting histograms. AbstractExecutor requests those explicit
partition IDs, geometries and sizes for every quality dependency, with the output curve. It plans
all inputs, opens their sessions, and validates reflected parameter requirements before opening
writable output scanners. It verifies the actual native output geometry against the preview.
Planning errors retain the binding name, source/output identity and original cause. Sessions close
after all tasks have completed, including executor cancellation and reflection failures.
Generic inputs retain their source type. Each typed function input independently requests its Java scanner type; the output type does not dictate input representation. Explicit FloatScanner bindings permit IEEE double-to-float narrowing; otherwise requests are lossless. Incompatible integer, boolean and floating-point bindings fail before output acquisition. Observation parameters retain their observation; StorageScan.View parameters describe the consumer. A physical Shard parameter is rejected before writing if the input spans shards or changes its geometry, curve or type. Native output Shard parameters and native output finalization remain unchanged. Native output View parameters have the output task geometry and no read-source descriptors.
Function and adapter declarations affect their own producer attribution, with the same runtime
overrides documented above. They do not force dependencies to be rewritten. Adapter resource input
payload transport remains the existing adapter API: this stage does not implement the pre-existing
AbstractResourceContextualizer.getInputData() placeholder or remote dependency transfer.
StorageReads constructs shared requests for consumers. ArgumentMatcher obtains one planned
read-only scanner per requested exporter scanner parameter using the exporter's declared curve and
Java type. A request merges all native shards into one traversal. LanguageService owns these
sessions through ScanResources; a returned InputStream takes ownership until explicit close,
EOF or read failure. A synchronous result, failed reflection call or unmatched overload releases
its sessions immediately. Caller-supplied scanners retain caller ownership.
An omitted event is allowed only when the observation has at most one temporal state, selecting
initialization. A multi-state export must supply storageEvent (event key), storageStart and
storageEnd (epoch milliseconds) in export parameters. All three are required together; they
construct an immutable TEMPORAL_TRANSITION Slice and use normal committed revision selection.
Unsupported coverage, curves and semantics fail without falling back to a native writable scanner.
ID-zero quality queries now bind to an existing positive-ID source in the authorized context.
Their metadata contains exactly one IM_QUERY_SOURCE_IDS entry. Requested geometry and Observable
remain on the detached result; contextualization metadata and native strategy beans are copied.
Storage lookup resolves the durable source through the context and never indexes ID zero.
createStorage() rejects detached queries. Different query views do not mutate the producer or
create graph observations, activities or data. A changed unsupported value mediator or
non-conformant geometry uses the S6 policy below; contextual conversion remains S5 work.
RuntimeService.readValue(StorageScan.Point, ContextScope) and the matching RuntimeClient call
provide point access. HTTP POST /api/v1/observation/value accepts a Point JSON object and returns
text/plain. Point contains a positive sourceId, an explicit slice, curve, optional full-view
geometry and semantics, and a zero-based offset. UNSPECIFIED curve selects native traversal.
The offset belongs to one complete consumer traversal, not a physical shard. Context authorization
and source lookup precede planning. The service checks bounds before opening the session, seeks
directly with Scanner.seek(long), checks validity, then formats the typed primitive. No preceding
cells are read and no full result is materialized. Seek permits size as the exhausted position;
value access there fails. Legacy writers need not implement seek.
Text is locale-independent Java primitive text: exact decimal longs and integers, true/false,
and Float/Double text with decimal points and optional exponent. Missing is the literal null;
zero and false remain valid. Text formatting allocates only at the API boundary. Point selection
uses the same provider plan and revision rules as dependency/export reads. Ordinary value conversions use the same S4 kernel as the other read routes.
TemporalWriteSet.writeLayout() previews output tasks. read(observation, request, PRIOR|CURRENT)
validates the event and conformant request and returns a closeable snapshot: PRIOR pins the causal
baseline; CURRENT includes sparse pending changes captured when opened. Later writes cannot change
an existing read. TemporalScalarExecution opens aligned input snapshots before acquiring output
writers. Ordinary contextualizers still reject non-initialization quality outputs without a write
set. Commit/rollback also closes outstanding temporal read sessions; finalization and publication
continue through the existing write-set protocol.
Temporal views identify transaction-local source descriptors, never persisted HAS_DATA shards, and cannot be passed as a physical Shard. Baseline reads and coordinate mapping use primitive kernels. Sparse changed offsets are sorted into primitive long arrays when a snapshot opens; lookups do not box their offsets. Existing transaction writers still retain boxed sparse change values. Neither this integration nor the native read path materializes a converted dataset.
core.inspector.scancheck(context, observation, curve, splits, samples) compares bounded samples
across partitioned, single-traversal and indexed-text views and verifies the producer contract stays
unchanged. Limits are 256 requested partitions and 64 samples per partition. It is an integration
check; independent codec oracles remain JUnit tests. celltext(context, observation, curve, offset)
exposes the same text reader to assertions. Both use initialization selection and propagate backend
errors. See TESTING.md and the executable storage testcase linked there.
Providers advertise VALUE_MEDIATION when they support the ordinary conversion contract.
A read request may change exactly one ordinary value mediator while retaining the same semantic
concept, observation contextualization and observer. StorageScan.Semantics.meaning carries that
non-value identity; observable retains the complete source/requested URNs for provenance. Legacy
five-argument semantic snapshots default meaning to observable, so callers using them must retain
the same observable identity. Contextual extent distributions must agree. Missing definitions,
changes to multiple mediators and non-affine units fail during planning. Spatial coverage follows S6 below.
ValueMediation compiles immutable coefficients before readers open. StorageScan.Description
version 3 contains a Conversion and the ordered pipeline INDEX_REMAP, VALUE_CONVERSION, <primitive adaptation>; index remapping may itself be identity. Versions 1 and 2 remain readable and retain
their previous fingerprint encoding. Portable descriptions are evidence, not executable handles:
providers must replan before reopening. Native source semantics, bytes and HAS_DATA descriptors do
not change. Converted histogram metadata is unavailable.
The initial conversion path accepts native FLOAT/DOUBLE and FLOAT/DOUBLE consumers. Arithmetic uses
primitive double values, with one final float cast where requested; double-to-float still requires
ALLOW_FLOAT_NARROWING. Native FLOAT results round to their requested float representation. Semantic
conversion of INTEGER/LONG/BOOLEAN fails explicitly rather than silently rounding an integer or
losing long precision. Their identity views remain exact. No converted dataset is materialized,
and mapping/coefficients/bounds never allocate per value. NaN passes through before arithmetic;
valid zero remains valid. peek() is repeatable and does not advance, and successful get() advances
once. A failed range read leaves the cursor on the offending value.
Unit conversion is destination-first: meters.convert(2, millimeters) and
UnitService.convert(2, meters, millimeters) both return 0.002. UnitService.conversion(destination, source) compiles a positive finite multiplier and finite offset. Compatibility now uses the unit
library; serialized UnitImpl definitions are reparsed without requiring transient converter data.
Scaled definitions do not contaminate the base-unit cache. Ordinary Celsius/Kelvin affine conversion
is supported. Depth-to-volume, calendar months/years and other location-dependent operations await S5.
Range snapshots encode lower:lowerExclusive:upper:upperExclusive. Conversion requires finite,
strictly increasing bounds, finite widths, and matching endpoint inclusion at both ends. It maps
linearly and monotonically; included endpoints map exactly. Unbounded/degenerate/reversed snapshots,
out-of-source-domain values and rounded results outside the target domain throw. No clipping,
extrapolation or implicit reversal is performed. NaN remains missing. The public numeric-range
upper-exclusion accessor now reports the correct endpoint.
CurrencyService.RateProvider.quote(source, target, valuation, operation) is the deterministic
provider boundary. Deployment chooses a provider explicitly; scanning never fetches a rate. A
CurrencyService.Rate pins source/target definitions, UTC Instant valuation, provider, provider
version, operation, positive finite factor, and IEEE_754_BINARY64 rounding. This is numerical
storage conversion, with no implicit rounding to monetary minor units. Missing or mismatched quotes
fail. Direct CurrencyImpl.convert preserves identity and otherwise throws instead of returning null.
Currency definitions are uppercase CODE or CODE@YYYY. EXCHANGE preserves the base-year suffix;
INFLATION preserves the currency code and changes the base year. A compound exchange-plus-inflation
request must be separated explicitly. No live provider or automatic currency selection is installed.
Pass the pinned quote in StorageScan.Request.rate or StorageScan.Point.rate. Consumer/query
observations carry the quote as JSON under StorageReads.RATE (im:storage-currency-rate). Ordinary
unit/range conversions require no rate. Dependency and temporal bindings propagate the consumer's
quote; query results use the query's quote and never inherit an unrelated source rate.
Each named dependency has its own AFFECTS relationship, even when two bindings share source and
consumer. The storageMediation property is a JSON StorageScan.Binding version 1 containing
binding ID (localName:rank), source/target semantics and compiled conversion, including any pinned
rate. Existing rank, prerequisite role, readState and semanticRelations properties remain intact.
Neo4j's existing CREATE relationship path and the transaction's directed pseudograph preserve
parallel edges. Binding metadata and consumer publication share one transaction. Process causal
and descriptive edge conventions remain unchanged; read mediation does not invent causal links.
Detached binding observations preserve the requested observable without mutating the source. In-process bindings follow source ID/URN/coverage through initialization and durable ID assignment; transport snapshots contain the resolved identity, never the transient source reference. Occurrence snapshots retain that observable and quote; restart resolves the durable source separately and replans against its current descriptors. An ID-zero query used by a durable dependency resolves its positive source ID before creating an edge. Standalone query/export/point reads create no graph observation, activity, relationship or converted storage.
Every dependency execution records a JSON StorageReads.Evidence version 1 in its current Activity
metadata under a unique im:storage-read:<UUID> key. It includes binding identity and the complete
plan description: source revision, event/slice, source descriptors, operations, type/precision and
conversion reproducibility inputs. Later executions append new evidence; they do not rewrite old
activities. Rollback removes staged evidence. Temporal descriptions identify transaction-local
snapshots, while committed storage descriptions pin the selected source generation and shards.
Session.description() exposes provider evidence (null is permitted for older providers).
Contextualizers, temporal scalar dependencies, ID-zero quality queries, exports and indexed cell
text all use the same conversion compiler. HTTP exports may supply storageObservable with the
requested observable definition and storageCurrencyRate with a pinned-rate JSON value, in addition
to the existing temporal selection parameters. Export argument matching receives a detached view;
stream ownership and cleanup remain unchanged. StorageScan.Point carries requested semantic
snapshots and an optional quote for the text API.
core.inspector.unitcheck(context, observation, unit, factor, offset) compares bounded samples
against an independently supplied expected affine mapping and also checks partitioned/export/text
agreement. The staging storage testcase checks elevation meters-to-millimeters with factor 1000.
Full-stack execution remains the user-run acceptance gate before S5; see TESTING.md.
ACCEPT_LOSSY_MEDIATIONS is a runtime Boolean setting, default true. Quality reuse in
ResolutionCompiler validates source/target spatial support before accepting an existing quality.
When false, mismatched extents fail with ACCEPT_LOSSY_MEDIATIONS=false, followed by the geometric
reason (for example, cell resolution differs or requested coverage differs). Partition/curve
changes that remain exactly conformant are still allowed. This gate does not change the separate
float-narrowing policy or migrate native storage.
StorageReads selects NEAREST for numeric/boolean data and MAJORITY for type of categories,
with MISSING_OUTSIDE for dependency, query, export and point/text reads. Explicit output partitions define target support independently of the input extent. Planning
first attempts S2 conformant mapping; on success it retains the exact description and reader.
Otherwise the runtime gate and requested sampling policy determine whether spatial mediation is
allowed. An explicit Sampling.EXACT, including Request.nativeRead, never opts into resampling.
All dependency plans validate before writable output acquisition. Cache keys include the gate;
opening a previously issued spatial plan also checks the current setting. Resolver-side runtime clients refresh their settings snapshot before this check. Already open sessions
retain their pinned contract. Failed requests do not change native values, histograms or descriptors.
| Policy | Types | Meaning |
|---|---|---|
| NEAREST (runtime default) | FLOAT, DOUBLE, INTEGER, LONG, BOOLEAN | Transform target cell centers; select containing half-open source cells, preserving exact native primitives. |
| INTERPOLATE | FLOAT, DOUBLE | Bilinear interpolation of source cell-center values; all positive-weight contributors must exist and be valid. No edge clamping or hole filling. |
| CONSERVATIVE | FLOAT, DOUBLE densities/intensive quantities | Overlap-area weighted mean normalized by the geometrically covered target area. |
| MAJORITY (categorical default) | KEYED | Same-CRS overlap-area majority, canonical-definition tie breaking, strict missing-contributor propagation. |
| CONSERVATIVE_TOTAL | FLOAT, DOUBLE cell totals | Sum source values times overlap area / source cell area. Full-domain coarsening/refinement conserves totals within floating-point tolerance. |
Conservative policies are explicit scientific choices; the runtime does not infer totals versus
densities from unit strings. For categorical information such as type of, the default policy is
area-weighted majority (MAJORITY), with the S7 dictionary contract below. Exact weighted ties
choose the lexicographically smallest canonical semantic definition. A missing positive-area
contributor makes the result missing; uncovered area is ignored when some coverage exists. Zero
overlap is missing. Category codes are never averaged or summed. Numeric interpolation and
conservative policies reject INTEGER/LONG/BOOLEAN rather than treating them as category dictionaries.
MISSING_OUTSIDE emits invalid values where a sample lacks source support. EXACT rejects target
footprints outside the source rectangle during metadata planning (1e-8 source-cell round-off
tolerance). It does not guarantee native validity. NaN contributors propagate missingness; neighbors
are not renormalized to fill missing source cells. Partial conservative densities normalize over
actual geometric overlap; partial totals include only overlap contributions. Zero overlap is missing.
Absent/corrupt files remain errors, never missing spatial values. Floating reads expose missingness
as NaN and text as null. Integer/long/boolean callers must check isValid() before get()/peek():
invalid reads throw, preserving zero/false as ordinary values. Seek/nextLong can skip invalid cells.
Spatial scanners expose target metadata in view() and never claim a physical shard() identity.
Spatial mediation supports regular, axis-aligned rectangular 2D grids. Source shards must cover the native rectangle without gaps/overlaps. Explicit target partitions must cover one target rectangle without gaps/overlaps, retaining identifiers and order. Derived partitions obey the existing partition/size budgets and checked long curve codecs. The native scale layer currently normalizes a single-cell observation to an irregular shape; such observations cannot enter this regular-grid resampling path (an explicitly gridded one-cell target partition is supported).
| Geometry change | Support |
|---|---|
| Same-CRS resolution/offset changes, coarse/fine grids, subset, partial/no overlap | All spatial policies |
| EPSG:4326 to/from EPSG:3857 | Nearest and bilinear through strict GeoTools transforms, XY/longitude-first axes |
| Conservative EPSG:4326 overlap | Spherical area proportional to longitude width times difference of sine(latitude); the common radius cancels |
| Conservative matching projected CRS | Planar overlap; coordinate-plane conservation, not ellipsoidal ground-area accuracy |
| Rotated/sheared grids, masks, arbitrary reprojection, antimeridian wrapping, polar/singular domains | Explicitly unsupported |
| Changed non-spatial extents or temporal resampling | Explicitly unsupported; selected event slices can be spatially mediated |
Geographic spatial bounds must remain within [-180,180] longitude and strictly inside
+/-85.0511287798066 latitude. Web Mercator bounds must be strictly inside +/-20037508.342789244.
This restricted domain permits monotonic transformed-window pruning. Other matching CRSs use planar
coordinates. Ambiguous cells (width within 32 ULPs of bounds or counts beyond binary64's reliable
integer range) fail rather than guessing. Unknown spatial grid parameters fail during remapping; concrete cell counts and bounds take precedence over the sgrid resolution hint.
Version 4 descriptions record SPATIAL_RESAMPLE, optional VALUE_CONVERSION, then the primitive
operation. Source/target geometry encodings carry bounds, shapes and traversal; Description.spatial() and
View.spatial() retain effective source/target CRS identifiers even when shard encodings omit them.
Executor requests supply the output observation CRS for such partitions. Policy, source
revision, event and budgets are explicit fields. Version 4 defines the XY convention, strict
transform, half-open boundaries, spherical/planar metrics, tolerance and binary64 arithmetic above.
These versioned policies are included in fingerprints and execution evidence. Versions 1-3 retain
their previous fingerprints. Spatial arithmetic uses double accumulators even over FLOAT storage;
ordinary unit conversion follows sampling and final narrowing happens once. Conservative totals
permit multiplicative ordinary conversion. Affine total and bounded-range/resampling combinations
fail. Contextual precipitation depth-to-volume conversion and calendar metrics remain S5 work.
Plans retain partition metadata and a source spatial directory, never per-cell maps. Transformed target windows prune source links. Each numeric cursor has one source-value window capped at min(4096, Budget.blockValues), plus fixed primitive scratch coordinates. Local opening still validates/leases the native shard group, including files outside the target window. Remote block fetching and lazy shard acquisition remain S8 work. No converted buffers or HAS_DATA views persist.
LocalTemporalWriteSet.read applies the same policies to pinned PRIOR/CURRENT snapshots and closes
sessions on commit/rollback. Pre-earthquake elevation can be read over a changed event extent.
Individual event effects use TemporalWriteSet.write over the event geometry. Explicit writes
scatter to native cells by nearest cell center; cells outside the event remain unchanged. A
single-cell event grid is supported. Writable value conversion is rejected, and the same rectangular
grid, CRS and mediation-setting limits apply. Closing a view stages changes in the transaction;
commit publishes them atomically and rollback preserves historical data. Ordinary temporal output
contextualizers still require a transaction-owned write set.
Storage.KeyScanner<Concept> now reads and writes concept values over four-byte signed integer
payloads. Code 0 is missing (null); positive codes are dictionary entries. Negative and unknown
codes are corruption errors, never missing values. An ordinary IntScanner cannot expose keyed
payloads as measurements. A key scanner exposes an immutable DataKey snapshot through key();
Storage.getKey() also returns an immutable snapshot. Codes can be inspected through a dictionary's
reverseLookup and lookup, which keeps their semantic binding explicit. DataKey.size() and its
indexed label/concept lists include the reserved missing entry at index 0, so every exposed index
round-trips without an offset. The persisted entry catalog contains only codes 1..N.
For type of X, the reasoner resolves each previously unseen definition and checks that it is a
concrete, non-generic, non-bottom subclass of X. X itself is rejected. Only validated canonical
concepts are cached. Repeated insertion uses a hot identity cache and a canonical-definition map;
there is no per-value reasoning, remote call, or serialization. Invalid definitions are cached too.
The observation owns one synchronized allocator shared by its shards and concurrent producers.
Codes are append-only and cannot collide. Their numerical order has no rank meaning: this type-of
dictionary is explicitly unordered. Read cursors decode to cached canonical concept objects.
The first successful typed insertion (including missing) permanently commits the entire root context
to a WorldviewCommitment: worldview identity plus a sorted map of resolvable ontology URNs to
SHA-256 source hashes. The reasoner advertises this content snapshot in its capabilities; a local
revision counter alone is insufficient across restarts. The commitment is recorded on the root
Neo4j context, exposed in DigitalTwin.Configuration, and copied to worldview.json beside storage.
Reopening a committed context requires an available, consistent reasoner with exactly the same
commitment, even if no keyed quality has been requested yet. Same worldview ID with changed content
is rejected. New plans/scanners and previously unseen semantic values recheck the environment;
existing read sessions keep their pinned contract. A failed/rolled-back activity may leave the
context committed, but cannot publish unbound semantic payloads. Cross-worldview translation is
rejected; no label-based equivalence is inferred.
Each finalized shard references an immutable key-<SHA256>.json dictionary and a categorical
histogram containing that dictionary hash, positive-code counts, and a separate missing count.
The dictionary records its schema version, root commitment, type constraint, ordered code-to-entry
catalog, canonical semantic definitions, labels, and authority references where supplied by the
reasoner. It never stores Java concept objects or reasoner-local IDs. Snapshot files are flushed and
renamed before a shard can be published; existing dictionary versions are never overwritten with
changed meanings. Graph shard descriptors retain dictionary and histogram references. Restoration
checks hashes, compatible prefix assignments, root commitment, and payload code/count consistency.
Legacy keyed payloads without this evidence fail explicitly. Dictionary entries are reconstructed
without per-cell reasoning; concept resolution/validation is lazy and cached. Offline semantic reads
fail closed when the committed worldview cannot be verified.
Dictionary translations match canonical definitions within the same worldview/type, with bounded
translation-table caching by both dictionary fingerprints. A shared allocator avoids shard remapping
inside an observation. Storage.getCategoryHistograms() merges per-time shard counts through these
semantic translations and returns the dictionary with each summary. Numeric histograms never operate
on category codes. Validated immutable dictionary files are cached (at most 16 snapshots of up to
4096 entries); a fresh manager revalidates persisted bytes.
Scan description version 5 includes the dictionary and worldview in execution evidence and fingerprints, and supports native, conformant and spatial keyed views. Versions 1–4 retain their previous fingerprints. Spatial majority uses the S6 planar/spherical overlap metrics and same-CRS support matrix. Scratch is proportional to dictionary cardinality, not cell count, and is reused between samples. Explicit nearest sampling is also available, including supported reprojection; numeric interpolation and conservative sums/means reject keyed values. Temporal PRIOR/CURRENT reads, commit and rollback use the same dictionaries and snapshots. Temporal localization preserves raw shard geometry and inherited CRS while replacing only time.
Dictionary/root/payload publication follows the existing staged filesystem/graph protocol; it is not a claim of atomic transactions across those systems. Orphan semantic snapshots are harmless and remain part of the final recovery/hardening work. Contextualized units remain S5; the existing S6 geometry limits and final full-stack acceptance checks remain explicit.