Store kinds
The layout contract every stored series obeys, the thirteen kinds declared today, and the gate that derives the live set from the code rather than from this table.
Everything Vike Trader keeps on disk as history lives under a store root in a kind= path segment,
and that segment is a contract between two ends that never meet: the live recorder, the property
and chain recorders, the backfill collectors and the journal materializer on one side; tick replay,
the reads a data node serves, and the Data Manager and Studio inventory views on the other.
crates/vike-data/src/store_kind.rs's STORE_KINDS is where that contract is written down — one
row per stored kind, thirteen of them today, each with its own page here. That producer list is
illustrative and not a census: a producer is anything holding a HistStore, so the header declines
to claim any list can be complete.
Until that file existed, no site named the contract, and its header records what that cost.
crates/vike-data/src/coverage.rs's TICK_KINDS was missing depth, so a whole kind was invisible
to the cross-kind coverage report while its rows sat on disk. Five Data Manager verbs built their
path from SeriesId::symbol, which is empty for a grouped series, so each silently operated on a
series that does not exist — delete was a no-op that returned Ok(()).
What a row declares
One StoreKind names the domain type a decoded row becomes, every Arrow column today's writer
emits in schema order with the series_codec! flavor that picks its type and nullability, the
Parquet schema-metadata key the kind stamps when it stamps one, how the leaf is partitioned,
whether a grouped form exists, what venue and symbol actually mean for that kind, and the
commit-key shapes its known producers use.
Two column flavors bite a consumer. A _null column is always present and may hold SQL NULL; an
_add column may be absent entirely from a part sealed before it was added, because the _add
flavors read through an absent-tolerant column reader. A kind's columns are what today's writer
emits, so nothing may assume an _add column is present in an old part.
Partitioning, stated once
Parts are date=-partitioned identically for every kind — <series leaf>/date=YYYY-MM-DD/ — so no
per-kind page repeats it. What the partition guarantees is that a part never spans a UTC day:
crates/vike-data/src/datafusion_hist/manifest.rs's seal_into_manifest groups a batch's rows by
day and seals one part per group.
A date partition holds many parts, not one
A writer seals one part per commit, so the live recorder — committing per buffer flush — produces
one every few seconds, and compaction merges them into part-c… outputs alongside. Measured
read-only on the live production tape:
kind=book/venue=polymarket/group=btc-updown-5m/date=2026-08-08 held 103 parquet files and
date=2026-08-07 held 130. manifest.rs's next_part_name is the authority — a per-date index,
one above the highest still live, a counter precisely because there are many. A consumer that
builds a reader for one-file-per-day mishandles the real 130-file case.
Below the shared kind=…/venue=… prefix the leaf has two spellings. The per-symbol form is
symbol=…, and bar alone adds interval=… beneath it so a 1m and a 1d series of one
instrument never interleave — the table's only Partition::SymbolInterval row. The grouped form is
kind=…/venue=…/group=…, written by DataFusionHist's group_dir, where one part holds many
symbols told apart by the row-level symbol_col column. The two are siblings under one parent and
a store can hold both at once, so anything building or parsing a path must branch on group
first — series_dir_of is the one function that does it correctly, and the five verbs that did
not are the incident above. In the same vein, SeriesId::symbol is empty for a grouped series,
SeriesId::group carries the name, and SeriesId::label answers for both.
Declare today's reality; pin the contradictions
The table is step one of the workspace's two-step playbook for a capability table: it merged
byte-identical in behaviour and writes down what the code does today, including where two sites
disagree. Each disagreement is named in a row's notes with both sides cited, and a test asserts
it still exists, so whoever resolves one is told to update the table rather than leave a stale
warning behind. Two rows carry such a note today. The depth row's is labelled a pinned
contradiction: crates/vike-backfill/src/regroup.rs's GROUPABLE_KINDS lists
depth as having a grouped form and the store has none. The
funding row's is labelled a pinned name collision — two unrelated series are called funding:
kind=funding is the account's realized payments, while the market funding-rate history is a
bar series under the reserved interval=funding label, carrying the rate on Bar::funding with
OHLCV and volume all zero.
The gate's pinned section holds four tests, not two. The other pair pins no row disagreement:
both_depth_defaults_refuse and both_catalog_defaults_refuse hold append_depth/scan_depth,
and list_series/inventory, to an Err rather than a fabricated empty read.
This is deliberately not schema versioning. crates/vike-data/src/store_kind.rs's header is
emphatic about the difference and says versioning must stay deferred: a row records a metadata key a kind already writes, and introduces no version
negotiation, no migration and no new on-disk byte.
The gate
crates/vike-data/tests/store_kind_gate.rs derives the live kind set from the code and fails until
a row exists — three independent derivations, because each is blind to a different mistake: the
kind= segments the store actually writes, read off the first argument of every
series_dir/ticks_dir/group_dir call in the backend module tree; the strings each compaction
dispatch accepts, each dispatch checked on its own; and the fn append_* verbs on the
always-compiled HistStore trait, which reddens first when a kind is added the ordinary way. Shape
is gated separately, by parsing the series_codec! invocations and comparing each row's columns and
schema-metadata key against the field list they were read from. Every check reads source text on
purpose — the codecs sit behind the hist-datafusion feature while the test runs in the default
lane, and regroup.rs is in a crate vike-data cannot depend on.
Two residuals are declared, and the declaration is the point. Dataflow is not followed, and that is
measured: a SeriesId with kind: "oracle" handed to the declared series_dir_of site builds a
kind=oracle directory with no compaction arm and no append verb while the gate stays at 25 passed,
0 failed. And the scope is this crate's src — another crate holding a store root can build a
directory with a plain join, and a store written by a newer build already holds kinds no source
here mentions.
The thirteen kinds
Four line up in the cross-kind coverage report — TICK_KINDS is trade, quote, book and
depth. Three kind ids carry underscores on disk (exec_fill, exec_order, perp_metrics) where
their page slugs use hyphens. Most seam verbs state their own default on the trait method; the one
pair worth carrying here is depth, where both append_depth and scan_depth return an Err
in a store that does not implement them, because a store with no depth lane and a store with no
depth rows must not read identically to a caller.
bar · quote · trade · book · depth · properties · equity · exec-fill · exec-order · funding · chain · cohort · perp-metrics
The seam itself, and where a store root resolves when nobody says, are on The history store; its live mirror is the live data seam. What fills a store is data providers and store maintenance; what reads it back is tick replay and the Data Manager's coverage view.