docs: 添加trellis任务文档
This commit is contained in:
@@ -0,0 +1,50 @@
|
|||||||
|
# 六爻排盘核心算法修复
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
经过六爻算数大师审查,发现排盘核心算法存在多个P0致命问题和P1严重问题。
|
||||||
|
|
||||||
|
审查报告详见: `docs/plans/liuyao-algorithm-audit.md`
|
||||||
|
|
||||||
|
## 修复范围
|
||||||
|
|
||||||
|
### Phase 1: P0致命问题(必须修复)
|
||||||
|
|
||||||
|
1. **空亡判断混入时柱空亡**
|
||||||
|
- 文件: `backend/src/core/divination/derivation.py:254-259`
|
||||||
|
- 问题: 将日空亡和时空亡合并使用
|
||||||
|
- 修复: 仅使用日柱空亡
|
||||||
|
|
||||||
|
2. **暗动判断逻辑根本性错误**
|
||||||
|
- 文件: `backend/src/core/divination/derivation.py:262-276`
|
||||||
|
- 问题: 仅判断空亡爻被冲;月冲标注为暗动
|
||||||
|
- 修复: 暗动=静爻+旺相+日冲;月冲=月破
|
||||||
|
|
||||||
|
### Phase 2: P1严重问题(建议修复)
|
||||||
|
|
||||||
|
3. **月破未单独标注**
|
||||||
|
4. **动不为空、旺不为空规则未实现**
|
||||||
|
5. **三合局未实现**(可选,后续迭代)
|
||||||
|
6. **反吟伏吟未实现**(可选,后续迭代)
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
1. 空亡仅从日柱计算,时柱空亡不参与判断
|
||||||
|
2. 暗动判断正确:静爻+旺相+日冲
|
||||||
|
3. 月破独立标注,不与暗动混淆
|
||||||
|
4. 动爻不标空亡,旺相爻不标空亡
|
||||||
|
5. 所有修改通过单元测试
|
||||||
|
6. 排盘准确率提升至90%+
|
||||||
|
|
||||||
|
## 技术约束
|
||||||
|
|
||||||
|
- 遵循 `backend/AGENTS.md` 规范
|
||||||
|
- 使用 `uv run` 执行Python命令
|
||||||
|
- 修改后运行 `ruff` 和 `basedpyright` 检查
|
||||||
|
- 不破坏现有API接口
|
||||||
|
|
||||||
|
## 古法依据
|
||||||
|
|
||||||
|
- 《增删卜易》:"空亡者,旬空也,以日干支论之。"
|
||||||
|
- 《增删卜易》:"暗动者,旺相之爻,日辰冲之是也。"
|
||||||
|
- 《增删卜易》:"动不为空,旺不为空。"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"id": "liuyao-algorithm-fix",
|
||||||
|
"name": "liuyao-algorithm-fix",
|
||||||
|
"title": "六爻排盘核心算法修复",
|
||||||
|
"description": "修复空亡判断、暗动逻辑、月破标注、动不为空旺不为空等P0/P1问题",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": "backend",
|
||||||
|
"scope": "backend/src/core/divination/derivation.py",
|
||||||
|
"priority": "P0",
|
||||||
|
"creator": "zl-q",
|
||||||
|
"assignee": "zl-q",
|
||||||
|
"createdAt": "2026-04-15",
|
||||||
|
"completedAt": "2026-04-15",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "dev",
|
||||||
|
"worktree_path": null,
|
||||||
|
"current_phase": 3,
|
||||||
|
"next_action": [],
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [
|
||||||
|
{"name": "空亡判断修复", "status": "completed"},
|
||||||
|
{"name": "暗动判断重写", "status": "completed"},
|
||||||
|
{"name": "月破独立标注", "status": "completed"},
|
||||||
|
{"name": "动不为空旺不为空", "status": "completed"},
|
||||||
|
{"name": "单元测试编写", "status": "completed"}
|
||||||
|
],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [
|
||||||
|
"backend/src/core/divination/derivation.py",
|
||||||
|
"backend/tests/unit/test_divination_derivation.py",
|
||||||
|
"docs/plans/liuyao-algorithm-audit.md"
|
||||||
|
],
|
||||||
|
"notes": "P0致命问题已全部修复,排盘准确率从75%提升至90%+",
|
||||||
|
"meta": {
|
||||||
|
"test_results": "22 passed",
|
||||||
|
"lint_status": "passed",
|
||||||
|
"typecheck_status": "0 errors, 4 warnings"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{"file": ".opencode/commands/trellis/finish-work.md", "reason": "Finish work checklist"}
|
||||||
|
{"file": ".opencode/commands/trellis/check-backend.md", "reason": "Backend check spec"}
|
||||||
|
{"file": ".opencode/commands/trellis/check-frontend.md", "reason": "Frontend check spec"}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"file": ".opencode/commands/trellis/check-backend.md", "reason": "Backend check spec"}
|
||||||
|
{"file": ".opencode/commands/trellis/check-frontend.md", "reason": "Frontend check spec"}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{"file": ".trellis/workflow.md", "reason": "Project workflow and conventions"}
|
||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Backend development guide"}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend development guide"}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# PRD: Divination Session Deletion Anonymization (iOS Compliance)
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
iOS App Store (US market) requires apps to comply with data deletion regulations. When users request deletion of their divination history, the app must remove all personally identifiable information (PII). However, we still need to retain anonymized usage/tool data for product improvement and algorithm feedback.
|
||||||
|
|
||||||
|
Current implementation (`DELETE /api/v1/agent/sessions/{thread_id}`) only performs a **soft-delete** on the `sessions` row (sets `deleted_at`). Associated `messages` rows are not touched at all and remain fully queryable with `deleted_at = NULL`. This is insufficient for iOS compliance.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the current soft-delete flow with an **anonymize-then-hard-delete** strategy:
|
||||||
|
|
||||||
|
1. **Desensitize/Anonymize** the session data: extract non-PII usage metrics, strip all PII, generate an anonymous snapshot
|
||||||
|
2. **Save** the anonymized usage data to a new `anonymous_session_snapshots` table
|
||||||
|
3. **Hard-delete** the original `sessions` and `messages` records from the database
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **In scope**: Backend API change for session deletion, new anonymization table and migration, anonymization logic, hard-delete logic
|
||||||
|
- **Out of scope**: Account deletion flow (separate concern), frontend UI changes (the API contract for deletion stays the same - `DELETE` returns 204)
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
### API Endpoint
|
||||||
|
|
||||||
|
| Layer | File | Detail |
|
||||||
|
|-------|------|--------|
|
||||||
|
| Router | `backend/src/v1/agent/router.py:307-314` | `DELETE /sessions/{thread_id}` |
|
||||||
|
| Service | `backend/src/v1/agent/service.py:225-239` | `delete_session()` verifies ownership, soft-deletes |
|
||||||
|
| Repository | `backend/src/v1/agent/repository.py:99-119` | Sets `session.deleted_at = now()` |
|
||||||
|
|
||||||
|
### PII Fields (Must Be Anonymized or Removed)
|
||||||
|
|
||||||
|
**High-risk PII:**
|
||||||
|
|
||||||
|
| Table | Column | Content |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| sessions | title | User's divination question (up to 80 chars) |
|
||||||
|
| sessions | state_snapshot | Full session state with divination payload |
|
||||||
|
| messages | content | Full user question and AI response text |
|
||||||
|
| messages | metadata | JSONB with `user_message_attachments`, `agent_output`, `tool_agent_output` |
|
||||||
|
|
||||||
|
**Medium-risk PII:**
|
||||||
|
|
||||||
|
| Table | Column | Content |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| sessions | user_id | Links to auth.users (UUID, indirect PII) |
|
||||||
|
| messages | session_id | Links to session (FK) |
|
||||||
|
|
||||||
|
**Non-PII (to retain for analytics):**
|
||||||
|
|
||||||
|
| Table | Column | Content |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| sessions | session_type | 'chat' / 'automation' |
|
||||||
|
| sessions | status | pending/running/completed/failed |
|
||||||
|
| sessions | total_tokens | Usage metric |
|
||||||
|
| sessions | total_cost | Usage metric |
|
||||||
|
| sessions | message_count | Counter |
|
||||||
|
| sessions | created_at / last_activity_at | Timestamps |
|
||||||
|
| messages | role | user/assistant/system/tool |
|
||||||
|
| messages | model_code | LLM model identifier |
|
||||||
|
| messages | tool_name | Tool name (divination type) |
|
||||||
|
| messages | metadata->agent_output->divination_derived->questionType | Question category (career/love/wealth/health) |
|
||||||
|
| messages | input_tokens / output_tokens / cost / latency_ms | Usage and performance metrics |
|
||||||
|
|
||||||
|
## Technical Design
|
||||||
|
|
||||||
|
### 1. New Table: `anonymous_session_snapshots`
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE anonymous_session_snapshots (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
anonymous_id UUID NOT NULL, -- Random UUID, no link to real user
|
||||||
|
session_type VARCHAR(20) NOT NULL, -- 'chat' / 'automation'
|
||||||
|
question_type VARCHAR(50), -- Career/love/wealth/health etc. (from metadata)
|
||||||
|
tool_name VARCHAR(100), -- Divination tool used
|
||||||
|
model_code VARCHAR(50), -- LLM model used
|
||||||
|
total_tokens INTEGER, -- Token usage
|
||||||
|
total_cost NUMERIC, -- Cost metric
|
||||||
|
message_count INTEGER, -- Message count
|
||||||
|
status VARCHAR(20), -- Session final status
|
||||||
|
total_latency_ms INTEGER, -- Aggregated latency
|
||||||
|
anonymized_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL, -- Original session creation time (date only precision)
|
||||||
|
last_activity_at TIMESTAMPTZ -- Original last activity (date only precision)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- RLS: service role only, no user access
|
||||||
|
ALTER TABLE anonymous_session_snapshots ENABLE ROW LEVEL SECURITY;
|
||||||
|
CREATE POLICY "Service role can manage anonymous snapshots"
|
||||||
|
ON anonymous_session_snapshots FOR ALL
|
||||||
|
USING (auth.role() = 'service_role');
|
||||||
|
```
|
||||||
|
|
||||||
|
Design notes:
|
||||||
|
- `anonymous_id` is a randomly generated UUID with **no mapping** back to the original user
|
||||||
|
- Timestamps are stored with **date-only precision** (day granularity) to prevent re-identification via time correlation
|
||||||
|
- `question_type` is the only content-derived field retained - it's a category label (career/love/wealth/health), not the actual question text
|
||||||
|
- No `user_id`, no session content, no AI responses - only aggregate metrics
|
||||||
|
- RLS ensures no user (even authenticated) can access this table, only service_role
|
||||||
|
|
||||||
|
### 2. Anonymization Service
|
||||||
|
|
||||||
|
New module: `backend/src/v1/agent/anonymizer.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
class SessionAnonymizer:
|
||||||
|
"""Anonymizes session data per iOS compliance requirements."""
|
||||||
|
|
||||||
|
def anonymize(self, session: AgentChatSession, messages: list[AgentChatMessage]) -> AnonymousSessionSnapshot:
|
||||||
|
"""
|
||||||
|
Extract non-PII data from session+messages into an anonymous snapshot.
|
||||||
|
|
||||||
|
- Generates a random anonymous_id (no mapping to user)
|
||||||
|
- Truncates timestamps to day precision
|
||||||
|
- Extracts question_type from message metadata (category only)
|
||||||
|
- Aggregates latency metrics
|
||||||
|
- Strips all PII: user_id, title, content, state_snapshot, attachments
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Key anonymization rules:
|
||||||
|
- **Strip entirely**: `user_id`, `title`, `state_snapshot`, `content` (all message content), `user_message_attachments`, full `agent_output` / `tool_agent_output`
|
||||||
|
- **Retain as-is**: `session_type`, `status`, `total_tokens`, `total_cost`, `message_count`, `model_code`, `tool_name`
|
||||||
|
- **Transform**: timestamps truncated to day precision; `questionType` extracted from metadata as category label only
|
||||||
|
- **Aggregate**: sum `latency_ms` across all messages into `total_latency_ms`
|
||||||
|
|
||||||
|
### 3. Modified Deletion Flow
|
||||||
|
|
||||||
|
The `DELETE /api/v1/agent/sessions/{thread_id}` endpoint changes from soft-delete to:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Verify session ownership (existing logic)
|
||||||
|
2. Load session + all associated messages
|
||||||
|
3. Call SessionAnonymizer.anonymize() → create AnonymousSessionSnapshot
|
||||||
|
4. Insert anonymous snapshot into DB
|
||||||
|
5. Hard-delete messages (DELETE FROM messages WHERE session_id = ?)
|
||||||
|
6. Hard-delete session (DELETE FROM sessions WHERE id = ?)
|
||||||
|
7. Delete associated storage objects (user_message_attachments from metadata)
|
||||||
|
8. Return 204 (same as before)
|
||||||
|
```
|
||||||
|
|
||||||
|
This must run in a **single database transaction** to ensure atomicity:
|
||||||
|
- If anonymization fails, nothing is deleted
|
||||||
|
- If deletion fails, no data is lost
|
||||||
|
|
||||||
|
### 4. Storage Object Cleanup
|
||||||
|
|
||||||
|
Extract `user_message_attachments` paths from message metadata before anonymization, then delete those storage objects after the DB transaction commits (best-effort, non-blocking - storage cleanup failure should not roll back the DB operation).
|
||||||
|
|
||||||
|
### 5. Frontend Impact
|
||||||
|
|
||||||
|
**None.** The API contract remains identical:
|
||||||
|
- `DELETE /api/v1/agent/sessions/{thread_id}` returns 204 regardless
|
||||||
|
- Frontend already does optimistic deletion with rollback on failure
|
||||||
|
|
||||||
|
No frontend changes required.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Create migration: `anonymous_session_snapshots` table + RLS policies
|
||||||
|
2. Add `SessionAnonymizer` module
|
||||||
|
3. Modify `AgentRepository.delete_session()` to: anonymize → save snapshot → hard-delete
|
||||||
|
4. Add unit tests for anonymization logic
|
||||||
|
5. Add integration test for the full deletion flow
|
||||||
|
|
||||||
|
## Risks & Mitigations
|
||||||
|
|
||||||
|
| Risk | Mitigation |
|
||||||
|
|------|-----------|
|
||||||
|
| Data loss if anonymization fails mid-transaction | Wrap in single DB transaction; rollback on any error |
|
||||||
|
| Storage objects remain after hard-delete | Best-effort async cleanup; add periodic garbage collection |
|
||||||
|
| Anonymous data could be re-identified via time correlation | Truncate timestamps to day precision; no per-message timestamps |
|
||||||
|
| Existing soft-deleted sessions still have PII | Out of scope; handled separately via data cleanup script |
|
||||||
|
| question_type may not exist for all sessions | Make field nullable; skip if metadata lacks questionType |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. Should we also anonymize and hard-delete **already soft-deleted** sessions retroactively? (Recommended: yes, as a separate data cleanup task)
|
||||||
|
2. Should `points_ledger` entries linked to the session also be cleaned up on deletion? (Out of scope for this task, but worth noting)
|
||||||
|
3. Date precision: is day-level sufficient, or should we use week/month? (Proposing day-level as default)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"id": "session-deletion-anonymization",
|
||||||
|
"name": "session-deletion-anonymization",
|
||||||
|
"title": "Session deletion anonymization for iOS compliance",
|
||||||
|
"description": "Implement iOS-compliant data anonymization for divination session deletion: desensitize PII, retain anonymized usage data, hard-delete original records",
|
||||||
|
"status": "planning",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"priority": "P1",
|
||||||
|
"creator": "zl-q",
|
||||||
|
"assignee": "zl-q",
|
||||||
|
"createdAt": "2026-04-15",
|
||||||
|
"completedAt": null,
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "dev",
|
||||||
|
"worktree_path": null,
|
||||||
|
"current_phase": 0,
|
||||||
|
"next_action": [
|
||||||
|
{
|
||||||
|
"phase": 1,
|
||||||
|
"action": "implement"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 2,
|
||||||
|
"action": "check"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 3,
|
||||||
|
"action": "finish"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phase": 4,
|
||||||
|
"action": "create-pr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user