4.6 KiB
Prior art: a header flag one writer sets and content another writer cannot see
Serves: variants:question:non-csharp-writers-mark-ids (raised in
variants:follow_up:foreign-ids-under-v1-header, campaign variants). Gathered by
Imagination, session self-2026-10-14b-eureka, 2026-10-14. Facts with pointers; the
recommendation lives in the question, not here.
The CultLib case: the store header (cultcache.store.v1 / v3) says whether any record
holds an element id, so a reader older than ids refuses the store instead of rewriting
elements without them. An element id is an ordinary keyed string in the payload, so a
runtime that does not know which member is the id (cultcache-py, -ts, -rs today) cannot
tell from content whether to mark. The persistence contract
(src/GameCult.Caching/Contracts/cultcache-persistence-format.md:424-427 at 6afa8ed9)
says those runtimes keep a v3 header they found and do not decide.
How established formats split the job
| System | Who sets the flag | What an implementation that does not understand the feature does | Pointer |
|---|---|---|---|
| ext4 superblock | The code that first uses a feature sets its bit in s_feature_compat, _ro_compat or _incompat. |
Unknown incompat bit: refuse to mount (fsck stops too). Unknown ro_compat bit: mount read-only, never write. Unknown compat bit: read and write freely; fsck should not modify. |
https://www.kernel.org/doc/html/latest/filesystems/ext4/super.html (feature fields) |
| SQLite file header | The writer's journal mode sets the write/read version bytes (offsets 18, 19); the schema format number (offset 44) is set by the writer of the newer schema features. | Read version 1-2 with write version > 2: open read-only. Read version > 2: neither read nor write. | https://www.sqlite.org/fileformat.html section 1.3.3, 1.3.10 |
| git repository format | A repository declares core.repositoryformatversion = 1 and the extensions.* it uses; the tool enabling a feature writes its extension key. |
Any unimplemented extensions.* key in a v1 repo: the operation must not proceed (read or write). |
https://git-scm.com/docs/repository-version |
| Protocol Buffers | No flag. New fields are self-delimited by tag. | Parsers keep unknown fields and write them back (proto2 always; proto3 again since 3.5), so an old reader does not drop a newer writer's data on rewrite. | https://protobuf.dev/programming-guides/proto3/#unknowns |
| Avro container files | The writer embeds its full schema in the file header. | Reader resolves writer schema against its own; a writer field the reader lacks is ignored, so it is lost if the reader rewrites. | https://avro.apache.org/docs/current/specification/ ("Schema Resolution") |
| Parquet | Writer stamps created_by (application and version) in FileMetaData. |
Readers keep a list of known-bad writer versions and distrust their metadata (parquet-mr ignores min/max statistics from writers before 1.8.0, PARQUET-251). | https://github.com/apache/parquet-format/blob/master/src/main/thrift/parquet.thrift (created_by); https://issues.apache.org/jira/browse/PARQUET-251 |
Patterns, as facts
- Flag-based formats (ext4, SQLite, git) put the duty to set the flag on the writer that introduces the content. None of them has a writer that carries feature content while not knowing it is that feature: an implementation that does not understand a feature either does not write (ro_compat, SQLite read-only) or does not touch the store (git).
- Formats without a flag (protobuf, Avro) handle the older reader by preservation
(protobuf keeps unknown fields) or accept loss (Avro drops unknown writer fields on
rewrite). CultCache's C# 1.x readers skip unknown keyed slots and do not preserve
them, which is why the
v3header exists. - Parquet's
created_byis a third option: readers judge content by who wrote it and compensate for writers known to get a flag or statistic wrong. - No system surveyed asks a reader to infer a feature flag from content it cannot recognise; where content cannot be recognised, the burden is on writers.
Mechanism facts from this pass (probes at 6afa8ed9 on Yggdrasil)
- A C# cache that pulls the Python-written
py-store/leaf-record.cc(v1, element ids000000000021/22) holds that record withHoldsIds = true,IdsInMemoryOnly = false; a C# write that adds an unrelated record copies it forward and leaves the headerv1. The ids survive in C#; a pre-id reader with a type lacking the id slot would drop them. - cultcache-py, -ts and -rs read
v3and keep it on rewrite; none has a declaration of which member is an element id.