Skip to content

epic-1/story-3: Create and Reuse Visitor Contexts (gap-fill) - #28

Closed
usmanabbas7 wants to merge 6 commits into
epic-1/story-6-deliver-quickstart-and-first-run-examplesfrom
epic-1/story-3-create-and-reuse-visitor-contexts
Closed

epic-1/story-3: Create and Reuse Visitor Contexts (gap-fill)#28
usmanabbas7 wants to merge 6 commits into
epic-1/story-6-deliver-quickstart-and-first-run-examplesfrom
epic-1/story-3-create-and-reuse-visitor-contexts

Conversation

@usmanabbas7

Copy link
Copy Markdown
Collaborator

Summary

Gap-fill story closing the three gaps identified by the 2026-06-07 sprint gap check — ~80% of the Context surface already shipped via story 1-4's minimal foundation (PR #26); this PR completes Story 1.3's contract on the current stacked lineage:

  1. Signature contract freezeCore.create_context(visitor_id, visitor_attributes=None) (was attributes=; reconciled-PRD frozen signature — drift would have propagated through epic-2+). Context gains a visitor_attributes accessor; attributes kept as back-compat alias. README updated; story 1-6 examples needed no change (no-attr calls). Run-time request-overlay attributes= on run_* left unchanged (separate call-time concern).
  2. src/convert_sdk/domain/context_state.py — frozen ContextState dataclass (visitor_id + defensively-copied read-only visitor_attributes + snapshot-by-reference) with non-mutating with_overlay(); Context now composes it, keeping visitor state separate from the shared immutable ConfigSnapshot.
  3. tests/test_context_creation.py — 12 dedicated AC#1/AC#2 tests (creation, stored/read-only/defensive-copy attrs, init-required, reuse across evaluations, no Core-managed cache, overlay non-mutation, snapshot linkage).

Tests: 193 → 218 passing (+8 context_state, +5 contract-freeze, +12 context_creation). No regressions.

Traceability

Notes for reviewer

  • Readiness gate produced 3 questions, auto-delegated in sprint mode (rename scope limited to creation contract; ContextState refactor depth; attributes back-compat alias). See conductor's assessment at ai-driven-product-dev/work/2026-06-07-create-and-reuse-visitor-contexts/readiness-assessment.md.
  • Code review: clean after round 1 (one finding fixed: snapshot-linkage tests made behavioral instead of reaching into _snapshot). Two non-blocking notes logged (stale module docstring; benign _snapshot/_state.snapshot duplication).

🤖 Generated with Claude Code

@usmanabbas7 usmanabbas7 self-assigned this Jun 7, 2026
@usmanabbas7
usmanabbas7 requested a review from clllaur June 7, 2026 10:21

@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 introduces a typed, immutable ContextState to manage per-visitor state separately from the shared configuration snapshot, and standardizes the API by renaming attributes to visitor_attributes across creation boundaries. The review feedback highlights a critical vulnerability in the defensive copying mechanism where wrapping a mutable dictionary in MappingProxyType can still allow mutations to leak, suggesting that the mapping should always be copied. Additionally, the feedback recommends resolving a type mismatch when initializing ContextState with an optional mapping, and adding corresponding test coverage for the defensive copying of proxy types.

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.

Comment on lines +52 to +57
if not isinstance(self.visitor_attributes, MappingProxyType):
object.__setattr__(
self,
"visitor_attributes",
MappingProxyType(dict(self.visitor_attributes or {})),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current implementation of defensive copying checks isinstance(self.visitor_attributes, MappingProxyType) to skip copying if the attributes are already wrapped in a MappingProxyType. However, a MappingProxyType is only a read-only view of an underlying dictionary. If the caller passes a MappingProxyType that wraps a mutable dictionary and subsequently mutates that dictionary, those mutations will leak into the context state, violating the defensive copying guarantee. Always copying the mapping ensures complete isolation.

        object.__setattr__(
            self,
            "visitor_attributes",
            MappingProxyType(dict(self.visitor_attributes or {})),
        )

Comment on lines +67 to +71
self._state = ContextState(
visitor_id=visitor_id,
snapshot=snapshot,
visitor_attributes=visitor_attributes,
)

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 Context.__init__, visitor_attributes is typed as Optional[Mapping[str, Any]] = None. Passing it directly to ContextState when it is None violates the type annotation of ContextState.visitor_attributes, which is typed as Mapping[str, Any] (not optional). To ensure type safety and avoid static analysis warnings, default to an empty dictionary visitor_attributes or {} when instantiating ContextState.

Suggested change
self._state = ContextState(
visitor_id=visitor_id,
snapshot=snapshot,
visitor_attributes=visitor_attributes,
)
self._state = ContextState(
visitor_id=visitor_id,
snapshot=snapshot,
visitor_attributes=visitor_attributes or {},
)

Comment on lines +55 to +60
def test_visitor_attributes_are_copied_defensively():
source = {"country": "US"}
state = ContextState(visitor_id="v", visitor_attributes=source, snapshot=_snapshot())
source["country"] = "DE" # mutate the caller's dict after construction
assert dict(state.visitor_attributes) == {"country": "US"}

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

Add a test case to verify that passing a MappingProxyType is also defensively copied and does not allow mutations to the underlying dictionary to leak into the ContextState.

Suggested change
def test_visitor_attributes_are_copied_defensively():
source = {"country": "US"}
state = ContextState(visitor_id="v", visitor_attributes=source, snapshot=_snapshot())
source["country"] = "DE" # mutate the caller's dict after construction
assert dict(state.visitor_attributes) == {"country": "US"}
def test_visitor_attributes_are_copied_defensively():
source = {"country": "US"}
state = ContextState(visitor_id="v", visitor_attributes=source, snapshot=_snapshot())
source["country"] = "DE" # mutate the caller's dict after construction
assert dict(state.visitor_attributes) == {"country": "US"}
def test_visitor_attributes_proxy_is_copied_defensively():
from types import MappingProxyType
source = {"country": "US"}
proxy = MappingProxyType(source)
state = ContextState(visitor_id="v", visitor_attributes=proxy, snapshot=_snapshot())
source["country"] = "DE"
assert dict(state.visitor_attributes) == {"country": "US"}

usmanabbas7 and others added 6 commits June 14, 2026 21:58
Beads: ai-driven-product-dev-vo1o

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… implementation (GREEN)

Beads: ai-driven-product-dev-vo1o

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(RED)

Beads: ai-driven-product-dev-5zfa

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… implementation (GREEN)

Beads: ai-driven-product-dev-5zfa

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beads: ai-driven-product-dev-nd5o

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ate attr access)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@usmanabbas7
usmanabbas7 force-pushed the epic-1/story-6-deliver-quickstart-and-first-run-examples branch from c63f8f6 to 0cf59fe Compare June 14, 2026 16:58
@usmanabbas7
usmanabbas7 force-pushed the epic-1/story-3-create-and-reuse-visitor-contexts branch from 9ec000b to 3962b4e Compare June 14, 2026 16:58
@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-1/story-3-create-and-reuse-visitor-contexts 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