Schema versioning
What this layer does
Phase 13 lets every schema in the substrate evolve without breaking older clients. The manifest, the eval-aggregate row, the brief frontmatter, and the bootstrap version all carry an explicit version field. Older clients refuse on unknown-future-version pull rather than silently corrupt local state.
Files involved
state/bin/sync/manifest.ts—SyncManifest.versionfield.state/bin/pid-aggregate.ts— writes_v: 1into every aggregate
row.
state/log/regen-queue/<skill>.<machine_id>.md— brief
frontmatter version: 1.
~/projects/snappy-os/.bootstrap-version— single integer; bumped
on every bootstrap-affecting change.
state/lint/sync-schema-version.ts— verifies remote manifest
version is compatible.
state/lint/aggregate-schema.ts— fails on unknown keys in
aggregate rows.
Manifest versioning
type SyncManifest = { version: 1; ... };
pullchecksversionfrom remote manifest. If remote > local
understands → refuse pull, print upgrade message naming the local bootstrap version and the suggested npx snappy-os@latest.
pushwrites its own version unconditionally — older client never
overwrites a newer manifest with a downgraded version field.
Eval-aggregate versioning
_v: 1 per the locked schema:
type AggRow = {
_v: 1;
ts: string;
skill: string;
score: number;
run_id: string;
tenant: string;
cost_usd_cents?: number;
ok: boolean;
};
The _v field comes first so a streaming reader can short-circuit on unknown-version rows without parsing the rest. The lint rejects unknown keys in known-version rows; bumping _v is the only path to adding a field.
Brief format versioning
Every brief in state/log/regen-queue/ has a frontmatter version: 1 block. The auto-regen worker reads version first; mismatches surface as eval: skipped (brief-version-mismatch) rather than running with unknown semantics.
Bootstrap version compatibility
~/projects/snappy-os/.bootstrap-version holds an integer. On every init, the bootstrap compares to the remote bootstrap-version at canonical:
- Remote ≤ local → silent green
- Remote == local + 1 → warn but proceed
- Remote > local + 1 → prompt "your snappy-os is N+ versions behind,
recommend re-running npx snappy-os@latest"
Never auto-upgrade silently. The user's runtime hooks may depend on specific bootstrap behavior; surprise upgrades break trust.
Operational gotchas
- Bumping
versionin any of these schemas requires a coordinated
release: lint update, aggregator update, Worker update, bootstrap bump. A mismatch between Worker and CLI surfaces as confusing "manifest invalid" errors on Joe machines.
- The
_vfield MUST be the first key in aggregate rows. Tail-based
readers parse just enough to get _v and skip on unknown.
- Bootstrap version is a SINGLE integer, not semver. Semver invites
patch-vs-minor confusion on a one-author project; the integer forces explicit "this is a new bootstrap behavior" decisions.
- Refusing a pull on version mismatch is intentional friction.
Auto-upgrade-on-pull would mean Joe's runtime configuration changes without his knowledge.
How to verify it's working
cat ~/projects/snappy-os/.bootstrap-versionreturns an integer.head -1 state/log/evals.aggregate.ndjson | jq '._v'returns 1.head state/log/sync-manifest.json | jq '.version'returns 1.- A simulated remote-version-bump (set local version to 0, pull from
current canonical) refuses with the documented upgrade message.
state/lint/sync-schema-version.tsand
state/lint/aggregate-schema.ts exit 0 in steady state.