Skip to content

fix: ops functions write through the wrapper's own __setitem__/__delitem__ (#3) - #4

Merged
thorwhalen merged 2 commits into
masterfrom
fix/3-route-ops-through-wrapper-chain
Oct 13, 2026
Merged

thorwhalen merged 2 commits into
masterfrom
fix/3-route-ops-through-wrapper-chain

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Closes #3.

What changed

The free functions in cosmodol.ops (replace_item, batch_items, add_container, delete_container(force=True), add_database, delete_database(force=True)) no longer write to the leaf directly. Each one performs the plain mapping write (store[k] = v or del store[k]) on the caller's outer store. First, it installs a one-shot redirect in the new cosmodol.routing module, held in a ContextVar and pinned to the expected leaf key. The redirect tells the leaf how to execute the single write that reaches it: as a conditional replace, a batch entry, a cascade or a create. Every layer's own write logic therefore runs:

  • Write guards (a read-only layer, mk_read_only, disable_setitem) refuse the operation.
  • preset= hooks and write-through caches see the value, exactly as they would for store[k] = v.
  • filt_iter scopes now refuse keys that an operation creates (batch create/upsert, add_container, add_database). The k in store check for existing resources is kept, and it now also applies to subclasses that override __contains__.
  • Misbehaving layers fail loudly with RedirectError. This covers a layer that writes another key first, writes twice, or swallows the write. The leaf refuses any write other than the expected (method, key) before acting. An error that a layer catches is still re-raised to the caller.
  • Batch patch has no mapping-write equivalent, so on a wrapped or subclassed store it now requires unguarded_patch=True.

Behaviour change: add_database / add_container on a filt_iter view now refuse a name outside the scope. Previously the resource was created. The old test test_add_database_does_not_raise_after_creating was rewritten to cover this.

Stated limits (in the cosmodol/ops.py docstring)

  • batch_items collects its writes before committing them as one transaction. A layer that acts after the inner write returns has already acted, even if the commit fails.
  • A layer that performs the write in another thread, or later (a write-behind buffer), escapes the redirect. The write then happens as a plain upsert or delete. The function raises, but it cannot undo the write.
  • Returned documents are in leaf form.

Adversarial review

A fresh Opus reviewer returned ship-after-fixes. All five confirmed defects are fixed, with a regression test for each:

  1. A forced delete or a replace could apply its semantics to a different key that a layer wrote first, cascading into a companion container. The redirect is now pinned to the leaf key from inner_most_key, and mismatched calls are refused before anything is touched.
  2. A precondition error that a layer swallowed was reported as success. It is now re-raised.
  3. A write made in another thread or deferred was misreported as "not performed". The message and the docs now say it may have been, or may later be, done as a plain upsert. Refusing such writes outright would also block unrelated concurrent writers that share the leaf, so this stays documented rather than enforced.
  4. The retry message wrongly said "already performed". It now reports whether the first call failed.
  5. A subclass that scopes only through __contains__ was bypassed. It is now checked.

Tests: 87 pass, including the doctests (16 new tests in tests/test_ops_layers.py, 10 of which fail on master). No dependents.

🤖 Generated with Claude Code

thorwhalen and others added 2 commits October 13, 2026 08:42
…tem__ (#3)

The free functions in cosmodol.ops now perform the plain mapping write on the
caller's store, with a one-shot ContextVar redirect (cosmodol.routing) telling
the leaf how to execute the single write that reaches it. Every layer's write
logic runs: write guards, preset hooks, write-through caches, and filt_iter
scopes on keys an operation creates. Swallowed/deferred/multiplied writes raise
RedirectError. batch_items patch on a wrapped or subclassed store needs
unguarded_patch=True.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rrors

Adversarial review of #3: a layer that writes another key first (companion
container, audit record) received the operation's semantics; now the leaf
refuses any write but the expected (method, key). Errors a layer catches are
re-raised; subclasses scoping by __contains__ are checked; messages no longer
claim a deferred write was not performed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thorwhalen
thorwhalen merged commit a3f5fa8 into master Oct 13, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the fix/3-route-ops-through-wrapper-chain branch October 13, 2026 08:48
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.

ops functions cannot see write guards, value caches or preset hooks of a wrapper

1 participant