# Phase 2 Explicit Anchor And Route Governance Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 把网页问答入口真正绑定到当前页面业务对象,并补一页 manager-only 的 route/context 治理看板,让 PMFlow 能同时做到“前台少串对象、后台能看误路由样本”。 **Architecture:** Phase 2A 不改 AdaptiveRAG 的语义边界,只把现有 `project_id / source_chat_id` 从稳定网页入口显式送进 `/rag/ask`,并在全局浮窗展示“当前页面锚点”。Phase 2B 在不引入在线自动调参的前提下,围绕 `query_route_memories` 新增只读 summary/list/detail API 和一页治理 UI,优先解决“看不见 misroute / clarify-heavy / negative feedback 样本”的问题。 **Tech Stack:** FastAPI, Pydantic, SQLAlchemy 2.0, AdaptiveRAG, React 18, TypeScript, React Query, Vitest, pytest --- ## Scope And Guardrails - v1 显式锚点优先做稳定强锚点: - 项目看板 / 宪章页路径里的 `project_id` - `Inbox` / `AI Steward` 页面 URL query 中已存在的 `project_id` - URL query 中已显式存在的 `source_chat_id` - v1 不在网页里新增“猜测型 source_chat_id 推断器”;只有真实入口已携带时才透传,避免把锚点系统重新做成隐式猜测。 - v1 治理页只做只读审计与人工排查支持: - `route_mode_predicted != route_mode_effective` - `reply_contract.mode=clarify` 或 `effective_source_policy=uncertain` - `feedback_outcome.rating=negative` - detail drawer 中暴露 `probe_snapshot / query_profile / feedback_outcome` - v1 不做在线阈值自调、不自动改路由、不引入新的 route memory 打分逻辑。 ## Baseline File Map ### Phase 2A: Explicit Web Anchor - Modify: `backend/tests/test_rag_route_contract.py` - 锁 `/rag/ask` 在 request 里显式传入 `project_id / source_chat_id` 时必须原样进入 ingress contract。 - Create: `frontend/src/components/ai-steward/qaPageAnchor.ts` - 统一从 `pathname + searchParams` 解析全局网页问答锚点,输出 `projectId / sourceChatId / anchorType / anchorLabel / origin`。 - Create: `frontend/src/components/ai-steward/qaPageAnchor.test.ts` - 锁不同页面路径和 query param 的锚点解析规则。 - Modify: `frontend/src/components/ai-steward/FloatingQAChat.tsx` - 接入页面锚点解析,把 `project_id / source_chat_id` 真正带进 `askRag()` 请求,并在状态条展示“当前页面锚点”。 - Modify: `frontend/src/components/ai-steward/QAConversationStatusBar.tsx` - 增加页面锚点文案和“当前页面已锁定项目/会话”的显式状态。 - Modify: `frontend/src/components/ai-steward/FloatingQAChat.test.tsx` - 锁全局浮窗在项目页 / inbox query 下会带锚点发问。 - Modify: `frontend/src/pages/project-board/ProjectBoardPage.tsx` - 项目页“打开 AI”入口改成带 `project_id` 跳转,确保页面级强锚点入口真正可达。 ### Phase 2B: Route And Context Governance Page - Create: `backend/app/schemas/query_route_governance.py` - 定义 summary/list/detail 的 response schema。 - Create: `backend/app/services/adaptive_rag_service/mixin_query_route_governance.py` - 新增 `summary / list / detail` 只读查询能力,封装 `query_route_memories` 的聚合、筛选和详情序列化。 - Modify: `backend/app/services/adaptive_rag_service/service.py` - 把治理 mixin 接入 `AdaptiveRAGService`。 - Create: `backend/app/api/rag_api/routes_route_governance.py` - 提供 manager-only 的 `/rag/query-route-governance/*` API。 - Modify: `backend/app/api/rag_api/__init__.py` - 挂载新路由。 - Create: `backend/tests/test_query_route_governance_api.py` - 锁 summary/list/detail 合同和权限边界。 - Create: `frontend/src/api/queryRouteGovernance.ts` - 前端 API 封装和类型定义。 - Create: `frontend/src/pages/query-route-governance/QueryRouteGovernancePage.tsx` - 治理页编排容器,沿用 manager-only 子页模式。 - Create: `frontend/src/pages/query-route-governance/components/QueryRouteGovernanceOverviewCards.tsx` - 顶部 KPI 卡。 - Create: `frontend/src/pages/query-route-governance/components/QueryRouteGovernanceTable.tsx` - 样本列表与筛选器。 - Create: `frontend/src/pages/query-route-governance/components/QueryRouteGovernanceDetailDialog.tsx` - detail drawer,展示 `reply_contract / probe_snapshot / query_profile / feedback_outcome`。 - Create: `frontend/src/pages/QueryRouteGovernancePage.tsx` - 页面 barrel export。 - Create: `frontend/src/pages/query-route-governance/QueryRouteGovernancePage.test.tsx` - 锁 manager-only UI、KPI、表格和 detail dialog。 - Modify: `frontend/src/router/index.tsx` - 挂载 `/ai-config/query-route-governance`,并放进 manager-only guard。 - Modify: `frontend/src/pages/ai-config/constants.ts` - 把治理页挂到 AI Config 治理工具入口。 ### Docs And Inventory - Modify: `CLAUDE.md` - Modify: `docs/architecture/ARCHITECTURE_INVENTORY.md` - Modify: `docs/architecture/CODE_REFERENCE_INDEX.md` - Modify: `docs/architecture/RUNTIME_GUARDRAILS.md` --- ### Task 1: Lock Explicit Anchor Contract Before UI Changes **Files:** - Modify: `backend/tests/test_rag_route_contract.py` - Create: `frontend/src/components/ai-steward/qaPageAnchor.test.ts` - [ ] **Step 1: Add the failing backend contract test for explicit anchor forwarding** Add a route-contract test that proves: ```python request = RAGAskRequest( question="这个项目怎么样", project_id=17, source_chat_id="pmflow-project-17", ) kwargs = rag_ask_contract.build_kernel_ingress_kwargs( request=request, current_user=SimpleNamespace(id=1, tenant_id=1), ) assert kwargs["project_hint_id"] == 17 assert kwargs["channel_ref"] == "pmflow-project-17" assert kwargs["ingress_options"]["rag_request_context"]["project_id"] == 17 assert kwargs["ingress_options"]["rag_request_context"]["source_chat_id"] == "pmflow-project-17" ``` - [ ] **Step 2: Add the failing front-end page anchor parser tests** Cover at least these cases: ```ts expect(resolveQAPageAnchor("/projects/17/board", "")).toMatchObject({ projectId: 17, anchorType: "project", }); expect(resolveQAPageAnchor("/inbox", "?project_id=19")).toMatchObject({ projectId: 19, origin: "search_param", }); expect(resolveQAPageAnchor("/ai-steward", "?tab=qa&project_id=23&source_chat_id=chat-23")).toMatchObject({ projectId: 23, sourceChatId: "chat-23", }); ``` - [ ] **Step 3: Run the focused tests and confirm they fail** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger && PYTHONPATH=backend backend/venv/bin/python -m pytest backend/tests/test_rag_route_contract.py -k "project_id or source_chat_id" -v cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run src/components/ai-steward/qaPageAnchor.test.ts ``` Expected: FAIL because the parser file does not exist yet and the forwarding assertions are not locked. ### Task 2: Implement Stable Web Anchor Resolution In The Global Floating Chat **Files:** - Create: `frontend/src/components/ai-steward/qaPageAnchor.ts` - Modify: `frontend/src/components/ai-steward/FloatingQAChat.tsx` - Modify: `frontend/src/components/ai-steward/QAConversationStatusBar.tsx` - Modify: `frontend/src/components/ai-steward/FloatingQAChat.test.tsx` - Test: `frontend/src/components/ai-steward/qaPageAnchor.test.ts` - Test: `frontend/src/components/ai-steward/FloatingQAChat.test.tsx` - [ ] **Step 1: Implement the minimal page-anchor resolver** Add a helper that only consumes stable web hints: ```ts export interface QAPageAnchor { projectId: number | null; sourceChatId: string | null; anchorType: "project" | "project_filter" | "chat_scope" | "none"; anchorLabel: string | null; origin: "pathname" | "search_param" | "none"; } ``` Parsing rules: - `/projects/:id/board` and `/projects/:id/charter/:charterId` -> `projectId` - query `project_id` -> `projectId` - query `source_chat_id` -> `sourceChatId` - no strong anchor -> `anchorType="none"` - [ ] **Step 2: Wire `FloatingQAChat` to send explicit anchors** When submitting a question: ```ts const pageAnchor = resolveQAPageAnchor(location.pathname, location.search); await aiStewardApi.askRag({ ...body, project_id: pageAnchor.projectId ?? undefined, source_chat_id: pageAnchor.sourceChatId ?? undefined, }); ``` Also keep the current session-state logic intact; page anchor only supplements request scope and UI display. - [ ] **Step 3: Extend the status bar with explicit page-anchor state** Render a compact line such as: ```tsx 当前页面锚点:项目 #17 ``` Only show it when `pageAnchor.anchorType !== "none"`. - [ ] **Step 4: Update floating-chat tests to assert anchor forwarding** Add a case that mounts `FloatingQAChat` under a project-board route and asserts: ```ts expect(aiStewardApiMock.askRag).toHaveBeenCalledWith( expect.objectContaining({ project_id: 17 }) ); ``` - [ ] **Step 5: Re-run the focused front-end tests** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run src/components/ai-steward/qaPageAnchor.test.ts src/components/ai-steward/FloatingQAChat.test.tsx ``` Expected: PASS ### Task 3: Route Existing Project Entry Points Into Anchor-Bearing AI URLs **Files:** - Modify: `frontend/src/pages/project-board/ProjectBoardPage.tsx` - Modify: `frontend/src/components/ai-steward/FloatingQAChat.test.tsx` - [ ] **Step 1: Write the failing project-entry test** Lock that project-board “打开 AI” jumps to an anchored URL: ```ts expect(navigate).toHaveBeenCalledWith("/ai-steward?tab=qa&project_id=17"); ``` - [ ] **Step 2: Run the focused test to confirm it fails** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run src/components/ai-steward/FloatingQAChat.test.tsx ``` Expected: FAIL because project-board still routes to bare `/ai-steward`. - [ ] **Step 3: Update project-board AI entrypoints** Change the existing project-board entry from: ```ts navigate("/ai-steward") ``` to: ```ts navigate(`/ai-steward?tab=qa&project_id=${id}`) ``` - [ ] **Step 4: Re-run the affected test** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run src/components/ai-steward/FloatingQAChat.test.tsx ``` Expected: PASS ### Task 4: Lock Route Governance API Contract **Files:** - Create: `backend/tests/test_query_route_governance_api.py` - [ ] **Step 1: Write the failing summary/list/detail API tests** Seed `query_route_memories` rows that cover: - `route_mode_predicted != route_mode_effective` - `feedback_outcome.rating == "negative"` - `reply_contract.mode == "clarify"` Assert summary returns: ```python assert payload["data"]["kpis"]["sample_total"] == 3 assert payload["data"]["kpis"]["route_mismatch_total"] == 1 assert payload["data"]["kpis"]["negative_feedback_total"] == 1 assert payload["data"]["kpis"]["clarify_total"] == 1 ``` Assert list returns rows with: ```python assert item["route_mode_predicted"] == "internal" assert item["route_mode_effective"] == "external" assert item["feedback_rating"] == "negative" assert item["is_clarify"] is True ``` Assert detail returns: ```python assert detail["probe_snapshot"]["effective_source_policy"] == "external" assert detail["query_profile"]["source_policy"] == "internal" assert detail["reply_contract"]["mode"] == "clarify" ``` - [ ] **Step 2: Add the failing manager-only permission test** Assert non-manager gets `403`, manager gets `200`. - [ ] **Step 3: Run the backend test and confirm it fails** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger && PYTHONPATH=backend backend/venv/bin/python -m pytest backend/tests/test_query_route_governance_api.py -v ``` Expected: FAIL because the API and service methods do not exist yet. ### Task 5: Implement Read-Only Route Governance Backend **Files:** - Create: `backend/app/schemas/query_route_governance.py` - Create: `backend/app/services/adaptive_rag_service/mixin_query_route_governance.py` - Modify: `backend/app/services/adaptive_rag_service/service.py` - Create: `backend/app/api/rag_api/routes_route_governance.py` - Modify: `backend/app/api/rag_api/__init__.py` - Test: `backend/tests/test_query_route_governance_api.py` - [ ] **Step 1: Add the minimal governance schemas** Define Pydantic models for: - `QueryRouteGovernanceSummaryResponse` - `QueryRouteGovernanceListItem` - `QueryRouteGovernanceDetailResponse` Keep the frontend-safe shape small; do not expose `question_embedding`. - [ ] **Step 2: Implement summary/list/detail service methods** Add read-only methods with project filter and status filters: ```python async def get_query_route_governance_summary(...) async def list_query_route_governance_items(...) async def get_query_route_governance_detail(...) ``` Derived fields: - `is_route_mismatch = route_mode_predicted != route_mode_effective` - `feedback_rating = feedback_outcome["rating"]` - `is_clarify = reply_contract["mode"] == "clarify"` - [ ] **Step 3: Expose manager-only API routes** Add: - `GET /rag/query-route-governance/summary` - `GET /rag/query-route-governance/items` - `GET /rag/query-route-governance/items/{route_memory_id}` Use `require_roles("ADMIN", "BOSS")`. - [ ] **Step 4: Re-run the governance API test** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger && PYTHONPATH=backend backend/venv/bin/python -m pytest backend/tests/test_query_route_governance_api.py -v ``` Expected: PASS ### Task 6: Build The Manager-Only Route Governance Page **Files:** - Create: `frontend/src/api/queryRouteGovernance.ts` - Create: `frontend/src/pages/query-route-governance/QueryRouteGovernancePage.tsx` - Create: `frontend/src/pages/query-route-governance/components/QueryRouteGovernanceOverviewCards.tsx` - Create: `frontend/src/pages/query-route-governance/components/QueryRouteGovernanceTable.tsx` - Create: `frontend/src/pages/query-route-governance/components/QueryRouteGovernanceDetailDialog.tsx` - Create: `frontend/src/pages/QueryRouteGovernancePage.tsx` - Create: `frontend/src/pages/query-route-governance/QueryRouteGovernancePage.test.tsx` - Modify: `frontend/src/router/index.tsx` - Modify: `frontend/src/pages/ai-config/constants.ts` - [ ] **Step 1: Write the failing page test** Cover these cases: ```tsx it("renders manager-only guard copy for non-manager users", ...) it("renders KPI cards and table rows from governance summary/list data", ...) it("opens detail dialog and shows probe snapshot / feedback outcome", ...) ``` - [ ] **Step 2: Run the front-end test to confirm it fails** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run src/pages/query-route-governance/QueryRouteGovernancePage.test.tsx ``` Expected: FAIL because the page and API wrapper do not exist yet. - [ ] **Step 3: Add the front-end API wrapper** Expose: ```ts getSummary(params?) listItems(params?) getDetail(routeMemoryId) ``` - [ ] **Step 4: Build the governance page using the semantic-facts board pattern** Page layout: - top header + feature guide - KPI cards - filterable table - detail drawer Suggested table columns: - question text - predicted/effective route - decision origin - feedback rating - flags (`mismatch`, `clarify`, `negative`) - created_at - [ ] **Step 5: Add manager-only route and AI Config entry** Router: ```tsx { path: "ai-config/query-route-governance", element: } ``` Governance tools card: ```ts { id: "query-route-governance", name: "Route 治理看板", to: "/ai-config/query-route-governance", } ``` - [ ] **Step 6: Re-run the front-end page test** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run src/pages/query-route-governance/QueryRouteGovernancePage.test.tsx ``` Expected: PASS ### Task 7: Docs Sync And Final Verification **Files:** - Modify: `CLAUDE.md` - Modify: `docs/architecture/ARCHITECTURE_INVENTORY.md` - Modify: `docs/architecture/CODE_REFERENCE_INDEX.md` - Modify: `docs/architecture/RUNTIME_GUARDRAILS.md` - [ ] **Step 1: Sync docs with new page, route, and anchor rules** Document: - explicit web-anchor baseline (`project_id/source_chat_id` from stable page contexts) - manager-only route governance page and API - “治理页只读、不自动改路由”的 guardrail - [ ] **Step 2: Run the focused backend verification** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger && backend/venv/bin/python -m py_compile \ backend/app/schemas/query_route_governance.py \ backend/app/services/adaptive_rag_service/mixin_query_route_governance.py \ backend/app/api/rag_api/routes_route_governance.py cd /Users/yuuuu/Documents/AIProjectManger && PYTHONPATH=backend backend/venv/bin/python -m pytest \ backend/tests/test_rag_route_contract.py -k "project_id or source_chat_id" -v cd /Users/yuuuu/Documents/AIProjectManger && PYTHONPATH=backend backend/venv/bin/python -m pytest \ backend/tests/test_query_route_governance_api.py -v ``` Expected: PASS - [ ] **Step 3: Run the focused front-end verification** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx vitest run \ src/components/ai-steward/qaPageAnchor.test.ts \ src/components/ai-steward/FloatingQAChat.test.tsx \ src/pages/query-route-governance/QueryRouteGovernancePage.test.tsx cd /Users/yuuuu/Documents/AIProjectManger/frontend && npx tsc --noEmit cd /Users/yuuuu/Documents/AIProjectManger/frontend && npm run build ``` Expected: PASS - [ ] **Step 4: Restart backend and confirm runtime recovery** Run: ```bash cd /Users/yuuuu/Documents/AIProjectManger && docker compose restart backend cd /Users/yuuuu/Documents/AIProjectManger && curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/docs cd /Users/yuuuu/Documents/AIProjectManger && docker compose ps backend ``` Expected: - `/docs` returns `200` - `pmflow-backend` returns to `healthy` --- ## Acceptance Checklist - 页面级全局浮窗在项目页、Inbox `project_id` 过滤页、AI Steward `project_id` query 下都会把锚点显式带进 `/rag/ask` - 项目页进入 AI 的入口不再裸跳 `/ai-steward` - `QAConversationStatusBar` 能让用户看见当前页面锚点 - manager-only `Route 治理看板` 已可查看 summary/list/detail - 治理页能直接筛出 `mismatch / clarify / negative feedback` 样本 - 文档、测试、build、backend restart 已完成