Skip to content

feat(core): one versioning rule — every mutation snapshots and bumps version#77

Merged
cuibonobo merged 1 commit into
mainfrom
claude/issue-61-versioning-rule
Jul 10, 2026
Merged

feat(core): one versioning rule — every mutation snapshots and bumps version#77
cuibonobo merged 1 commit into
mainfrom
claude/issue-61-versioning-rule

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Closes #61.

Only content updates participated in versioning before this: associate(), dissociate(), setPermissions(), and delete() passed straight through to the adapter — no snapshot, no version bump, no audit trail. That made #59's recoverability promise ("anything a write-holder does, the owner can undo") false for every association or permission change, and left ifVersion-style conflict detection (#48) blind to non-content races.

One rule now, no special cases: every mutation of a record — content, associations, permissions, soft delete, undelete — snapshots the prior full state and bumps version. Hard delete is the one exception; it destroys the record and its history outright, so there's nothing to snapshot.

What's added

  • RecordVersion gains optional associations/permissions (packages/core/src/types.ts, wire-types, adapter-api's parseVersion/serializeVersion). The SQLite adapter's versions table had no columns for either — added as JSON, the same pattern already used for permissions on the live records table.
  • associate/dissociate/setPermissions/delete (soft)/undelete all fetch the record, snapshot it, mutate, then bump version — with no-op guards so an identical re-associate, a dissociate of something not present, or a deep-equal setPermissions costs nothing (no bump, no snapshot). Association equality matches the SQLite adapter's existing primary key (kind, label, fileId/recordIdmimeType excluded).
  • restoreVersion() now restores associations when the target snapshot has them, but never permissions — those stay owner/creator territory (setPermissions's existing carve-out), so a content rollback can't silently widen or narrow access as a side effect.
  • Behavior tightening, worth flagging: associate/dissociate/soft-delete didn't fetch the record first, so a call against a missing record either silently no-op'd or threw a generic adapter Error, depending on which adapter. Snapshotting requires fetching first, so these now consistently throw StackNotFoundError — matching update/setPermissions/undelete/restoreVersion, which already worked this way.
  • Spec (docs/spec.md) states the one-rule contract in §Versions and updates the wire endpoint description to match.

Out of scope (per #61's own comment thread)

Extending the ifVersion CAS option to the newly-versioned paths — that's #48's feature and it hasn't landed at all yet (no ifVersion anywhere in the codebase to extend).

Test plan

  • Added tests across packages/core (version monotonicity across mixed mutations, no-op guards, restore-restores-associations, restore-never-restores-permissions, StackNotFoundError for missing records), record-adapter-sqljs (version round-trip of associations/permissions), and adapter-api (wire parsing of the new fields)
  • pnpm -r build — clean
  • pnpm -r typecheck — clean across all 6 packages
  • pnpm -r lint — clean
  • pnpm -r test — 371 tests passing workspace-wide
  • prettier --check on changed files — clean

Refs #61, #59, #48, #47


Generated by Claude Code

…version

Closes #61. Only content updates participated in versioning before this:
associate(), dissociate(), setPermissions(), and delete() passed straight
to the adapter with no snapshot, no version bump, no audit trail. That
made #59's recoverability promise ("anything a write-holder does, the
owner can undo") false for every association or permission change, and
left ifVersion-style conflict detection (#48) blind to non-content races.

One rule now, no special cases: every mutation of a record — content,
associations, permissions, soft delete, undelete — snapshots the prior
full state and bumps version. Hard delete is the only exception; it
destroys the record and its history outright, so there's nothing to
snapshot.

- RecordVersion gains optional associations/permissions (core, wire-types,
  adapter-api's parseVersion/serializeVersion). The SQLite adapter's
  versions table had no columns for either — added as JSON, same pattern
  already used for permissions on the live records table.
- associate/dissociate/setPermissions/delete(soft)/undelete all fetch the
  record, snapshot it, mutate, then bump version — with no-op guards so an
  identical re-associate, a dissociate of something not present, or a
  deep-equal setPermissions costs nothing (no bump, no snapshot).
  Association equality matches the SQLite adapter's existing primary key
  (kind, label, fileId/recordId — mimeType excluded).
- restoreVersion() now restores associations when the target snapshot has
  them, but never permissions — those stay owner/creator territory
  (setPermissions's existing carve-out), so a content rollback can't
  silently widen or narrow access as a side effect.
- Behavior tightening: associate/dissociate/delete(soft) didn't fetch the
  record first, so a call against a missing record either silently no-op'd
  or threw a generic adapter Error depending on which adapter. Snapshotting
  requires fetching first, so these now consistently throw
  StackNotFoundError — matching update/setPermissions/undelete/
  restoreVersion, which already worked this way.

Out of scope, per #61's own comment thread: extending the ifVersion CAS
option to the newly-versioned paths. That's #48's feature and it hasn't
landed at all yet — there's no ifVersion anywhere in the codebase to
extend.
@cuibonobo cuibonobo merged commit 2fdd3b2 into main Jul 10, 2026
5 checks passed
@cuibonobo cuibonobo deleted the claude/issue-61-versioning-rule branch July 10, 2026 00:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One versioning rule: every record mutation bumps version and snapshots full prior state (associations and permissions included)

2 participants