# The guarantee

If a store opens, every row it holds was rebuilt through the schema you are holding right now. There is no such thing as a row from an older version: those live in an older file, under an older fingerprint.

add and put accept a row by making the round trip it will make anyway — the instance is encoded to its transport form and rebuilt through the schema — so what the store keeps always came out of the constructor and is known to survive the file.

The store has no second validation system. A row that does not validate fails with the core's own error, word for word:

tasks.add(Task(title=""))
# SchemaValueError: title: too short: 0 chars, minimum 1

That is the same line struct_of(Task).build({"title": ""}) produces. Only one message belongs to the store itself, because only the store can be asked this:

tasks.add("not a task")
# SchemaTypeError: expected Task, got str

The instance the store keeps is not the one you passed; mutating yours afterwards changes nothing inside.

One question the store asks on its own behalf, after the core has spoken: a row has to fit in a UTF-8 file. A lone surrogate is a str Python allows and no encoder can write, so it is refused at add rather than discovered at close:

tasks.add(Task(title="\ud800"))
# SchemaValueError: cannot be written as UTF-8: 'utf-8' codec can't encode …

The property, stated plainly: if the core compiles your schema, the store persists it without loss or fails loudly — at add, or at store_of before a lockfile is taken. No exception, and no asterisk.