epic-3/story-1: Add the Persistence Boundary and In-Memory Store - #33
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the storage boundary of the SDK by moving the concrete InMemoryDataStore adapter to adapters/storage/in_memory.py (L3) and leaving only the DataStore protocol in ports/storage.py (L1). The DataStore protocol is expanded to include a delete method and an optional ttl parameter on set. Additionally, the Tracker now receives the DataStore via dependency injection from the composition root (core.py), and deduplication keys are updated to be collision-safe using JSON serialization. Feedback on these changes suggests updating the ttl parameter type annotation from Optional[int] to Optional[float] in both the protocol and the in-memory implementation to allow for sub-second durations and conform to standard Python conventions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def set(self, key: str, value: Any) -> None: | ||
| """Store ``value`` under ``key`` (overwriting any existing value).""" | ||
| ... | ||
| def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: |
There was a problem hiding this comment.
In Python, time durations/offsets in seconds are conventionally represented as float (which PEP 484 type checkers treat as compatible with int). Typing ttl as Optional[int] unnecessarily restricts the protocol to whole seconds and will cause type-checking warnings if a user or adapter implementation wants to use sub-second/fractional TTLs (e.g., 0.5).\n\nConsider changing the type hint of ttl to Optional[float] in both the DataStore protocol and the InMemoryDataStore implementation.
| def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: | |
| def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None: |
| return None | ||
| return value | ||
|
|
||
| def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: |
There was a problem hiding this comment.
In Python, time durations/offsets in seconds are conventionally represented as float (which PEP 484 type checkers treat as compatible with int). Typing ttl as Optional[int] unnecessarily restricts the implementation to whole seconds and will cause type-checking warnings if a user or adapter implementation wants to use sub-second/fractional TTLs (e.g., 0.5).\n\nConsider changing the type hint of ttl to Optional[float] to match the standard Python conventions for durations.
| def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None: | |
| def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None: |
Beads: ai-driven-product-dev-j5yh,z3yx,h77h,wd9r,200t,0igt DataStore 4-method protocol, InMemoryDataStore L3 adapter (ttl/NFR9/threadsafe), core wiring + NFR19, F-050 dedup key, layering enforcement, qs-06 integration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-j5yh,z3yx,h77h,wd9r,200t,0igt - DataStore L1 protocol extended to get/set/has/delete + ttl (@runtime_checkable) - InMemoryDataStore relocated to adapters/storage/in_memory.py (L3): per-instance dict+lock, lazy monotonic ttl (F-049), idempotent delete, None absence - core.py owns one per-Core DataStore (None->InMemoryDataStore), injected into Tracker - dedup state behind injected DataStore via F-050 namespaced key; truth table unchanged - additive root exports DataStore + InMemoryDataStore; ports re-exports DataStore - layering enforced via test (inner layers never import concrete adapter) Full suite: 396 passed (was 366; +30; zero regressions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a4d2036 to
919e549
Compare
c413d52 to
52f5a08
Compare
|
Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup. |
Story 3.1 — Add the Persistence Boundary and In-Memory Store
Finalizes the
DataStorepersistence boundary and its in-memory default, and migrates Story 2.3's conversion-dedup state behind that boundary with zero behavioral change. Pulled forward (Phase 4) because the evaluation core will need a stable storage protocol for sticky bucketing.What was built
ports/storage.py(L1) —DataStore@runtime_checkabletyping.Protocolextended to the frozen 4-method MVP surface:get(key) -> Any | None,set(key, value, ttl: int | None = None),has(key) -> bool,delete(key).get_many/set_manydocumented as optional future extensions (not required). Protocol-only now — the concrete class was relocated out.adapters/storage/in_memory.py(L3, NEW) — thread-safeInMemoryDataStore: own private per-instancedict+ per-instancethreading.Lock; idempotentdelete; lazyttlexpiry viatime.monotonic()((value, expires_at_monotonic));Nonefor absence. Stdlib-only.core.py(L4) — sole composition-root import site:self._data_store = config.data_store if config.data_store is not None else InMemoryDataStore(); oneDataStoreperCore, injected down into the tracker/dedup seam as aDataStore-typed param.tracking/deduplication.py—(visitor_id, goal_id)dedup state now read/written ONLY through the injectedDataStore, using the collision-safe namespaced keyf"dedup:{json.dumps([visitor_id, goal_id])}"(F-050). Truth table unchanged.DataStorere-exported fromconvert_sdk.ports;DataStore+InMemoryDataStoreexported from the package root (additive;Core/Context/__version__/LifecycleEventstable).Acceptance criteria & precedence rulings (resolved against ground truth + on-branch code)
All 6 ACs and 12 Critical Warnings satisfied.
httpxremains the sole runtime dependency.@runtime_checkablestructural validation; core depends only on the protocol, never the concrete class.ttlusestime.monotonic()(immune to wall-clock/NTP/DST shifts), nottime.time().json.dumpsnamespaced dedup key.force_multiple=True) before sending — consistent with Story 2.3's shipped F-007; the stale "only after a tracked enqueue" phrasing (Critical Warning feat(python-sdk): add logger, datastore queue parity, and docs refresh #6) was not regressed to.Layering enforcement
InMemoryDataStorerelocated L1→L3;ports/storage.pyholds only the protocol;core.pyis the sole concrete-import site. import-linter is not in the MVP toolchain on this branch, so the contract is enforced bytests/test_layering.py, which statically scansdomain//ports//evaluation//tracking//context.pyfor any import of the concrete adapter (all clean).Tests
366 → 396 (+30; zero regressions). NFR9 two-instance isolation, thread-safety under concurrent access, NFR19 duck-typed-stub swappability +
isinstance(stub, DataStore), lazy-ttl expiry, dedup truth-table regression (marker observable via injected store), and qs-06 integration via the sharedin_memory_storefixture. The single directTracker(...)test fixture was updated to inject aDataStore(seam change; all prior 2.3/2.4 tests pass).Traceability
Beads epic
ai-driven-product-dev-pctr; tasks-j5yh,-z3yx,-h77h,-wd9r,-200t,-0igt(all closed). Readiness: PASS 8.8/10, 4 auto-delegated questions (sprint-mode "your call"; see assessment report). Code review: clean, round 1.Deferred / noted
enrichData/serializer left referencingconfig.data_storeper Critical Warning Codex/feat tracking delivery mvp #12 — the default-enrichData=falseconsequence is a Story 2.2 serializer concern, not this story.typing.Anyunused intracking/flush.py) predates this story; left untouched.Part of sprint
sprint/2026-04-06-convert-python-sdk. Stacked on #32 (story 2-4) → #31 → #30 → #29 → #24 chain.