RFC-0015: chronicle.manifest Dataset Sidecar¶
| Field | Value |
|---|---|
| Status | Proposed |
| Format name | chronicle.manifest (TOML) |
| Date | 2026-09-07 (updated 2026-09-11) |
| Component | persisting-pchronicle, pchronicle CLI, Warehouse Datasets UI |
| Implements | crates/persisting-pchronicle/src/store/chronicle_manifest.rs |
| Related | RFC-0014 Compact JSONL · RFC-0013 path Directory · RFC-0001 Storyline |
Summary¶
chronicle.manifest is a pChronicle-owned TOML sidecar placed at a Dataset
node root. It enables cheap discovery and stores common aggregate statistics so
pchronicle ls and the Web Datasets page can classify a directory and show
type / trajectory-count previews without opening Lance or scanning every
record.
This RFC uses RFC 2119 MUST, MUST NOT, SHOULD, and MAY.
Motivation¶
Large local datasets (for example a compact-jsonl Lance directory with many
rows and _offload/ objects) currently force discovery to Dataset::open and
force the Datasets UI to reconstruct folders from run summaries. With a
warehouse that mounts several such roots, five-second catalog refresh and tree
polling keep the serve process on high CPU even when the operator only browses
one directory level.
Lance's internal _versions/*.manifest is an MVCC control plane owned by Lance.
pChronicle MUST NOT encode application discovery or UI statistics there.
pChronicle already uses application control files for other layouts (CURRENT
for Storyline, _manifest.json for Events). chronicle.manifest extends that
pattern to a uniform, nestable Dataset node descriptor.
Goals and non-goals¶
Goals:
- Define a stable on-disk file name, TOML schema, and nesting rules.
- Let discovery classify Dataset nodes by reading a small TOML file.
- Persist aggregate stats used by
ls/ Web Datasets previews (type and trajectory count). - Support nested Dataset trees by automatically scanning child directories for
chronicle.manifest. - Split list from open:
list(path)is shell-like: one immediate level of children. It MUST NOT flatten nested sources.open(path)builds a Snapshot. A leaf Dataset is one source. A plain directory MAY be opened as a virtual dataset that unions all nested Dataset leaves and peripheral JSON under that path.- When the sidecar is missing, still classify Datasets via
CURRENT/ compact-jsonl markers.listMUST NOT recursively list an entire object-store prefix just to classify.openof a directory MAY recurse under the existingmax_entries/max_filesbounds.
Non-goals (v1):
- Extending Lance protobuf manifests.
- Replacing SQL or detailed run/record listing with the sidecar.
- Hand-maintained explicit
childrenlists in parent manifests. - Rewriting Storyline
CURRENTor Events_manifest.jsoninto this format (those remain authoritative for their layouts; optional future alignment).
Terminology¶
- Dataset node: a directory that is either a physical source root (leaf) or a directory that aggregates nested Dataset nodes (branch).
- Leaf: a node whose
formatidentifies a physical store (v1:compact-jsonl/v1orstoryline/v1). - Branch: a node that exists to nest children; it has no physical
format. - Directory: a path with no leaf Dataset marker. It is a navigational
folder for
list. It MAY still beopen'd as a virtual dataset. This is not RFC-0013 path Directory (name → path + ACL). - list: one-level listing of a path. CLI
pchronicle lsand the Web Datasets page MUST share this operation. - open: freeze a path as a Snapshot (
query/find/stats/ serve mount / Open in Runs). - Virtual dataset: the Snapshot produced by opening a Directory: nested Dataset leaves plus JSON / JSONL / NDJSON that are not inside a leaf.
- Fingerprint: a string that binds
[stats]to one physical revision so readers can detect staleness.
File location and name¶
- The file name MUST be exactly
chronicle.manifest. - The file MUST live at the Dataset node root (sibling to Lance
data/, StorylineCURRENT, etc., when those exist). - Encoding MUST be UTF-8 TOML.
Nesting and discovery¶
list and open share classification. They MUST NOT share result shape.
A path is a leaf Dataset when any of these holds, in this order:
chronicle.manifestwithkind = "leaf"CURRENT(Storyline)- compact-jsonl Lance (
pchronicle.format = compact-jsonl/v1, or a leaf sidecar withformat = "compact-jsonl/v1")
Otherwise the path is a Directory (including chronicle.manifest
kind = "branch").
Symlinks MUST be ignored. Existing max_entries / max_files limits still
apply to traversal. import / sync use a separate recursive JSON scan
and are not bound by list.
list(path) — shell-like one level¶
pchronicle ls PATH and Web Datasets MUST list immediate children of
PATH, like a shell ls. They MUST NOT flatten nested sources into the
current listing.
Each child is one of:
| Child | list kind |
Preview |
|---|---|---|
| Leaf Dataset directory | dataset | format plus [stats].record_count / failed_count when the sidecar has them |
| Directory | directory | name only; MUST NOT deep-scan for a preview |
.json / .jsonl / .ndjson at this level |
file | name; format MAY be unknown until open |
list of a leaf Dataset MUST NOT list Lance interiors (data/,
_versions/, generations/, _offload/, and similar). The listing is that
one Dataset plus its preview. Further drill-down is open / Runs, not
another ls.
pchronicle ls --sources MUST open(path) and print the Snapshot source
table (the virtual-dataset members). Default ls stays one-level children.
--sources MUST NOT change list itself.
Parents MUST NOT require an explicit children list in a branch manifest.
list of a branch or Directory inspects immediate children only.
A child with a Dataset marker is listed as a dataset; other directories
are listed as directories; loose JSON at this level is listed as a file.
open(path) — Snapshot, including virtual datasets¶
open(path) builds the query Snapshot:
- If
pathis a leaf Dataset, the Snapshot has one source_file_ = ".". Readers MUST NOT recurse into the leaf for additional sources, even if a nestedchronicle.manifestexists inside it. - If
pathis a JSON / JSONL / NDJSON file, the Snapshot has one file source. - If
pathis a Directory, the caller MAY force it as a query root. The Snapshot is a virtual dataset: recurse underpathand collect every nested leaf Dataset and every JSON / JSONL / NDJSON that is not inside a leaf. Intermediate unlabeled directories are not sources; they only contribute path segments to_file_._file_is relative to thispath.
A serve mount is one open(uri). Browsing inside that mount is list of a
prefix. Open in Runs on a prefix SHOULD keep that Snapshot and MAY narrow
with _file_. The Runs page MAY still reconstruct a path tree from run
summaries (PathExplorer); that tree is not Datasets list.
pchronicle query ./warehouse/team is a separate open of that URI.
Branch manifests still mean "this node nests children". open of a branch
or of a Directory without a sidecar uses the same virtual-dataset walk.
list of either is still one level.
Branch aggregation and trajectory counts¶
Branch nodes MAY omit [stats] and MUST NOT be treated as trajectory sources.
Only leaf nodes contribute trajectories.
When presenting Dataset previews after open, or a leaf card in list:
- A leaf's trajectory count is
[stats].record_count(andfailed_count) when the sidecar is present and the fingerprint is trusted. listof a Directory MUST NOT roll up descendant counts (that would be a deep scan). Roll-up is a Snapshot /openconcern.- Intermediate branch directories contribute 0 of their own as sources.
- A leaf MUST NOT recurse for additional nested sources, so a physical leaf cannot double-count child leaves.
No ancestor write-back (write amplification)¶
Publishing or updating a leaf MUST update only that leaf's
chronicle.manifest. Writers MUST NOT rewrite ancestor branch manifests to
cache rolled-up totals. Branch files SHOULD remain descriptive only, for
example:
Rolled-up folder counts are a read-side concern.
Process-level refresh cache¶
Warehouse / Catalog MAY keep an in-process cache of list children and
trusted leaf stats so periodic UI refresh does not re-open Lance. Cache
entries SHOULD invalidate when a leaf fingerprint or manifest mtime
changes, or when a new chronicle.manifest appears among immediate
children. Process cache MUST NOT replace on-disk leaf manifests as the
source of truth that travels with the dataset. list refresh MUST NOT run
acceleration SQL or steps token/duration queries.
TOML schema (v1)¶
Required top-level fields¶
| Field | Type | Rules |
|---|---|---|
schema_version |
integer | MUST be 1 for this RFC |
kind |
string | MUST be "leaf" or "branch" |
Leaf-only fields¶
| Field | Type | Rules |
|---|---|---|
format |
string | MUST be present for kind = "leaf"; v1 writers MUST use compact-jsonl/v1 or storyline/v1 |
Unknown format values MUST be preserved by generic readers; format-specific
openers MAY reject unsupported values.
[identity]¶
| Field | Type | Rules |
|---|---|---|
fingerprint |
string | MUST be present when [stats] is present; binds stats to a physical revision |
For compact-jsonl v1, fingerprint SHOULD be lance:version:<N> where <N> is
the published Lance dataset version after write.
[stats]¶
| Field | Type | Rules |
|---|---|---|
record_count |
integer ≥ 0 | MUST be present for leaf compact-jsonl writers |
failed_count |
integer ≥ 0 | MUST be present; use 0 when unknown/none |
min_timestamp |
string | MAY be omitted |
max_timestamp |
string | MAY be omitted |
total_tokens |
integer ≥ 0 | MAY be omitted |
Additional stats keys MAY be added in later schema versions; v1 readers MUST
ignore unknown keys under [stats].
Example: leaf¶
schema_version = 1
kind = "leaf"
format = "compact-jsonl/v1"
[identity]
fingerprint = "lance:version:1"
[stats]
record_count = 12345
failed_count = 0
min_timestamp = "2026-01-01T00:00:00Z"
max_timestamp = "2026-09-07T01:00:00Z"
Example: branch¶
Write path¶
chronicle.manifestis a store-layer contract. The only compact-jsonl publication exits areCompactJsonlStore::publish_manifestandCompactJsonlStore::import_path. CLIimportandsyncMUST go through that store API and MUST NOT invent a parallel sidecar writer.- Compact JSONL
import/ successful republish /syncsnapshot MUST writechronicle.manifestat the output dataset root. - Writes MUST be atomic on local filesystems (write temp + rename into place).
- After a successful physical write,
fingerprintMUST match the published revision and[stats].record_countMUST equal the published row count. - If manifesto publication fails during
import_path, the import MUST fail so a half-published contract is not exposed. For read-sideensure_manifestupgrades, failure MAY be logged while still opening the physical dataset.
Read path and staleness¶
- When
fingerprintmatches the opened physical revision, readers MAY trust[stats]forls/ Datasets previews without scanning rows. - When the file is missing, unreadable, or fingerprint mismatches, store-layer
ensure_manifestSHOULD rewrite the sidecar in place; if that fails, readers MUST fall back to existing discovery / summary paths. - Manifest stats MUST NOT be the sole authority for query correctness; SQL and record listing still read the physical store.
Warehouse Datasets / ls implications¶
- CLI default
lsand/api/explorer/tree(or its successor) MUST be the samelist(path): one level, Dataset children annotated fromchronicle.manifestwhen present. pchronicle ls --sourcesMUST printopen(path)Snapshot members. SQLdataset.sourcesremains the in-query equivalent.- Web Datasets MUST NOT reconstruct folders from
RunSummary,explorer_weight, shallow-nav fallback, or anotheroverflow bucket. - The Runs Run paths tree MAY keep grouping runs by
_file_/ import path (run-reconstructed tree). That surface is Runs, not Datasets. - Clicking a Directory navigates (
listof that prefix). Clicking a leaf Dataset does not drill into Lance interiors. Open in Runsopens the current query root (the serve mount) and MAY filter_file_. - Detailed run/record pages MAY still open the physical leaf; this RFC does not require a full sidecar index of every record identity.
Required unit tests (v1)¶
Implementations MUST cover at least:
listis one level:warehouse/(Directory)withteam/(Directory)andarchive/(leaf, N)liststeam/as a directory andarchiveas a dataset withrecord_count = N. It MUST NOT listteam/codex_jsonl.openis a virtual dataset:open(warehouse)yields a compact source atteam/codex_jsonlwithrecord_count = N(and any sibling leaves / JSON outside leaves), without opening Lance when the leaf manifesto is present.listof a leaf:list(archive)reports one Dataset plus preview and MUST NOT listdata/or_versions/.- Leaf non-recursion on
open: a leaf directory that also contains a nestedchronicle.manifestMUST NOT emit an additional source for the nested child. - Write isolation (contract): updating one leaf's manifesto MUST NOT
require changing parent branch files for that leaf's
listpreview to remain correct. - CLI/UI parity: the Web Datasets children for a prefix match
pchronicle lsof the same path (names, kinds, and Dataset previews). ls --sources:ls --sources warehouseliststeam/codex_jsonl(and sibling Snapshot members) and MUST matchopen(warehouse)/dataset.sources.
Compatibility¶
- Datasets without
chronicle.manifestremain valid. The first store open or heuristic discovery that confirms compact-jsonl SHOULD backfill the sidecar. - Lance schema metadata
pchronicle.format = compact-jsonl/v1remains the physical format marker; the sidecar does not replace it. - Object-store URIs are out of scope for v1 writers; remote reads MAY be added
later with the same schema. Nested branch scanning on object stores MAY use
prefix listing plus exact-key reads of
chronicle.manifest; v1 does not require S3 name-glob search.
Alternatives considered¶
- Extend Lance
_versions/*.manifest— rejected: binary MVCC format, not owned by pChronicle, unsuitable for nesting and UI stats. - Warehouse-only memory cache as the sole stats store — rejected: does not travel with the dataset and resets on process restart. Process cache remains valid as a refresh optimization on top of on-disk leaf manifests.
- Explicit parent
childrenlists — deferred: automatic scanning matches directory trees and avoids stale child lists. - Write rolled-up
[stats]onto every ancestor branch — rejected for v1: causes write amplification and stale parents when a single leaf is updated. - Reconstruct Datasets folders from run summaries / acceleration —
rejected for Datasets / default
ls. The Runs Run paths tree MAY still group by run_file_/ import path. - Default
lsdumping Snapshot members — rejected. Defaultlsmatches shell listing.ls --sourcesanddataset.sourcesremain the Snapshot member views.