chore(user-interviews): address facade isolation review nits#59241
Conversation
- Swap stdlib dataclass for pydantic.dataclasses.dataclass in facade/contracts.py — matches the architecture guidance for runtime validation of the public contract. - Move parse_interviewee_identifier + has_replied into backend/logic.py so the facade only delegates and stays thin and stable. - Populate facade/__init__.py with public re-exports (parse_interviewee_identifier, has_replied, IntervieweeIdentity) and add a docstring to presentation/__init__.py so neither file is an empty __init__. Generated-By: PostHog Code Task-Id: 639c774a-3c16-4b58-94a4-35682c62ade2
Prompt To Fix All With AIFix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
products/user_interviews/backend/logic.py:12
**`logic.py` imports from the `facade` package — dependency direction is inverted**
`logic.py` is the inner/lower layer; `facade/` is the outer layer that wraps it. Having `logic.py` import from `facade/contracts.py` means the lower layer depends on the upper layer, which is backwards. In practice this doesn't cause a circular import today (since `contracts.py` itself imports nothing from `logic`), but it makes the layering confusing and fragile — any future import added to `contracts.py` could create a cycle.
The fix is to move `IntervieweeIdentity` (and `contracts.py`) one level up to `products/user_interviews/backend/contracts.py` so that both `logic.py` and `facade/api.py` import it from a neutral sibling module. `facade/__init__.py` can continue to re-export it for the public surface.
Reviews (1): Last reviewed commit: "chore(user-interviews): address facade i..." | Re-trigger Greptile |
Reviewer pointed out the inversion: logic.py was importing from facade/contracts.py, which makes the inner layer depend on the outer one. No cycle today (contracts has no imports from logic) but fragile. Move contracts.py one level up to backend/contracts.py so logic.py and facade/api.py both import it from a neutral sibling. facade/__init__.py keeps re-exporting IntervieweeIdentity so external callers continue to import it via the facade package. Generated-By: PostHog Code Task-Id: 639c774a-3c16-4b58-94a4-35682c62ade2
There was a problem hiding this comment.
A bot reviewer raised a valid unresolved concern: the new logic.py imports IntervieweeIdentity from facade/contracts.py, making the inner layer depend on the outer layer — the opposite of the intended direction. Per review policy, bot comments with valid concerns that go unaddressed require escalation.
There was a problem hiding this comment.
The bot's concern about inverted dependency direction was raised on an older commit and has been fully addressed: contracts.py was moved out of facade/ to backend/, so logic.py now imports from the sibling backend/contracts.py rather than from the outer facade/ layer. Dependency direction is now correct, and no showstoppers remain.
…tion CI's "Lint product structure" check (tools/hogli-commands/.../product/checks.py) requires every product listed in tach.toml's canonical facade alternation to have backend/facade/contracts.py — moving contracts.py to backend/contracts.py broke that invariant. This matches products/architecture.md, which puts contracts.py under facade/, and the established pattern in mcp_analytics, experiments, and access_control where logic.py also imports from facade/contracts.py. The greptile bot's layering concern is noted but doesn't survive the codebase convention: facade/contracts.py is a public-surface marker (no Django imports, tracked as Turbo contract inputs), not the outer wrapping layer it might look like from the path alone. Generated-By: PostHog Code Task-Id: 639c774a-3c16-4b58-94a4-35682c62ade2
New commits pushed (delta classified non_trivial_delta) — stamphog approval dismissed; re-review running automatically.
There was a problem hiding this comment.
Clean facade isolation refactor — moves logic out of the facade into a dedicated layer, makes the facade thin, and wires up proper exports. The noted inverted dependency (logic.py importing IntervieweeIdentity from facade/contracts.py) is an architectural nit on a no-dependency frozen dataclass, not a functional or safety issue, and the inline concern is marked resolved.
Problem
Follow-up nits from the review of #59132 (the user_interviews facade + contracts isolation PR):
facade/contracts.pyused stdlibdataclasses.dataclass— the recent architecture guidance preferspydantic.dataclasses.dataclassso contracts get runtime validation at construction time.facade/__init__.pyandpresentation/__init__.pywere empty, which CLAUDE.md discourages.logic.py.Changes
dataclasses.dataclassforpydantic.dataclasses.dataclassinproducts/user_interviews/backend/facade/contracts.py.products/user_interviews/backend/logic.pycontainingparse_interviewee_identifierandhas_replied(the regex + ORM call). The facade now just delegates.facade/__init__.pywith public re-exports (mirrors theexperimentsfacade convention) and add a docstring topresentation/__init__.py.Public facade surface (
parse_interviewee_identifier,has_replied,IntervieweeIdentity) is unchanged, so callers inposthog/api/sharing.pyandpresentation/views.pykeep working as-is.How did you test this code?
I'm an agent — no manual testing performed. Existing automated coverage in
products/user_interviews/backend/test_facade.pyexercises both functions through the facade and should continue to pass unchanged.Publish to changelog?
no
🤖 Agent context
Authored by PostHog Code (an agent) in response to the human author asking it to apply three specific review comments from #59132. Decisions:
pydantic.dataclasses.dataclassrather thanpydantic.BaseModel— products/architecture.md describes this as the preferred upgrade over stdlib dataclasses, andIntervieweeIdentityhas no need forBaseModel-only features.experimentsproduct'sfacade/__init__.pyshape (re-exports +__all__) and theerror_trackingproduct'spresentation/__init__.pyshape (single docstring) since both already match this codebase's existing conventions.Created with PostHog Code