Skip to content

epic-3/story-1: Add the Persistence Boundary and In-Memory Store - #33

Closed
usmanabbas7 wants to merge 2 commits into
epic-2/story-4-expose-tracking-lifecycle-events-and-delivery-outcomesfrom
epic-3/story-1-add-the-persistence-boundary-and-in-memory-store
Closed

epic-3/story-1: Add the Persistence Boundary and In-Memory Store#33
usmanabbas7 wants to merge 2 commits into
epic-2/story-4-expose-tracking-lifecycle-events-and-delivery-outcomesfrom
epic-3/story-1-add-the-persistence-boundary-and-in-memory-store

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Story 3.1 — Add the Persistence Boundary and In-Memory Store

Finalizes the DataStore persistence 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_checkable typing.Protocol extended 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_many documented as optional future extensions (not required). Protocol-only now — the concrete class was relocated out.
  • adapters/storage/in_memory.py (L3, NEW) — thread-safe InMemoryDataStore: own private per-instance dict + per-instance threading.Lock; idempotent delete; lazy ttl expiry via time.monotonic() ((value, expires_at_monotonic)); None for 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(); one DataStore per Core, injected down into the tracker/dedup seam as a DataStore-typed param.
  • tracking/deduplication.py(visitor_id, goal_id) dedup state now read/written ONLY through the injected DataStore, using the collision-safe namespaced key f"dedup:{json.dumps([visitor_id, goal_id])}" (F-050). Truth table unchanged.
  • Public surfaceDataStore re-exported from convert_sdk.ports; DataStore + InMemoryDataStore exported from the package root (additive; Core/Context/__version__/LifecycleEvent stable).

Acceptance criteria & precedence rulings (resolved against ground truth + on-branch code)

All 6 ACs and 12 Critical Warnings satisfied.

  • F-046 — default store stdlib-only; httpx remains the sole runtime dependency.
  • F-047/F-048 — 4-method protocol; @runtime_checkable structural validation; core depends only on the protocol, never the concrete class.
  • F-049ttl uses time.monotonic() (immune to wall-clock/NTP/DST shifts), not time.time().
  • F-050 — collision-safe json.dumps namespaced dedup key.
  • F-005 — dedup "goal tracked" marker persisted unconditionally for every goal-rule-passing conversion (incl. 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

InMemoryDataStore relocated L1→L3; ports/storage.py holds only the protocol; core.py is the sole concrete-import site. import-linter is not in the MVP toolchain on this branch, so the contract is enforced by tests/test_layering.py, which statically scans domain//ports//evaluation//tracking//context.py for 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 shared in_memory_store fixture. The single direct Tracker(...) test fixture was updated to inject a DataStore (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 referencing config.data_store per Critical Warning Codex/feat tracking delivery mvp #12 — the default-enrichData=false consequence is a Story 2.2 serializer concern, not this story.
  • One pre-existing pyflakes finding (typing.Any unused in tracking/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.

@usmanabbas7 usmanabbas7 self-assigned this Jun 8, 2026
@usmanabbas7
usmanabbas7 requested a review from clllaur June 8, 2026 07:40

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:

usmanabbas7 and others added 2 commits June 14, 2026 21:59
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>
@usmanabbas7
usmanabbas7 force-pushed the epic-2/story-4-expose-tracking-lifecycle-events-and-delivery-outcomes branch from a4d2036 to 919e549 Compare June 14, 2026 16:59
@usmanabbas7
usmanabbas7 force-pushed the epic-3/story-1-add-the-persistence-boundary-and-in-memory-store branch from c413d52 to 52f5a08 Compare June 14, 2026 16:59
@abbaseya

Copy link
Copy Markdown
Collaborator

Superseded — all commits already in main (bc76b64). Closing without merge as part of post-sprint cleanup.

@abbaseya abbaseya closed this Jun 18, 2026
@abbaseya
abbaseya deleted the epic-3/story-1-add-the-persistence-boundary-and-in-memory-store branch June 18, 2026 16:31
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.

2 participants