diff --git a/docs/开发任务包/T15-报告域骨架.md b/docs/开发任务包/T15-报告域骨架.md new file mode 100644 index 0000000..a4db47a --- /dev/null +++ b/docs/开发任务包/T15-报告域骨架.md @@ -0,0 +1,80 @@ +# T15 · 报告域骨架——触发/配额/异步链路/中间态恢复 + +> 对应任务:`T-P3-07`(触发与频控、配额生效)、`T-P3-09` 🔴(失败不需重新上传照片——中间态落库+分步恢复)、架构 §7.1 步骤 1~7 与 22~24 | 分支:`task/t15-report-skeleton` | worktree:`/home/xsl/meida_backend-t15` +> 先读《README.md(协作规程)》,遵守全部纪律。**第四批三个任务(T15/T16/T17)拼成 M3 报告流水线:你是骨架与编排,T16 供视觉/诊断阶段,T17 供文案/校验阶段——你们互不改对方文件,靠协调提交定好的阶段注册机制衔接。** + +## 1. 目标 + +报告域的编排骨架:`POST /reports`(资格校验→终身配额预扣→建 tk_task→入 cpu 队列)→ worker 执行**可注入的流水线**(阶段动态装配、中间态逐段落库、视觉结果可复用不重跑)→ 失败自动退款;`GET /reports`、`GET /reports/{id}`、`POST /reports/{id}/regenerate`(走 `report_regen` 月配额)。 + +## 2. 已就绪的契约(协调提交已提供,禁改 models/migrations/config/errors) + +### 表(迁移 `0006`,ORM `RpReport`/`RpInput`/`RpShare`,先读 models.py 核对) + +- `rp_report`:`report_id` PK、`user_id`、`tier`('basic'/'full')、`status`('pending'/'running'/'succeeded'/'failed')、`version`、`result` JSONB(**流水线中间态与最终产物都写这里**,按 stage 键增量更新)、`model_info` JSONB、`cost_cents` Numeric、`created_at/completed_at`;Index(user_id,created_at) +- `rp_input`:`input_id` PK、`report_id`、`questionnaire` JSONB、`photo_meta` JSONB、**`derived_features` JSONB**(视觉中间态——T16 的 vision 阶段写,**已存在则跳过视觉**=T-P3-09 的核心);UNIQUE(report_id) +- `rp_share`:`share_id` PK、`report_id`、`share_token` UNIQUE、`expire_at`、`view_count`、`revoked`(V1 只建结构,路由不做) + +### 错误码:`REPORT_NOT_FOUND = 40420`(404)、`REPORT_ALREADY_EXISTS = 40920`(409「本期报告已生成过」,hint「每 30 天可重新生成一次」——**文案不含价格字样**) + +### 配置:`report_pipeline_stages: list[str]`(**dotted path 列表**,默认 `["packages.report.vision_stage:VisionStage","packages.report.diagnosis_stage:DiagnosisStage","packages.report.copy_stage:CopyStage","packages.report.validation_stage:ValidationStage"]`)、`report_vision_provider: str = "deterministic"`、`report_tier: str = "basic"` + +### 阶段注册机制(协调提交的 `packages/report/pipeline.py` 骨架里有接口约定,你实现) + +- `Stage` Protocol:`name: str` + `async def run(ctx: ReportContext) -> None`(阶段把产物写进 `ctx.report.result[self.name]`,异常即流水线失败); +- `ReportContext`(frozen dataclass):`report: RpReport`、`rp_input: RpInput`、`session`、`settings`、`gateway: LLMGateway | None`(worker 装配时注入——**T17 的阶段要用**); +- `assemble_stages(settings) -> list[Stage]`:用 **importlib 按 dotted path 动态导入**(这是刻意的插件式装配:T16/T17 各自建阶段模块,不碰你的文件)。**模块/类不存在时抛 `AppError(CONFIG_ERROR)` 并列出缺失项**——绝不静默跳过(静默=流水线空跑还扣配额)。动态导入绕过 import-linter 静态检查,注释写明这一点与理由。 + +### 复用设施(先读再用) + +- `BillingService.reserve/commit/refund`(quota_key:首次 `report_basic`(lifetime)、重新生成 `report_regen`(month));幂等键沿用 `reserve_idempotency_key` 风格; +- `TaskService.submit(...)`(幂等键=`request_key`)+ `tk_task` 状态机;celery `task_routes` 已预设 `meida.report.*`→cpu; +- 参考现成范式:`apps/api/routers/tasks.py` 的 ping 投递 + `apps/worker/tasks.py` 的 `_run_ping`(asyncio.run 包装)。 + +## 3. 实现要点 + +允许改动:`packages/report/**`(pipeline.py、service.py、repo.py、schemas.py、`__init__.py`、README.md)、`apps/api/routers/reports.py`(填充协调提交的空桩)、`apps/worker/report_tasks.py`(新建)、测试文件。**vision_stage.py / diagnosis_stage.py / copy_stage.py / validation_stage.py 四个文件归 T16/T17,禁建禁改**——你的流水线只认配置里的 dotted path。 + +1. **pipeline.py**:Stage Protocol、ReportContext、assemble_stages(动态导入+缺失即 CONFIG_ERROR)、`run_pipeline(ctx, stages)`——逐阶段执行,每阶段完成后 `ctx.report.result[stage.name] = {...}` 并 **flush 落库**(中间态持久化);阶段异常:tk_task→failed + refund + `rp_report.status='failed'` + error 写 result,**异常上抛给 celery 重试逻辑前先保证退款事务提交**(参考 agent service 的事务纪律)。 +2. **service.py** `ReportService`: + - `create_report(user_id, questionnaire, photo_object_key, request_key)`:查该用户已 succeeded 的 basic 报告 → 有则 40920(**首次终身 1 次的资格语义**,FR 定义见需求文档 §2.1);reserve(report_basic, amount=1) → 建 rp_report(pending)+rp_input → tk_task(idempotency_key=request_key) → 入队 → 返回 task_id+report_id。**reserve 与 tk_task 同事务,入队在事务提交后**(队列消息不能早于数据落库); + - `regenerate(report_id, request_key)`:目标报告须 succeeded;reserve(report_regen) → 复用 rp_input(**derived_features 保留——这就是"失败/重生成不需重新上传"**)→ version+1 新报告行; + - `get_report / list_reports`(归属校验,result 透出但 failed 只透错误摘要); + - **幂等**:同 request_key 二次提交返回同一 task_id,不重复预扣(tk_task 幂等键 + reserve 幂等键双重保证)。 +3. **worker/report_tasks.py**:`@celery_app.task(name="meida.report.generate", bind=True, max_retries=1)`——开 session、装配 gateway(worker 侧 `build_llm_gateway(settings)`——deps.py 是 apps.api 的,worker 里自建装配函数,不 import apps.api)、`assemble_stages`、执行 run_pipeline、成功后 commit 配额 + status=succeeded + completed_at + 成本聚合写 `model_info`。**重试语义**:重试时若 `rp_input.derived_features` 已存在,视觉阶段自然跳过(T16 实现),网络类异常才重试。 +4. **路由 reports.py**(prefix `/reports`):`POST /`(body:questionnaire dict + photo_object_key + 可选 request_key)、`GET /`、`GET /{id}`、`POST /{id}/regenerate`。响应模型放 `packages/report/schemas.py`。 +5. **测试钩子**:集成测试不启真 celery——直接调 `run_pipeline`(注入 Fake 阶段)+ 独立测 celery 任务函数(`task.run()` 直调)。**先看 tests/integration/test_task_bonus.py 里已有的 dev-token 与 app fixture 模式。** + +## 4. 测试要求 + +- 单测 `tests/unit/test_report_pipeline.py`:assemble_stages 缺失模块 → CONFIG_ERROR 且列出缺失项;run_pipeline 两阶段顺序执行、产物按键写入、第二阶段抛错 → 状态 failed + 已完成阶段产物仍在。 +- 集成 `tests/integration/test_report_flow.py`(新文件,Fake 阶段注入——把配置里的 stages 用 monkeypatch 换成文件内定义的两个 Fake 阶段): + 1. dev-token → POST /reports → 200,task/report id;tk_task PENDING、bl_ledger reserved 一行(report_basic lifetime); + 2. 直调 worker 函数(Fake 阶段成功)→ rp_report succeeded、result 含两个阶段键、ledger committed; + 3. **重复同 request_key → 同 task_id、无第二笔 reserve**; + 4. **首次资格**:succeeded 后再 POST /reports → 40920; + 5. **失败退款**:Fake 阶段二抛错 → status failed、ledger 出现 refund、**rp_input.derived_features 保留**(Fake 阶段一写的); + 6. regenerate:走 report_regen 月配额、version+1、复用 derived_features。 + +## 5. 禁改清单 + +通用禁改之外:`packages/report/{vision,vision_stage,diagnosis_stage,copy,copy_stage,validation,validation_stage,dimensions}.py`(T16/T17 所有)、全部已有测试、`apps/worker/tasks.py`。 + +## 6. 验收标准(T-P3-07/09) + +1. 首次终身资格校验与 40920;regenerate 走月配额; +2. 幂等提交不重复扣减; +3. 阶段产物逐段落库;失败自动退款且视觉中间态保留(重试/重生成不重跑视觉的结构保证); +4. 全部检查绿(mypy 范围:T6 同口径 + `packages/report`)。 + +## 7. 提交信息 + +``` +feat(report): orchestration skeleton with staged pipeline and refund-on-failure + +POST /reports 骨架:资格校验→终身配额预扣→tk_task→cpu 队列;动态装配 +流水线(dotted-path 注册、缺阶段即 CONFIG_ERROR);中间态逐段落库、失败 +退款、derived_features 跨重试/重生成复用(T-P3-07/09)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T16-报告诊断装配.md b/docs/开发任务包/T16-报告诊断装配.md new file mode 100644 index 0000000..9e4f6f1 --- /dev/null +++ b/docs/开发任务包/T16-报告诊断装配.md @@ -0,0 +1,91 @@ +# T16 · 报告流水线·视觉分析与 32 维诊断装配(mock vision) + +> 对应任务:`T-P3-01`(各项返回置信度与质量标记)、`T-P3-06` 🔴(**32 维度**每维四要素完整,缺失显式标注)| 分支:`task/t16-report-diagnosis` | worktree:`/home/xsl/meida_backend-t16` +> 先读《README.md(协作规程)》,遵守全部纪律。**第四批拼流水线:T15 定 Stage 接口(协调提交的 pipeline 骨架),你供视觉与诊断两个阶段,文件名与类名已由 T15 的任务契约定死——按下面 §2 的 dotted path 精确落位。** + +## 2. 你要创建的文件(与流水线的挂点,禁差分毫) + +| 文件 | 导出 | 挂进流水线的名字 | +| --- | --- | --- | +| `packages/report/dimensions.py` | `DIMENSIONS`(32 项冻结结构)等 | —(纯字典) | +| `packages/report/vision.py` | `VisionProvider` Protocol、`DeterministicVisionProvider`、`build_vision_provider(settings)` | —(被 vision_stage 用) | +| `packages/report/vision_stage.py` | `class VisionStage` | `packages.report.vision_stage:VisionStage` | +| `packages/report/diagnosis_stage.py` | `class DiagnosisStage` | `packages.report.diagnosis_stage:DiagnosisStage` | + +Stage 接口以协调提交的 `packages/report/pipeline.py` 骨架 docstring 为准(`name` + `async run(ctx)`;`ctx.report.result[stage.name]` 写产物;`ctx.rp_input.derived_features` 是你的中间态位)。 + +## 3. 维度字典(dimensions.py——本任务的事实核心) + +**必读**:`G:\meida\docs\美学报告-需求文档.md` 的 **§4 诊断维度字典**(32 行表)。逐行编码成: + +```python +@dataclass(frozen=True, slots=True) +class DimensionSpec: + number: int # 1~32(33/34 已永久废弃,不复用) + key: str # 稳定英文键,如 body_shape / season_code / face_shape + name_cn: str + source: str # questionnaire | vision | color | kb | rule | llm(按表「数据来源」列归类) + basic_coverage: str # "full" | "label" | "partial" | "none"(按「初级」列的 ✅/标签/部分/❌) +``` + +- `DIMENSIONS: tuple[DimensionSpec, ...]` 必须恰好 32 项、number 1~32 连续;模块常量 `BASIC_FULL_KEYS`(初级 full 覆盖集,验收「初级 21/32」口径的机器可读形态——**以你逐行统计为准并写注释**)。 +- key 命名参考 kb/记忆系统已有词汇(body_shape、face_shape、season_code…),全库一致。 + +## 4. 视觉分析(vision.py + vision_stage.py) + +1. **`VisionProvider` Protocol**:`async def analyze(photo_meta: dict, questionnaire: dict) -> dict`(返回派生特征 dict:体型、脸型、肤色 Lab 采样、量感/直曲粗判……**形状由你定,写进 docstring**——这是 T9 SPIKE 之后的正式接口位,真实 RTMPose/BiRefNet 实现后续替换)。 +2. **`DeterministicVisionProvider`**:不读照片字节(V1 骨架根本没有照片下载步骤),从 `questionnaire` + `photo_meta["object_key"]` 的 sha256 派生**确定性**特征(同输入同输出——黄金集与集成测试才能断言)。特征生成规则写注释:哪个问题驱动哪个维度、哈希驱动哪些枚举选择。 +3. `build_vision_provider(settings)`:按 `settings.report_vision_provider` 选择(当前只认 `"deterministic"`,其它 → `AppError(CONFIG_ERROR)`)。 +4. **`VisionStage`**(name=`"vision"`):`ctx.rp_input.derived_features` **已存在且非空 → 跳过**(写 `ctx.report.result["vision"] = {"skipped": true, "reused": true}`——这就是 T-P3-09 的实现点);否则 provider.analyze → 写 `derived_features` 并 flush → `result["vision"] = {"status": "ok", "dimensions_touched": [...]}`。质量标记:photo_meta 里可选 `quality_flags`(模糊/背光等,mock 下默认空列表)透传进产物(T-P3-01 的形状)。 + +## 5. 诊断装配(diagnosis_stage.py) + +`DiagnosisStage`(name=`"diagnosis"`): +1. 输入 = questionnaire(`ctx.rp_input.questionnaire`)+ derived_features;**逐个维度**产四要素(T-P3-06 验收原文): + +```python +@dataclass(frozen=True, slots=True) +class DimensionResult: + number: int + key: str + value: Any # 结论值(枚举/数值/列表) + confidence: float # 0~1:问卷自评 0.6、确定性视觉 0.85、规则推导 0.9(口径写注释,对齐记忆系统 §3.5) + source: str # questionnaire | vision | rule | derived + evidence: str # 一句人话依据(将出现在报告里) + rule_hits: list[str] # 命中的 rule_key + status: str # "ok" | "skipped"——**缺失/输入不足的维度必须显式 skipped + 原因,不允许缺键**(验收红线) +``` + +2. 数据流:问卷字段直接映射(性别/身高体重/职业/生日/核心诉求/场景权重/心理风格/偏好禁忌…);视觉字段映射(体型/脸型/肤色档位/季型/量感/直曲/基因风格…);**规则引擎联动**:构造 `RuleContext`(body_shape/face_shape/features/goals…)→ `RulesService` 载入 published 规则 → `evaluate()` → verdict 的 recommend/avoid 聚合进「核心诉求/偏好与禁忌」相关维度的 rule_hits 与 evidence。规则服务接口以 `packages/rules/service.py` 实际代码为准。 +3. 产物:`ctx.report.result["diagnosis"] = {"dimensions": [32 个 DimensionResult 的 dict], "coverage": {"basic_full": N, "total": 32}}`;序列化用 dataclass `asdict`。 +4. **色板维度(#19/21)**:V1 骨架从 `kb_color` 读该季型 base/accent/avoid 各取若干(库空则该维度 skipped+「色板未录入」——**显式缺失优于编造**,这正是 skipped 语义的用例)。 + +## 6. 测试要求 + +- 单测 `tests/unit/test_report_dimensions.py`:32 项、number 连续、33/34 不存在、basic_coverage 枚举合法、`BASIC_FULL_KEYS` 与逐行统计一致。 +- 单测 `tests/unit/test_report_vision.py`:确定性(同输入同输出)、不同问卷驱动不同体型、工厂未知名拒绝。 +- 单测 `tests/unit/test_report_diagnosis.py`:四要素齐全性——**构造故意缺问卷+缺视觉的场景,断言缺失维度 status=skipped 且 32 键全在**;规则命中进 rule_hits(用内置最小规则集,不连库——RulesService 若需 DB,把规则求值抽成可注入纯函数 `evaluate_dimensions(ctx_data, verdict)`)。 +- 集成 `tests/integration/test_report_stages.py`(新文件,**只测你的两个阶段,不测整条流水线**——T15 的集成已覆盖编排):建 rp_report/rp_input + 种 2 条 published 规则 → VisionStage → DiagnosisStage 断言 derived_features 落库、32 维全键、规则 rule_hits 出现、kb_color 为空时色板维度 skipped。 + +## 7. 禁改清单 + +通用禁改之外:`packages/report/` 除你四个文件外全部(pipeline/service/repo/schemas 归 T15,copy*/validation* 归 T17)、`apps/**`、全部已有测试。 + +## 8. 验收标准(T-P3-01/06) + +1. `DIMENSIONS` 与需求文档 §4 逐行一致(32 项、编号连续、初级覆盖口径机器可读); +2. 32 维四要素全量输出,缺失显式 skipped——**任何输入组合下 result["diagnosis"]["dimensions"] 恒 32 键**(单测证明); +3. 视觉中间态复用:derived_features 存在时 vision 阶段 skipped/reused; +4. 规则引擎硬结论进入 rule_hits/evidence; +5. 全部检查绿(mypy 范围同 T15 + 你的文件;`packages/report` 整包进列表)。 + +## 9. 提交信息 + +``` +feat(report): deterministic vision provider and 32-dimension diagnosis assembly + +dimensions.py 逐行编码需求文档 §4(32 项四要素、缺失显式 skipped)+ +VisionProvider 接口位与确定性实现 + 规则引擎联动 rule_hits(T-P3-01/06)。 + +偏差说明:<如有写在这里,无则写"无"> +``` diff --git a/docs/开发任务包/T17-报告文案与校验.md b/docs/开发任务包/T17-报告文案与校验.md new file mode 100644 index 0000000..e2f0eeb --- /dev/null +++ b/docs/开发任务包/T17-报告文案与校验.md @@ -0,0 +1,68 @@ +# T17 · 报告流水线·LLM 文案与生成后校验 + +> 对应任务:`T-P3-10`(文案结论与诊断值一致,无自相矛盾)、`T-P3-11` 🔴(**硬性禁忌违反数 = 0**;贬损词零通过)、架构 §7.1 步骤 17~19(「生成后校验不可跳过:校验失败最多重试 2 次,仍失败降级为规则直出模板文案,绝不放行违规内容」)| 分支:`task/t17-report-copy` | worktree:`/home/xsl/meida_backend-t17` +> 先读《README.md(协作规程)》,遵守全部纪律。**第四批拼流水线:T15 定 Stage 接口,你供文案与校验两个阶段,文件名与类名已定死。** + +## 2. 你要创建的文件(挂点,禁差分毫) + +| 文件 | 导出 | 流水线 dotted path | +| --- | --- | --- | +| `packages/report/copy.py` | 纯逻辑:结论→文案素材、模板降级渲染 | — | +| `packages/report/copy_stage.py` | `class CopyStage` | `packages.report.copy_stage:CopyStage` | +| `packages/report/validation.py` | 纯逻辑:贬损词表、一致性校验器 | — | +| `packages/report/validation_stage.py` | `class ValidationStage` | `packages.report.validation_stage:ValidationStage` | + +Stage 接口以协调提交 `packages/report/pipeline.py` 骨架为准;`ctx.gateway` 是 T15 注入的 `LLMGateway`(集成测试可换 FakeProvider——参考 `tests/integration/test_vertical_slice.py` 的做法)。 + +## 3. LLM 文案(copy.py + copy_stage.py) + +1. **Prompt**:`prompts/report_copy/v1.txt`(你新建,版本化目录规则见 `prompts/README.md`——`[system]` 段**不许含任何变量**(缓存前缀纪律,`validate_cache_prefix` 会拦);`[user]` 段放 `{dimensions_json}`)。system 定角色:形象顾问报告文案;红线写进 system:描述而非评判、不用绝对化用语、不出现贬损词、结论只能复述给定诊断值**不得新增维度结论**、简体中文。 +2. **结构化输出**:`gateway.complete(scene=Scene.REPORT_COPY, prompt_vars={...}, schema=ReportCopy, user_id=..., ref_type="report", ref_id=str(report_id))`——先读 `packages/llm/gateway.py` 的 `complete` 签名与 `Scene` 枚举、`packages/llm/routing.py` 里 REPORT_COPY 场景现有配置(temperature/prompt_id 指向你的 v1)。`ReportCopy(BaseModel)`:`summary: str`、`sections: list[Section]`(Section:`title/paragraphs: list[str]`)——**schema 宽进严出的取舍写注释**(字段越少结构化成功率越高,文案主体自由度放 sections 内)。 +3. **素材组装(纯函数 `build_prompt_material(result_diagnosis) -> str`)**:从 T16 的 `result["diagnosis"]["dimensions"]` 取 status=ok 的维度(skipped 不进 prompt——**LLM 看不到缺失维度就不会编造它**,这是防幻觉的第一道闸);序列化成紧凑 JSON(key+value+evidence,舍 confidence 小数位)。**总长控制**:拼好后若超 ~6000 字符,按 basic_coverage 优先级裁剪维度并记录 `dropped_keys`(对齐架构 §7.2 的裁剪思想:低重要度先丢,硬结论永不丢)。 +4. **`CopyStage`(name="copy")**:无 diagnosis 产物 → 抛错(流水线顺序错);调 gateway → `result["copy"] = {"summary": ..., "sections": [...], "model": completion.model, "prompt_version": ..., "dropped_keys": [...]}`。**这里不做校验**——校验是下一阶段的独立职责(T-P3-10 的"生成后校验"是流水线步骤 18,不是文案函数的内嵌断言)。 + +## 4. 生成后校验(validation.py + validation_stage.py) + +1. **贬损词表初版**(`validation.py` 模块常量 `PEJORATIVE_TERMS`,来源:`G:\meida\docs\合规与安全方案.md` §3「内容伦理红线」——**开工先读该节**,把"描述而非评判/不使用缺陷框架"翻译成具体词表:胖/丑/矮小/缺陷/土气/难看/显老/失败 等约 20~40 词 + 简单变形("有点胖"命中"胖"即可,不做词干)。词表带注释:这是初版内嵌版,正式版归合规词库任务(T-P0-07)。 +2. **一致性校验(纯函数)**: + - `check_pejorative(text) -> list[str]`(命中词列表); + - `check_conclusion_consistency(copy: ReportCopy, dimensions: list[dict]) -> list[str]`(不一致清单):初版三条规则——① 文案中出现的**体型/脸型/季型枚举词**必须是诊断值同款(诊断说 H 型、文案写 A 型 → 不一致);② 文案提到具体色值 `#RRGGBB` 必须在色板维度 value 集合内;③ 出现"建议"句式却无对应 rule_hits 支撑的维度结论 → 标记(宽松口径,注释说明误报代价 vs 漏报代价的取舍)。 + - `check_absolute_claims(text)`:绝对化用语(最/第一/彻底/百分百——prompt 禁了,校验兜底)。 +3. **`ValidationStage`(name="validation")**:取 copy + diagnosis → 全部校验 → + - **全过**:`result["validation"] = {"passed": true, "checks": {...计数}}`; + - **有违规**:**重试一次**——把违规清单作为修正指令追加(messages 上下文,参考 gateway 的结构化重试机制但不能用它内部的重试——那是解析层,这是语义层,两层独立)再生成 → 再校验 → 仍违规 → **降级模板**:`render_fallback_copy(dimensions, rule_hits)`(纯函数:按维度 evidence + rule explanation 拼模板文案,零 LLM)→ `result["validation"] = {"passed": true, "degraded": true, "violations_first_pass": [...]}`。**任何出口 passed 必须为 true 或阶段抛错——绝不放行未过校验的文案**(T-P3-11 红线)。 +4. 校验产物同段落库(`result["validation"]`),报告查询侧能看到是否降级。 + +## 5. 测试要求 + +- 单测 `tests/unit/test_report_copy.py`:素材组装(skipped 不进、超长裁剪记录 dropped、硬结论不裁)、模板降级渲染(有 rule_hits 时含 explanation)。 +- 单测 `tests/unit/test_report_validation.py`:词表命中(含"有点胖"变形)、一致性三条规则各一正一反、绝对化用语、重试后仍违规 → 降级路径纯函数级验证。 +- 集成 `tests/integration/test_report_copy_stage.py`(新文件,FakeProvider 替换 gateway——参考 test_vertical_slice): + 1. 干净 LLM(Fake 返回合规文案)→ copy + validation 两阶段 passed=true; + 2. **脏 LLM**(Fake 第一次返回含"微胖/最合适",修正后干净)→ 第一轮 violations 非空 → 重试通过; + 3. **持续脏 LLM**(恒返贬损词)→ 降级模板 passed=true degraded=true,**最终文案零贬损词**(T-P3-11 的黄金断言)。 +- 不测整条流水线(T15 集成已覆盖编排);只直接构造 ctx 调你的阶段。 + +## 6. 禁改清单 + +通用禁改之外:`packages/report/` 除你四个文件 + `prompts/report_copy/**` 外全部、`apps/**`、全部已有测试。**`prompts/report_copy/v1.txt` 归你**(新目录不与他人冲突)。 + +## 7. 验收标准(T-P3-10/11) + +1. 校验不可跳过:任何出口要么 passed 要么抛错; +2. 贬损词零通过(含持续脏 LLM 的降级路径断言); +3. 文案-结论一致性三条规则生效; +4. LLM 看不到 skipped 维度(防幻觉得到结构保证); +5. 全部检查绿(mypy 范围同 T15 + 你的文件)。 + +## 8. 提交信息 + +``` +feat(report): LLM copy generation with mandatory post-validation and fallback + +report_copy/v1 prompt(系统段零变量)+ 结构化 ReportCopy + 素材组装 +(skipped 不进 prompt/超长裁剪)+ 贬损词表/一致性/绝对化三重校验 + +重试一次后降级规则直出模板(T-P3-10/11)。 + +偏差说明:<如有写在这里,无则写"无"> +```