# User Profile Scoring（任务清单 · V1.2）

> 依据：`spec_kit/User Profile Scoring/plan.md`（V1.2）
>
> 规则目标：在客户端实现“问卷答案（可跳过）→ 用户画像（含 profile_answered/confidence）+ 硬规则输出”，供推荐/Push/Widget 复用。

## 任务状态说明

- `[ ]`：未开始
- `[~]`：进行中
- `[x]`：已完成
- `[-]`：已取消/不做

---

## A. 代码实现（客户端 TypeScript 模块）

### A1. 建立目录与导出入口

- [x] 在 `client/src/features/` 下新增目录 `userProfileScoring/`
  - **验收**：目录存在，且后续文件均放入该目录
- [x] 新建 `client/src/features/userProfileScoring/index.ts`
  - **内容**：对外导出主函数与类型（避免上层直接深路径 import）
  - **验收**：上层可仅从 `.../userProfileScoring` 引入

### A2. 定义类型与枚举（V1.2）

- [x] 新建 `client/src/features/userProfileScoring/types.ts`
  - **定义输入类型**：`QuestionnaireAnswersV1_2`
    - 字段允许 `undefined`/`null`：`mom_stage/emotion/context/need`
  - **定义输出类型**：`UserProfileV1_2`
    - 固定 `profile_version: "v1.2"`、`profile_source: "questionnaire"`
    - `profile_answered`：`{ stage; emotion; context; need }`
    - `emotion_score: number | null`
    - `context/need: Record<string, 1>`（稀疏 one-hot；跳过为 `{}`）
  - **定义扩展输出类型**：`UserProfileV1_2_Extended`
    - `rule_hits: string[]`
    - `hard_rules: { forbidden_risk_flags: string[]; forbidden_content_predicates: ... }`
  - **验收**：类型字段与 `spec_kit/User Profile Scoring/spec.md`（V1.2）一致

### A3. 实现答案归一化（非法值按跳过）

- [x] 新建 `client/src/features/userProfileScoring/scoring.ts`
- [x] 实现 `normalizeAnswers(raw): QuestionnaireAnswersV1_2`
  - **规则**：
    - 值不在枚举内 → 归一化为 `undefined`
    - `null` 保留为 `null`（表示显式跳过/无值）
  - **验收**：任意输入不抛错；输出只包含合法枚举/`undefined`/`null`

### A4. 计算 profile_answered（V1.2）

- [x] 实现 `computeProfileAnswered(normalized)`：
  - `stage = (mom_stage !== undefined && mom_stage !== null)`
  - `emotion/context/need` 同理
  - **验收**：与 `spec.md` 6.1.1 一致；可区分“缺失/跳过”与“明确作答”

### A5. 计算时间衰减 conf_time

- [x] 实现 `computeTimeConfidence(generatedAt, now): number`
  - **规则**（与 `spec.md` 6.2 一致）：
    - 0–7 天：1.0
    - 7–30 天：线性衰减到 0.7
    - 30 天以上：0.5
  - **验收**：给定固定时间输入，输出符合分段规则（允许浮点误差）

### A6. 计算 profile_confidence（V1.2 完整度因子 + clamp）

- [x] 实现 `computeProfileConfidence(conf_time, profile_answered): number`
  - **规则**（与 `设计说明文档/客戶端問卷打分規則.md` V1.2 一致）：
    - `answered_count = Σ I[profile_answered.* = true]`
    - `completion = answered_count / 4`
    - `completion_factor = 0.5 + 0.5 * completion`
    - `conf = clamp(conf_time * completion_factor, 0.2, 1.0)`
  - **验收**：
    - 全部跳过且 `conf_time=1` → `conf=0.5`
    - 全部作答且 `conf_time=1` → `conf=1.0`
    - 极端情况下不会低于 0.2 / 高于 1.0

### A7. 生成四维度画像字段（含跳过策略）

- [x] 实现 `buildUserProfileFromQuestionnaire(answers, options)`
  - **元信息**：
    - `profile_version="v1.2"`
    - `profile_source="questionnaire"`
    - `profile_generated_at`：允许注入；默认 `new Date().toISOString()`
  - **stage**：
    - 若 `mom_stage` 缺失/为 `null`：按安全策略输出 `stage.unknown=1`
    - 若为枚举：对应 one-hot
  - **emotion_score**：
    - 若跳过：`null`
    - 否则按映射表输出数值
  - **context/need**：
    - 若跳过：输出 `{}`
    - 否则输出稀疏 one-hot（例如 `{ work: 1 }`）
  - **验收**：与 `spec.md` 4.2/5.x/6.x 一致

### A8. 硬规则与可观测输出（rule_hits / hard_rules）

- [x] 在 `buildUserProfileFromQuestionnaire` 内生成 `rule_hits` 与 `hard_rules`
  - **risk_flags 映射规则**（与 `spec.md` 7 一致）：
    - `stage.unknown=1` → `unsafe_for_stage_unknown`
    - `stage.parenting=1` → `unsafe_for_stage_parenting`
    - `emotion_score !== null && emotion_score<=0.2` → `unsafe_for_emotion_low`
  - **跨维度规则谓词**（与 `spec.md` 7、客户端规则 V1.2 一致）：
    - 若 `stage.unknown=1`：追加谓词
      - `id`: 建议 `unknown_block_parenting_pressure_personalized`
      - `when_user`: `{ stage_unknown: true }`
      - `forbid_content`: `{ need: "parenting_pressure", personalization_power: 1 }`
  - **验收**：
    - 输出结构稳定
    - 不依赖内容侧字段（本模块只输出“可执行条件”）

---

## B. 单元测试（只测纯函数）

> 若当前工程暂不引入测试框架，可先把测试用例写成“可执行脚本/最小断言”；但建议最终落地 `vitest` 或 `jest`。

### B1. 建立测试框架（可选但建议）

- [x] 若 `client/` 尚无测试：引入 `vitest`（或 `jest`）并添加 `test` 脚本
  - **验收**：能在本机运行测试并输出结果

### B2. 覆盖 spec.md 的关键用例

- [x] 用例：完整作答映射正确（对应 `spec.md` 用例 A/B）
- [x] 用例：时间衰减（对应 `spec.md` 用例 C/D/E）
- [x] 用例：全部跳过 `{}`（对应 `spec.md` 用例 A2）
  - 断言：`profile_answered` 全 false；`stage.unknown=1`；`emotion_score=null`；`context/need={}`；`profile_confidence=0.5`（当 conf_time=1）
- [x] 用例：硬规则命中
  - `emotion_score<=0.2` → 含 `unsafe_for_emotion_low`
  - `stage.parenting=1` → 含 `unsafe_for_stage_parenting`
  - `stage.unknown=1` → 含 `unsafe_for_stage_unknown` 且带跨维度谓词

---

## C. 接入与文档回写（后续任务）

- [ ] Onboarding 提交问卷处接入本模块（调用 `buildUserProfileFromQuestionnaire`）
  - **验收**：提交后能生成并持久化/上传画像对象（具体存储策略由上层决定）
- [ ] 在推荐/Push/Widget 侧消费 `hard_rules`
  - **验收**：Hard Filter 在打分前执行；且能使用跨维度谓词表达 unknown+育儿压力强个性化禁推

