Skip to content

Commit 28a9f53

Browse files
earayuclaude
andauthored
refactor(modularization): Phase 6 (#20) cleanup — retire residual Protocol + promote is_terminal + identity facade (#1635)
## Phase 6 cleanup — residual seams resolved (task #20) Lands the canonical Phase 6 cleanup log registered during Phase 5 close-out (msg=da408d0f, 5 entries). PM msg=09e6753c / msg=95b584b0 locked scope strictly to these 5 entries; no Phase 3/4/5 legacy shim sweep, no new-phase re-migration. ### Entries - **Entry 4** `1e80679` — retire the dead ``ChatDocumentOps`` Protocol class literal (no runtime reference after Phase 5 5-S5b). - **Entry 5** `68ef567` — promote ``_TERMINAL_RUN_STATUSES`` frozenset to ``EvaluationRunStatus.is_terminal()`` classmethod. Task #23 race-fix guard (PR #1631 54cd86b) is byte-identical — classmethod accepts both enum members and raw strings because ``EvaluationRunStatus`` inherits ``str`` and the DB column is plain ``String``. - **Entry 2** `8ac8ce2` — retire the ``ChatCollectionServiceOps`` Protocol + DI seam. ``chat_document_service`` sibling-imports ``chat_collection_service`` directly now that both live in the conversation domain after 5-S4f. CRITICAL_WIRINGS registry collapsed 3→2 in the same commit per PM msg=f7616d25. - **Entry 1** `d04ed2a` — replace the 5-S4f raw ``UPDATE users …`` with an ``identity_user_ops.set_chat_collection`` facade (9a-sexdec hierarchy-1 terminal state). User ORM writes now travel through a narrow identity-owned entry point; G16 stays green without raw SQL. - **Entry 3** `4a7f0bc` — record ``PromptTemplateOps`` (and ``QuotaOps``) as permanent standalone-infra seams rather than transitional Phase-6 candidates. Doc-only refactor; no code move, no caller touch. ### Final state The ``CRITICAL_WIRINGS`` registry converges to **2 permanent entries** — both provider modules are standalone-infrastructure (no natural domain home): 1. ``conversation.bot_service._quota_ops`` — ``aperag.service.quota_service`` 2. ``agent_runtime.runtime._prompt_template_ops`` — ``aperag.service.prompt_template_service`` Original canonical estimate 5 → Phase 5 collapsed to 3 → Phase 6 collapsed to 2 permanent. ### Merge gate - @huangheng current-head CR no-blocker for bfd5590 (Phase 6 CR chain 5/5 all no-blocker) - lint-and-unit pass (2m54s) - e2e-http-compose skipping (path filter by design) - Per @earayu2 canonical (16:13/16:16): 1 CR no-blocker + lint-and-unit pass ⇒ merge authorized, e2e not gating Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c0a60d4 commit 28a9f53

14 files changed

Lines changed: 171 additions & 275 deletions

File tree

aperag/app.py

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,6 @@
4242
chat_router as chat_router,
4343
)
4444
from aperag.domains.conversation.service.bot_service import set_quota_ops as _conv_set_quota_ops
45-
from aperag.domains.conversation.service.chat_document_service import (
46-
set_chat_collection_ops as _conv_set_chat_collection_ops,
47-
)
4845
from aperag.domains.evaluation.api.routes import router as evaluation_v2_router
4946
from aperag.domains.governance.api.routes import router as governance_router
5047
from aperag.domains.identity.service.user_manager import (
@@ -107,33 +104,19 @@
107104
_kb_set_quota_ops(_legacy_quota_service)
108105

109106
# Wire the conversation domain's consumer-owned QuotaOps DI slot for
110-
# ``bot_service`` (Phase 5 step 5-S4c, canonical msg=4a93c97e Q1 C).
111-
# The legacy quota_service structurally satisfies the narrower
112-
# conversation ``QuotaOps`` Protocol (``check_and_consume_quota`` +
113-
# ``release_quota``) via the same singleton, so the same legacy
114-
# instance is plugged in here — Phase 6 cleanup removes the wire-up
115-
# when quota_service finds its permanent home.
107+
# ``bot_service``. ``quota_service`` is a standalone-infrastructure
108+
# module with no natural domain home, so the Protocol + DI seam is
109+
# the permanent integration point. The singleton structurally
110+
# satisfies the narrower conversation ``QuotaOps`` Protocol
111+
# (``check_and_consume_quota`` + ``release_quota``) directly.
116112
_conv_set_quota_ops(_legacy_quota_service)
117113

118-
# Wire the ``ChatCollectionServiceOps`` DI slot for ``chat_document_service``
119-
# (Phase 5 step 5-S4d). ``chat_document_service`` moved into the
120-
# conversation domain before ``chat_collection_service`` itself — the
121-
# sibling service still needs a Phase 4 identity-domain rebase for the
122-
# ``session.get(User, ...)`` rewrite. Until 5-S4f (after ``#1633``
123-
# merges), the legacy singleton fills the DI slot; it structurally
124-
# satisfies the Protocol verbatim.
125-
from aperag.service.chat_collection_service import ( # noqa: E402
126-
chat_collection_service as _legacy_chat_collection_service,
127-
)
128-
129-
_conv_set_chat_collection_ops(_legacy_chat_collection_service)
130-
131-
132114
# Wire the agent_runtime domain's consumer-owned PromptTemplateOps DI
133-
# slot (Phase 5 step 5-S5b). ``prompt_template_service`` stays as a
134-
# legacy provider through Phase 6 cleanup, so an adapter exposes the
135-
# three Protocol methods onto the legacy singleton + module-level
136-
# ``build_agent_query_prompt`` helper.
115+
# slot. ``prompt_template_service`` is a standalone-infrastructure
116+
# module (no natural domain home), so the Protocol + DI seam is the
117+
# permanent integration point — an adapter exposes the three Protocol
118+
# methods onto the singleton + module-level ``build_agent_query_prompt``
119+
# helper.
137120
from aperag.service.prompt_template_service import ( # noqa: E402
138121
build_agent_query_prompt as _legacy_build_agent_query_prompt,
139122
)

aperag/db/repositories/evaluation_v2.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616
``aperag.domains.evaluation.db.repositories.evaluation_v2`` after
1717
Phase 5 step 5-S6.
1818
19-
``aperag.db.ops`` pulls in the repository mixin from here (plus the
20-
``_TERMINAL_RUN_STATUSES`` frozenset if any caller still wants the
21-
module-level alias), so the existing call sites keep working without
22-
a rename sweep. Phase 6 cleanup removes the shim.
19+
``aperag.db.ops`` pulls in the repository mixin from here so the
20+
existing call sites keep working without a rename sweep. Phase 6
21+
cleanup removes the shim.
2322
"""
2423

2524
from aperag.domains.evaluation.db.repositories.evaluation_v2 import ( # noqa: F401

aperag/domains/agent_runtime/ports.py

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,13 @@
1414

1515
"""Cross-domain contracts owned by the ``agent_runtime`` domain.
1616
17-
Phase 5 step 5-S5b adds the ``PromptTemplateOps`` Protocol for the
18-
legacy ``aperag.service.prompt_template_service`` provider —
19-
``prompt_template_service`` stays in ``aperag/service/`` through
20-
Phase 6 cleanup, so the agent_runtime domain cannot ``from
21-
aperag.service.*`` import it directly without tripping G1.
22-
23-
The ``ChatDocumentOps`` Protocol originally seeded in Phase 5 step 1
24-
has been **retired** per msg=940bd884 simplification: after
25-
``chat_document_service`` physically moved into the conversation
26-
domain (Phase 5 step 5-S4d), ``runtime.py`` can reach it via a
27-
direct cross-domain import (domain→domain is allowed by G1). The
28-
Protocol is kept here only in archived form (below the ``__all__``
29-
for the live surface) so any in-flight branch still referencing it
30-
resolves the same shape; Phase 6 removes it outright.
17+
``PromptTemplateOps`` is the consumer-owned Protocol seam for
18+
``aperag.service.prompt_template_service``. The service is a
19+
standalone-infrastructure module (cross-cutting across agent_runtime
20+
runtime calls, conversation bot-config resolution, indexing prompt
21+
resolution, and a user-facing ``/prompts`` REST surface) with no
22+
natural domain home — the Protocol + DI seam is its permanent
23+
integration point under G1.
3124
3225
``AuthenticatedUser`` stays per-domain (lesson 9a-ter); runtime
3326
handlers only need ``id`` for turn ownership / lease checks.
@@ -53,8 +46,7 @@ class AuthenticatedUser(Protocol):
5346

5447
@runtime_checkable
5548
class PromptTemplateOps(Protocol):
56-
"""Consumer-owned view of the legacy
57-
``aperag.service.prompt_template_service``.
49+
"""Consumer-owned view of ``aperag.service.prompt_template_service``.
5850
5951
Exposes the three surfaces ``runtime.py`` actually uses:
6052
@@ -73,11 +65,6 @@ class PromptTemplateOps(Protocol):
7365
singleton plus the module-level ``build_agent_query_prompt``
7466
function, so ``aperag/app.py`` wires an adapter that fans out to
7567
both to satisfy the Protocol.
76-
77-
Phase 6 cleanup will either move ``prompt_template_service`` into
78-
a canonical domain home (agent_runtime or model_platform candidate
79-
per msg=65a3b27d) or retire the Protocol in favour of a direct
80-
cross-domain import.
8168
"""
8269

8370
async def resolve_agent_system_prompt(self, *, bot: Any, user_id: str) -> str: ...
@@ -99,17 +86,3 @@ def build_agent_query_prompt(
9986
"AuthenticatedUser",
10087
"PromptTemplateOps",
10188
]
102-
103-
104-
# ``ChatDocumentOps`` retained for any in-flight caller that still
105-
# imports it. Phase 5 step 5-S5b replaced the runtime's DI seam with a
106-
# direct cross-domain import of
107-
# ``aperag.domains.conversation.service.chat_document_service`` now
108-
# that the conversation domain is merged — the Protocol definition is
109-
# no longer wired at app startup and is scheduled for removal in
110-
# Phase 6.
111-
112-
113-
@runtime_checkable
114-
class ChatDocumentOps(Protocol): # pragma: no cover - retired, Phase 6 deletion candidate
115-
async def has_documents_in_chat(self, chat_id: str, user_id: str) -> bool: ...

aperag/domains/agent_runtime/runtime.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -296,11 +296,6 @@ async def emit(
296296
)
297297

298298
history_context = await self.history_writer.build_history_context(user, chat.id)
299-
# Direct cross-domain import of the sibling conversation service
300-
# (Phase 5 step 5-S4d moved it into the conversation domain) —
301-
# G1 allows domain→domain. msg=940bd884 simplification avoids a
302-
# ``ChatDocumentOps`` Protocol seam now that the provider has
303-
# moved out of the legacy ``aperag.service.*`` ban target.
304299
from aperag.domains.conversation.service.chat_document_service import chat_document_service
305300

306301
has_chat_files = await chat_document_service.has_documents_in_chat(chat.id, user)

aperag/domains/conversation/ports.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -115,34 +115,8 @@ async def release_quota(
115115
) -> None: ...
116116

117117

118-
@runtime_checkable
119-
class ChatCollectionServiceOps(Protocol):
120-
"""``chat_document_service``'s view of ``chat_collection_service``.
121-
122-
Phase 5 step 5-S4d moves ``chat_document_service`` into the
123-
conversation domain before ``chat_collection_service`` itself —
124-
``chat_collection_service.create_user_chat_collection`` reaches
125-
``session.get(User, ...)`` which requires the Phase 4 identity
126-
domain merge before it can be rewritten without leaking a G1
127-
legacy-aggregate ``User`` import into the domain module.
128-
129-
Until the sibling service follows (5-S4f after ``#1633`` merges,
130-
per PM ``msg=0f9f10c4`` β ruling), the two surfaces
131-
``chat_document_service`` actually calls —
132-
``get_user_chat_collection`` and ``create_user_chat_collection``
133-
— are exposed here as a consumer-owned Protocol. ``aperag/app.py``
134-
wires the legacy singleton in at startup; the legacy instance
135-
structurally satisfies the Protocol verbatim.
136-
"""
137-
138-
async def get_user_chat_collection(self, user_id: str) -> Any: ...
139-
140-
async def create_user_chat_collection(self, user_id: str) -> Any: ...
141-
142-
143118
__all__ = [
144119
"KnowledgeBaseCollectionView",
145120
"AuthenticatedUser",
146121
"QuotaOps",
147-
"ChatCollectionServiceOps",
148122
]

aperag/domains/conversation/service/chat_collection_service.py

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,33 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Chat-collection management service moved to the conversation domain
16-
in Phase 5 step 5-S4f.
17-
18-
Phase 4 ``#1633`` + Phase 3 ``#1629`` are merged on main, so the
19-
cross-domain ORM imports take the β canonical path from
20-
``msg=4ed698d8`` — direct domain-to-domain imports of ``User`` /
21-
``Collection`` + sibling domain services, no stopgap Protocol + DI
22-
wrapper:
23-
24-
* ``aperag.db.models.User`` → ``aperag.domains.identity.db.models.User``
25-
* ``aperag.db.models.Collection`` →
26-
``aperag.domains.knowledge_base.db.models.Collection``
27-
* ``aperag.schema.view_models.CollectionConfig / ModelSpec /
28-
TagFilterCondition / TagFilterRequest`` → ``aperag.schema.common``
29-
for the first two and
30-
``aperag.domains.model_platform.schemas`` for the tag filters.
31-
* ``aperag.service.collection_service`` →
32-
``aperag.domains.knowledge_base.service.collection_service``
33-
* ``aperag.service.llm_available_model_service`` →
34-
``aperag.domains.model_platform.service.llm_available_model_service``
35-
36-
The pre-move ``KnowledgeBaseCollectionView`` return type (from
37-
``aperag.domains.conversation.ports``) is kept — the Protocol was
38-
established in Phase 3 Step 5c for exactly this module, and there is
39-
no benefit in swapping it for the concrete ``Collection`` class here.
40-
41-
The ``_mark_as_chat_collection`` transaction closure uses
42-
``session.get(Collection, ...)`` — Collection is now imported from the
43-
knowledge_base domain, which G1 permits. The ``User.chat_collection_id``
44-
update is issued via ``sqlalchemy.text`` rather than ``session.get(User,
45-
...)`` + attribute assignment so the conversation domain does not have
46-
to import the identity-owned ``User`` ORM class (G16 canonical per
47-
``msg=6d2ae86a`` forbids ``User`` imports outside the identity domain).
48-
The raw UPDATE hits the same ``users`` table that the ORM mapper binds
49-
to, so the transaction semantics are identical.
15+
"""Chat-collection management service in the conversation domain.
16+
17+
Cross-domain dependencies are direct imports:
18+
19+
* ``aperag.domains.identity.db.models.User`` is **not** imported here —
20+
``User.chat_collection_id`` writes travel through the identity-owned
21+
``set_chat_collection`` facade (lesson 9a-sexdec hierarchy-1) so G16
22+
stays green without the conversation domain leaking the ``User`` ORM.
23+
* ``aperag.domains.knowledge_base.db.models.Collection`` is imported
24+
directly (domain→domain is legal under G1).
25+
* ``aperag.domains.knowledge_base.service.collection_service`` + the
26+
``model_platform`` availability service similarly sibling-import.
27+
28+
The pre-move ``KnowledgeBaseCollectionView`` return type is kept —
29+
the Protocol was established in Phase 3 Step 5c for exactly this
30+
module, and there is no benefit in swapping it for the concrete
31+
``Collection`` class here.
5032
"""
5133

5234
from __future__ import annotations
5335

5436
import logging
5537
from typing import Optional
5638

57-
from sqlalchemy import text
58-
5939
from aperag.db.ops import async_db_ops
6040
from aperag.domains.conversation.ports import KnowledgeBaseCollectionView
41+
from aperag.domains.identity.service.identity_user_ops import set_chat_collection as identity_set_chat_collection
6142
from aperag.domains.knowledge_base.db.models import Collection
6243
from aperag.domains.knowledge_base.schemas import CollectionCreate
6344
from aperag.domains.knowledge_base.service.collection_service import collection_service
@@ -172,13 +153,7 @@ async def _mark_as_chat_collection(session):
172153
session.add(collection_obj)
173154
await session.flush()
174155

175-
# Update User.chat_collection_id via raw SQL so this module
176-
# does not have to import the identity-owned ``User`` ORM
177-
# class (G16 canonical).
178-
await session.execute(
179-
text("UPDATE users SET chat_collection_id = :cid WHERE id = :uid"),
180-
{"cid": collection_response.id, "uid": user_id},
181-
)
156+
await identity_set_chat_collection(session, user_id, collection_response.id)
182157
await session.flush()
183158

184159
await self.db_ops.execute_with_transaction(_mark_as_chat_collection)

0 commit comments

Comments
 (0)