PMFlow/docs/superpowers/plans/2026-04-17-phase2-explicit-...

545 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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: <ManagerOnlyGuard><QueryRouteGovernancePage /></ManagerOnlyGuard> }
```
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 已完成