fix:更新算法

This commit is contained in:
吕新雨
2026-02-02 11:22:35 +08:00
parent 814b96edb6
commit 58d17fc39f
27 changed files with 3286 additions and 62 deletions

View File

@@ -0,0 +1,184 @@
# User Profile Scoring技术计划 · V1.2
## 1. 计划目标
在**客户端TypeScript**实现一个可复用的“用户画像计算”模块:
- **输入**Onboarding 问卷答案(每题**可跳过**;字段可缺失/为 `null`
- **输出**:严格符合 `spec_kit/User Profile Scoring/spec.md`V1.2)的用户画像对象(含 `profile_answered`、四维度字段、`profile_confidence`
- **硬规则**:输出一份“下游可直接执行/可观测”的硬规则结果,用于推荐/Push/Widget 统一复用(对齐 V1.2 规则语义)
- **约束**:不抛异常、不报错;非法值按**跳过**处理
## 2. 默认技术决策(本计划采用)
- **实现位置**`client/src/features/userProfileScoring/`
- **语言**TypeScript纯函数为主便于测试与跨端复用
- **时间与可测性**:允许调用方注入 `generatedAt/now`,默认使用 `new Date()`,保证可回归测试
- **规则对齐来源**
- 画像与置信度:`设计说明文档/客戶端問卷打分規則.md`V1.2
- 下游过滤语义:`设计说明文档/個性化推薦算法規則.md`Hard Filter / `risk_flags` 语义)
## 3. 输入与输出契约V1.2,含跳过)
### 3.1 输入:问卷答案(允许部分缺失/跳过)
定义输入类型 `QuestionnaireAnswersV1_2`(所有字段可选;也允许显式 `null`
- `mom_stage?: "expecting" | "parenting" | "unknown" | null`
- `emotion?: "low" | "overwhelmed" | "tired" | "neutral" | "calm" | "joyful" | null`
- `context?: "family" | "work" | "relationship" | "friends" | "health" | null`
- `need?: "emotional_support" | "parenting_pressure" | "self_worth" | "anxiety_relief" | "rest_balance" | null`
非法值处理策略:
- 字段存在但值不在枚举内:按**跳过**处理(归一化为 `undefined`),不抛错
### 3.2 输出:用户画像(严格对齐 spec.md V1.2
输出 `UserProfileV1_2`,满足 `spec.md` 的字段与取值:
- `profile_version: "v1.2"`
- `profile_source: "questionnaire"`
- `profile_generated_at: string`ISO8601
- `profile_confidence: number`01
- `profile_answered: { stage: boolean; emotion: boolean; context: boolean; need: boolean }`
- `stage: { expecting?: 0|1; parenting?: 0|1; unknown: 0|1 }`
- 注:题目跳过时按安全策略输出 `unknown=1`
- `emotion_score: number | null`
- `context: Record<string, 1>`(稀疏 one-hot题目跳过时为 `{}`
- `need: Record<string, 1>`(稀疏 one-hot题目跳过时为 `{}`
### 3.3 额外输出:硬规则(供下游直接执行/打点)
`spec.md` 要求“向下游暴露可直接执行的硬规则结果”,但不强制字段结构。本计划采用以下扩展字段(不破坏 `spec.md` 核心结构):
- `rule_hits: string[]`
- 仅用于可观测/回归测试(例如 `["unsafe_for_emotion_low"]`
- `hard_rules: { ... }`
- `forbidden_risk_flags: string[]`(对齐内容侧/推荐侧 `risk_flags` 名称)
- `forbidden_content_predicates: Array<{ id: string; when_user: object; forbid_content: object }>`
- 用于表达“需要同时看用户与内容字段才能执行”的规则(例如 unknown + 育儿压力 + 强个性化)
## 4. 跳过题目的处理策略V1.2:安全优先 + 可观测)
> 目标:题目跳过时不报错;画像仍可计算;并通过 `conf_U` 与下游 `P_uncertainty` 思路自动降个性化/降风险。
- **mom_stage 跳过**
- 输出:`stage.unknown = 1`
- `profile_answered.stage = false`
- **emotion 跳过**
- 输出:`emotion_score = null`(不强行填 0.6,避免制造“伪精确画像”)
- `profile_answered.emotion = false`
- **context 跳过**
- 输出:`context = {}`
- `profile_answered.context = false`
- **need 跳过**
- 输出:`need = {}`
- `profile_answered.need = false`
## 5. 画像计算规则(映射 + 置信度 + 硬规则)
### 5.1 映射规则(对齐 `客戶端問卷打分規則.md` V1.2
- `mom_stage``stage.*`one-hot若跳过则 `unknown=1`
- `emotion``emotion_score`
- `low=0.0``overwhelmed=0.2``tired=0.4``neutral=0.6``calm=0.8``joyful=1.0`
- 若跳过:`emotion_score = null`
- `context` → 稀疏 one-hot例如 `work``{ work: 1 }`;若跳过:`{}`
- `need` → 稀疏 one-hot例如 `rest_balance``{ rest_balance: 1 }`;若跳过:`{}`
### 5.2 置信度(`profile_confidence / conf_U`计算V1.2
时间衰减(与 `spec.md` 一致):
- 07 天:`conf_time = 1.0`
- 730 天:线性衰减到 `0.7`
- 30 天以上:`conf_time = 0.5`
作答完整度因子(对齐 `客戶端問卷打分規則.md` V1.2
- `answered_count = Σ I[profile_answered.* = true]``total_questions = 4`
- `completion = answered_count / total_questions`
- `completion_factor = 0.5 + 0.5 * completion`
- `profile_confidence = clamp(conf_time * completion_factor, 0.2, 1.0)`
### 5.3 硬规则输出(对齐 V1.2
本模块输出 `rule_hits``hard_rules`,用于下游先过滤、后打分:
1) **按用户状态映射到内容 `risk_flags` 的禁推**
-`stage.unknown = 1`
- `rule_hits += ["unsafe_for_stage_unknown"]`
- `hard_rules.forbidden_risk_flags += ["unsafe_for_stage_unknown"]`
-`stage.parenting = 1`
- `rule_hits += ["unsafe_for_stage_parenting"]`
- `hard_rules.forbidden_risk_flags += ["unsafe_for_stage_parenting"]`
-`emotion_score !== null``emotion_score <= 0.2`
- `rule_hits += ["unsafe_for_emotion_low"]`
- `hard_rules.forbidden_risk_flags += ["unsafe_for_emotion_low"]`
2) **跨维度产品规则(需要同时看用户与内容字段)**
-`stage.unknown = 1`
- 增加一个可执行谓词,表达“禁推 育儿压力且高个性化内容”:
- `when_user`: `{ stage_unknown: true }`
- `forbid_content`: `{ need: "parenting_pressure", personalization_power: 1 }`
- 该规则由下游在内容侧字段存在时执行(对齐 `spec.md` 7 与 `客戶端問卷打分規則.md` 6 的 V1.2 口径)
> 注:本模块不判断内容的 `personalization_power`(属于内容侧),只输出“可执行条件”。
## 6. 文件与目录规划(客户端)
`client/src/` 新增/调整:
- `client/src/features/userProfileScoring/types.ts`
- `QuestionnaireAnswersV1_2` / `UserProfileV1_2` / 枚举联合类型
- `client/src/features/userProfileScoring/scoring.ts`
- `buildUserProfileFromQuestionnaire(answers, options): UserProfileV1_2 & { rule_hits; hard_rules }`
- `computeTimeConfidence(generatedAt, now): number`
- `normalizeAnswers(raw): QuestionnaireAnswersV1_2`(非法值 → 跳过)
- `computeProfileAnswered(normalized): profile_answered`
- `computeProfileConfidence(conf_time, profile_answered): number`
- `client/src/features/userProfileScoring/index.ts`
- 对外导出(供 Onboarding/推荐/Push 调用)
## 7. 测试计划(客户端可回归)
若工程未提供 `test` 脚本,建议补齐最小单元测试能力(只测纯函数):
- 新增 `vitest`(或 `jest`)为 devDependencies并在 `client/package.json` 增加 `test` 脚本
-`buildUserProfileFromQuestionnaire` 增加用例(与 `spec.md` 第 10 章一致):
- 映射正确性(完整作答)
- 置信度时间衰减07/730/30+
- 硬规则触发(`emotion_score<=0.2``stage.unknown``stage.parenting`
- 跳过场景V1.2
- 全部跳过(输入 `{}``profile_answered` 全 false`stage.unknown=1``emotion_score=null``context/need={}``profile_confidence=0.5`(当 conf_time=1 时)
- 仅回答 1 题:验证 `completion_factor` 与 clamp最小 0.2)逻辑正确
## 8. 实施步骤
1.`client/src/features/userProfileScoring/` 增加类型定义与常量映射表V1.2
2. 实现答案归一化(非法值按跳过;支持 `null/undefined`
3. 实现画像生成主函数(含 `profile_answered`、元信息生成、置信度计算)
4. 实现硬规则命中与结构化输出(`forbidden_risk_flags` + `forbidden_content_predicates`
5. 补齐最小测试框架与单测(仅纯函数)
6. Onboarding 流程完成处接入(后续任务):问卷提交后生成画像对象;持久化/上传由上层决定
## 9. 验收标准
- 任意问卷答案输入(含任意题跳过/非法值),都能生成符合 `spec.md`V1.2)结构的用户画像对象(不抛错)
- 输出包含:
- V1.2 元信息(`profile_version/source/generated_at/confidence/profile_answered`
- 四维度字段(`stage/emotion_score/context/need`;允许跳过后的 `null/{}`
- `rule_hits``hard_rules`(供下游执行 Hard Filter 与打点)
- 置信度:
- 时间衰减符合 `spec.md` 6.2
- 跳过越多 `profile_confidence` 越低(按 V1.2 完整度因子),并被 clamp 到 `[0.2, 1.0]`
## 10. 后续演进(不阻塞 V1.2
- 支持 `context/need` 多选(输出仍为 one-hot可多个 key=1
- 引入 `profile_source="mixed"`:行为信号修正 `emotion_score` 或补全缺失维度(保留 questionnaire 原始画像用于可观测)
- 将硬规则输出进一步与推荐系统规则引擎 schema 对齐(使 `forbidden_content_predicates` 可直接落入规则引擎)

View File

@@ -4,13 +4,76 @@
本模块用于将 Onboarding 问卷答案转换为标准化、可计算、可版本化的**用户画像User Profile**,供其他模块(个性化推荐 / Push 推送排序 / 内容过滤)统一调用。
### 1.1 问卷信息V1.2,供外部 UI 实现;支持可跳过)
本模块**不接入 UI**,仅定义“问卷答案 → 用户画像”的规则。外部 UI或服务端只需在用户完成 Onboarding 问卷后,按以下题目将答案编码为枚举值并调用本模块。
V1.2 题目集合(每题**可跳过**;作答时为单选;**中英对齐**
#### Q1 母职阶段(`mom_stage`
- 中文:你处于哪个母职阶段?
- English: Which stage of motherhood are you in?
| UI 展示(中文) | UI 展示English | 枚举值(提交给本模块) |
| --- | --- | --- |
| 备孕 / 怀孕 / 准备迎接宝宝 | Pregnant / Preparing | `expecting` |
| 已经在育儿阶段 | Parenting | `parenting` |
| 不想说 / 不希望被母职身份定义 | Prefer not to say | `unknown` |
#### Q2 当下情绪(`emotion`
- 中文:你现在感觉如何?
- English: How are you feeling right now?
| UI 展示(中文) | UI 展示English | 枚举值(提交给本模块) |
| --- | --- | --- |
| 低落 | Low | `low` |
| 过载 / 不堪重负 | Overwhelmed | `overwhelmed` |
| 疲惫 | Tired | `tired` |
| 还好 / 一般 | Okay | `neutral` |
| 平静 | Calm | `calm` |
| 愉悦 | Joyful | `joyful` |
#### Q3 主要触发场景(`context`
- 中文:这些感受主要受到什么影响?
- English: Whats been influencing how you feel?
| UI 展示(中文) | UI 展示English | 枚举值(提交给本模块) |
| --- | --- | --- |
| 家庭 | Family | `family` |
| 工作或学习 | Work or study | `work` |
| 亲密关系 | Relationship | `relationship` |
| 朋友 | Friends | `friends` |
| 健康 | Health | `health` |
#### Q4 最需要的支持(`need`
- 中文:你现在最需要哪一种支持?
- English: What kind of support do you need most right now?
| UI 展示(中文) | UI 展示English | 枚举值(提交给本模块) |
| --- | --- | --- |
| 情绪支持 | Emotional support | `emotional_support` |
| 育儿压力 | Parenting pressure | `parenting_pressure` |
| 自我价值 | Self-worth | `self_worth` |
| 缓解焦虑 | Anxiety relief | `anxiety_relief` |
| 休息与平衡 | Rest & balance | `rest_balance` |
说明:
- 外部 UI 可自由决定视觉样式,但建议文案**中英成对出现**并与上表一致;无论文案如何变化,**答案编码必须与上述枚举一致**,以保证跨端一致性与可回归测试。
- 每一题允许用户**跳过不答**;当题目被跳过时,外部调用可选择“不传该字段”或传 `null`(以具体接口约定为准),本模块将按第 6.1.1 节输出 `profile_answered` 并生成保守画像。
- 后续若扩展为多选(例如 `context/need` 多选),应遵循第 8 节兼容策略,保持 V1 输出结构可向后兼容。
该模块的设计原则:
- **不做心理诊断**:只描述「此刻允许被如何对待」
- 区分 **身份(离散)****状态(连续)**
- 允许「通用」与「高个性化」内容并存,并通过置信度控制“个性化力度”
来源规则文档:`设计说明文档/客戶端問卷打分規則.md`V1 / V1.1 工程补充
来源规则文档:`设计说明文档/客戶端問卷打分規則.md`V1 / V1.1 / V1.2
## 2. 模块定位与边界
@@ -37,38 +100,43 @@
### 4.1 输入(问卷答案)
V1 假设每题为**单选**
V1.2:每题**可跳过**;作答时为**单选**
- `mom_stage``expecting | parenting | unknown`
- `emotion``low | overwhelmed | tired | neutral | calm | joyful`
- `context``family | work | relationship | friends | health`
- `need``emotional_support | parenting_pressure | self_worth | anxiety_relief | rest_balance`
- `mom_stage?``expecting | parenting | unknown`
- `emotion?``low | overwhelmed | tired | neutral | calm | joyful`
- `context?``family | work | relationship | friends | health`
- `need?``emotional_support | parenting_pressure | self_worth | anxiety_relief | rest_balance`
> 说明:多选 `context/need` 可在后续版本扩展,但 V1 输出结构需保持可兼容升级。
> 说明:
>
> - 带 `?` 表示该字段允许缺失(题目跳过)。
> - 多选 `context/need` 可在后续版本扩展,但 V1 输出结构需保持可兼容升级。
### 4.2 输出(用户画像)
输出为一个 JSON 对象(或等价的 TypeScript 类型),包含:
- **元信息V1.1**
- `profile_version`: string`"v1.1"`
- **元信息V1.2**
- `profile_version`: string`"v1.2"`
- `profile_source`: `"questionnaire"`(后续可扩展 `"behavior"` / `"mixed"`
- `profile_generated_at`: stringISO8601 时间戳)
- `profile_confidence`: number01
- **四个维度V1**
- `stage`: one-hot
- `emotion_score`: number01
- `context`: one-hot
- `need`: one-hot
- `profile_answered`: object标记各题是否作答
- **四个维度V1V1.2 允许缺失/跳过)**
- `stage`: one-hot若题目跳过按安全策略输出 `unknown=1`
- `emotion_score`: number01`null`(题目跳过)
- `context`: one-hot 或空对象 `{}`(题目跳过)
- `need`: one-hot 或空对象 `{}`(题目跳过)
推荐的最小输出示例(结构示意):
```json
{
"profile_version": "v1.1",
"profile_version": "v1.2",
"profile_source": "questionnaire",
"profile_generated_at": "2026-01-30T00:00:00Z",
"profile_confidence": 1.0,
"profile_answered": { "stage": true, "emotion": true, "context": true, "need": true },
"stage": { "expecting": 0, "parenting": 1, "unknown": 0 },
"emotion_score": 0.4,
"context": { "work": 1 },
@@ -76,6 +144,22 @@ V1 假设每题为**单选**
}
```
题目跳过时的最小输出示例V1.2
```json
{
"profile_version": "v1.2",
"profile_source": "questionnaire",
"profile_generated_at": "2026-01-30T00:00:00Z",
"profile_confidence": 0.5,
"profile_answered": { "stage": false, "emotion": false, "context": false, "need": false },
"stage": { "unknown": 1 },
"emotion_score": null,
"context": {},
"need": {}
}
```
## 5. 画像字段定义V1
### 5.1 mom_stage母职阶段离散
@@ -89,6 +173,7 @@ V1 假设每题为**单选**
语义约束:
- `unknown` 的语义是:**不希望被母职身份定义**(不是“没有孩子”)
- 若题目被跳过:按安全策略输出 `stage.unknown = 1`,同时 `profile_answered.stage=false`
### 5.2 emotion当下情绪连续 01
@@ -105,7 +190,7 @@ V1 假设每题为**单选**
| Calm | emotion: calm | 0.8 |
| Joyful | emotion: joyful | 1.0 |
输出:`emotion_score ∈ [0,1]`
输出:`emotion_score ∈ [0,1]`;若题目被跳过则输出 `null`(或不输出该字段),并通过 `profile_answered.emotion=false` 表示“未作答”
### 5.3 context影响来源离散
@@ -117,6 +202,11 @@ V1 假设每题为**单选**
- Friends → `context.friends = 1`
- Health → `context.health = 1`
若题目被跳过:
- 输出 `context = {}`
- `profile_answered.context = false`
语义说明:
- `context` 不是情绪,而是情绪的“触发场景”
@@ -132,19 +222,38 @@ V1 假设每题为**单选**
- Anxiety relief → `need.anxiety_relief = 1`
- Rest & balance → `need.rest_balance = 1`
若题目被跳过:
- 输出 `need = {}`
- `profile_answered.need = false`
语义说明:
- `need` 是**推荐权重最高的维度**
- 表示“她现在最缺的是哪一种心理资源”
## 6. 元信息与置信度V1.1
## 6. 元信息与置信度V1.2
### 6.1 profile_version / source / generated_at
- `profile_version`:用于规则升级与兼容(建议从 `"v1.1"` 起步)
- `profile_version`:用于规则升级与兼容(建议从 `"v1.2"` 起步;本规范与规则文档已对齐 V1.2
- `profile_source`:固定为 `"questionnaire"`(后续可扩展)
- `profile_generated_at`:画像生成时间(用于推送降风险)
### 6.1.1 profile_answered支持题目可跳过V1.2
> 目的:区分“明确选择 unknown”与“题目被跳过导致 unknown/缺失”,并让下游在不确定时自动降个性化/降风险。
- `profile_answered`:记录每一题是否作答
- `stage/emotion/context/need``true/false`
输出约定与设计说明文档保持一致V1.2
- `mom_stage` 被跳过:按安全策略输出 `stage.unknown = 1`,同时 `profile_answered.stage=false`
- `emotion` 被跳过:`emotion_score` 输出 `null`(或不输出该字段),同时 `profile_answered.emotion=false`
- `context` 被跳过:`context` 输出为空对象 `{}`,同时 `profile_answered.context=false`
- `need` 被跳过:`need` 输出为空对象 `{}`,同时 `profile_answered.need=false`
### 6.2 profile_confidenceconf_U
默认规则(可配置参数):
@@ -155,6 +264,13 @@ V1 假设每题为**单选**
- 730 天:线性衰减到 `0.7`
- 30 天以上:`conf_U = 0.5`(除非用户重新做问卷或有行为信号更新)
V1.2 补充:叠加“作答完整度”(题目可跳过场景):
-`answered_count = Σ I[profile_answered.* = true]`,总题数 `total_questions = 4`
- `completion = answered_count / total_questions`
- `completion_factor = 0.5 + 0.5 * completion`
- `conf_U = clamp(conf_time * completion_factor, 0.2, 1.0)`(其中 `conf_time` 按时间衰减得到)
下游使用建议:
- Push 推送等高风险场景:当 `conf_U` 低时,应启用不确定性惩罚(例如 `P_uncertainty`)并限制个性化力度上限。
@@ -163,10 +279,11 @@ V1 假设每题为**单选**
本模块应向下游暴露“可直接执行”的硬规则结果(例如 `forbidden_tags``content_tone_blacklist`),以保证在任何排序之前先做安全过滤。
V1 核心禁推规则:
V1.2 核心禁推规则:
1. **`stage.unknown = 1`**
- 禁推:强指向育儿压力的内容(`need: parenting_pressure`
- 禁推:强指向育儿压力且高个性化的内容(用于避免“把她当妈妈/正在育儿”的对话
- 条件(由下游在内容侧判定):`need: parenting_pressure``personalization_power = 1`
2. **`stage.parenting = 1`**
- 禁推:明确怀孕 / 孕期 / 胎动等内容
3. **`emotion_score ≤ 0.2`low / overwhelmed**
@@ -188,3 +305,172 @@ V1 核心禁推规则:
- 输出包含元信息(版本、来源、时间、置信度)与四维度字段
- 硬规则输出可被推荐/推送模块直接用于过滤(先过滤、后打分)
## 10. 测试(输入题目答案 → 输出用户画像)
> 本模块作为**独立模块**暂不接入 UI外部 UI 只需按本规范提供枚举答案,本模块输出标准化用户画像对象。以下用例用于外部 UI / 服务端编写自动化测试,验证“输入答案 → 输出画像”稳定且可版本化。
### 10.1 测试约定(为可重复性而设)
- `profile_version`:固定为 `"v1.2"`
- `profile_source`:固定为 `"questionnaire"`
- `profile_generated_at`:测试用例中使用固定时间戳(避免随系统时间波动)
- `profile_confidence`
- 若“刚完成问卷”,测试时可令 `now == profile_generated_at`,期望 `profile_confidence = 1.0`
- 若测试时间衰减,则固定 `profile_generated_at``now`,按第 6.2 节规则计算期望值
### 10.2 字段映射用例(基础正确性)
#### 用例 A完整映射parenting + tired + work + rest_balance
输入(题目答案):
```json
{
"mom_stage": "parenting",
"emotion": "tired",
"context": "work",
"need": "rest_balance"
}
```
期望输出(用户画像;示例中 `profile_generated_at` 由测试固定注入):
```json
{
"profile_version": "v1.2",
"profile_source": "questionnaire",
"profile_generated_at": "2026-01-30T00:00:00Z",
"profile_confidence": 1.0,
"profile_answered": { "stage": true, "emotion": true, "context": true, "need": true },
"stage": { "expecting": 0, "parenting": 1, "unknown": 0 },
"emotion_score": 0.4,
"context": { "work": 1 },
"need": { "rest_balance": 1 }
}
```
#### 用例 Bunknown 语义(不希望被母职身份定义)
输入:
```json
{
"mom_stage": "unknown",
"emotion": "neutral",
"context": "relationship",
"need": "emotional_support"
}
```
期望输出(关键点:`stage.unknown = 1`
```json
{
"profile_version": "v1.2",
"profile_source": "questionnaire",
"profile_generated_at": "2026-01-30T00:00:00Z",
"profile_confidence": 1.0,
"profile_answered": { "stage": true, "emotion": true, "context": true, "need": true },
"stage": { "expecting": 0, "parenting": 0, "unknown": 1 },
"emotion_score": 0.6,
"context": { "relationship": 1 },
"need": { "emotional_support": 1 }
}
```
#### 用例 A2全部跳过V1.2 最小可计算画像)
输入(所有题都跳过;用“不传字段”的方式表示):
```json
{}
```
期望输出(关键点:`profile_answered` 全 false`stage.unknown=1``emotion_score=null``context/need={}`
```json
{
"profile_version": "v1.2",
"profile_source": "questionnaire",
"profile_generated_at": "2026-01-30T00:00:00Z",
"profile_confidence": 0.5,
"profile_answered": { "stage": false, "emotion": false, "context": false, "need": false },
"stage": { "unknown": 1 },
"emotion_score": null,
"context": {},
"need": {}
}
```
### 10.3 置信度衰减用例(时间维度)
> 本组用例只验证 `profile_confidence` 计算是否符合第 6.2 节;其余字段可沿用任意合法输入。
#### 用例 C07 天(不衰减)
- `profile_generated_at = 2026-01-01T00:00:00Z`
- `now = 2026-01-05T00:00:00Z`
期望:`profile_confidence = 1.0`
#### 用例 D730 天(线性衰减到 0.7
- `profile_generated_at = 2026-01-01T00:00:00Z`
- `now = 2026-01-11T00:00:00Z`(第 10 天)
期望:`profile_confidence ≈ 1.0 - 0.3 * (10 - 7) / (30 - 7) ≈ 0.9609`
> 测试建议:用浮点容差断言(例如 `abs(actual - expected) <= 1e-3`)。
#### 用例 E30 天以上(固定为 0.5
- `profile_generated_at = 2026-01-01T00:00:00Z`
- `now = 2026-02-15T00:00:00Z`
期望:`profile_confidence = 0.5`
### 10.4 硬规则触发用例(可选,但建议覆盖)
> 本节只验证“规则是否被触发”。由于第 7 节目前只规定了规则语义,未强约束硬规则输出字段结构,测试可用两种方式之一:
>
> - 方式 1若实现提供 `hard_rules`(或同义字段),则断言其包含对应禁推项
> - 方式 2实现不提供结构化 `hard_rules` 时,至少应提供可观测的 `rule_hits`(字符串数组)用于下游与回归测试
#### 用例 Femotion_score ≤ 0.2 禁推 joyful 调性
输入:
```json
{
"mom_stage": "expecting",
"emotion": "overwhelmed",
"context": "health",
"need": "anxiety_relief"
}
```
期望:
- `emotion_score = 0.2`
- 规则命中:`emotion_score ≤ 0.2` → 禁推高能量庆祝型joyful 调性)内容
#### 用例 Gstage.unknown 禁推“育儿压力 + 高个性化”内容
输入:
```json
{
"mom_stage": "unknown",
"emotion": "calm",
"context": "family",
"need": "parenting_pressure"
}
```
期望:
- `stage.unknown = 1`
- 规则命中:`stage.unknown = 1` → 下游禁推满足以下条件的内容:
- `need: parenting_pressure`
- `personalization_power = 1`

View File

@@ -0,0 +1,143 @@
# 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_answeredV1.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 一致):
- 07 天1.0
- 730 天:线性衰减到 0.7
- 30 天以上0.5
- **验收**:给定固定时间输入,输出符合分段规则(允许浮点误差)
### A6. 计算 profile_confidenceV1.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+育儿压力强个性化禁推