fix(docs): iteration-4/06 表名列名按正典回改,并暴露 first_attempt_at 缺口
CI / docs-build (push) Successful in 36s
CI / docs-build (push) Successful in 36s
用户拍板修改。原报告全篇使用本角色自拟名 `creation.generation_tasks` (11 处,含 4 段可执行 SQL),正典 patbond_postgresql.sql:605 实为 `creation.generation_jobs`;列名亦不一致。而该报告 §4.2 自称「对后端 工单的硬要求」——照字面实现会建出第二张表或写错列名。 改动: - 表名 11 处 → creation.generation_jobs(零残留;§4.2 修订说明中对旧名 的引用刻意保留) - 列名回改:queued_at→created_at、finished_at→completed_at、 failure_reason→error_code/error_message、last_attempt_at→started_at、 model_key→model_id(+generation_models.code / provider_model_snapshot)、 style_key→style_code_snapshot;input/output count 改为由 input_asset_id / output_asset_id 是否为 NULL 派生 - §4.2 新增 15 行逐列映射表,标明每列在正典中的对应与状态 - §5 未取证段更新:列名已回改,但 first_attempt_at 落地前两项 SQL 不可运行 **本次修改新发现的真问题(非改名)**:`first_attempt_at` 正典无对应列, 且 `ck_generation_jobs_state` 会主动销毁该信息——该 CHECK 规定 status='queued' 分支必须 started_at IS NULL,故失败重排队时首次尝试 时间不可恢复,导致「排队时长」与「纯执行耗时」永远无法分离(正是 §4.2 自己警告的后果)。处置:V6 增加 first_attempt_at timestamptz (仅首次进入 running 写入、重试不覆盖),并在状态机 CHECK 的 queued 分支中排除该列。这是 §4.2 硬要求中唯一需改动正典结构的一项,另一项 是 A2 拍板(参考图选填)导致的 input_asset_id 改可空。 验证:secret-scan exit 0;mkdocs build --strict exit 0。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
3. **白名单实际 41 条,不是 42**,根因是 `experiment_exposed` 被重复计数(community 域实为 18 条,加 platform 域 1 条共 19 条新增;文档记作「19 + experiment_exposed」= 20)。**无任何测试断言字典条数**,故漂移四份文档一路不可见。M4 前置项:补一个条数断言,见 §1.1、§7。
|
3. **白名单实际 41 条,不是 42**,根因是 `experiment_exposed` 被重复计数(community 域实为 18 条,加 platform 域 1 条共 19 条新增;文档记作「19 + experiment_exposed」= 20)。**无任何测试断言字典条数**,故漂移四份文档一路不可见。M4 前置项:补一个条数断言,见 §1.1、§7。
|
||||||
4. **eventVersion 定型为「每个事件自身的 props schema 版本」**(否决「字典世代」读法):客户端现行硬编码 1 在此读法下**本就正确**,零客户端改动、零历史回填;反之则全部历史行皆错且不可修复。配套给出 bump 规则与三层校验对齐方案,见 §2。
|
4. **eventVersion 定型为「每个事件自身的 props schema 版本」**(否决「字典世代」读法):客户端现行硬编码 1 在此读法下**本就正确**,零客户端改动、零历史回填;反之则全部历史行皆错且不可修复。配套给出 bump 规则与三层校验对齐方案,见 §2。
|
||||||
5. **`platform=linux` 整批 400 是契约明文行为,枚举排除 linux 是三层锁死的设计意图;缺陷在客户端**——`analytics_service.dart:99-100` 注释称「逐条 rejected(不影响客户端)」与实现相反:实际整批 400 且被 `uploadBatch` 判为永久拒绝**整批丢弃**,桌面端 100% 静默丢事件。处置为客户端本地短路 + 调试覆盖开关,不动枚举,见 §3。
|
5. **`platform=linux` 整批 400 是契约明文行为,枚举排除 linux 是三层锁死的设计意图;缺陷在客户端**——`analytics_service.dart:99-100` 注释称「逐条 rejected(不影响客户端)」与实现相反:实际整批 400 且被 `uploadBatch` 判为永久拒绝**整批丢弃**,桌面端 100% 静默丢事件。处置为客户端本地短路 + 调试覆盖开关,不动枚举,见 §3。
|
||||||
6. **服务端生成侧不建第二条埋点管道**:`platform.product_events` 的 `anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表;裁定以 `creation.generation_tasks` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接,见 §4.2。
|
6. **服务端生成侧不建第二条埋点管道**:`platform.product_events` 的 `anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表;裁定以 `creation.generation_jobs` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接,见 §4.2。
|
||||||
7. **M4 首个「实验」应是 A/A 基建验证空跑,真实 A/B 冻结设计待流量**。A/B 前置为 **5 绿 3 半**——分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**(本角色逐项 grep 取证),必须作为前置工单而非既有资产,见 §6。
|
7. **M4 首个「实验」应是 A/A 基建验证空跑,真实 A/B 冻结设计待流量**。A/B 前置为 **5 绿 3 半**——分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**(本角色逐项 grep 取证),必须作为前置工单而非既有资产,见 §6。
|
||||||
|
|
||||||
**事件增量**:新增 **8** 条(全部客户端侧,`creation` 域),既有事件补属性 **2** 处,废弃 **0** 条;字典 **41 → 49**。
|
**事件增量**:新增 **8** 条(全部客户端侧,`creation` 域),既有事件补属性 **2** 处,废弃 **0** 条;字典 **41 → 49**。
|
||||||
@@ -338,7 +338,7 @@ M4 的生成执行发生在 Worker(队列消费、租约、重试、超时)
|
|||||||
| A | Worker 走 `POST /api/v1/events`,`platform`/`anonymousId`/`sessionId` 填伪值 | **否决**。`platform` 只能填 android/ios(`V2:23`),会把服务端流量混进平台维度,**污染所有既有按平台切分的指标**;`session_id NOT NULL` 也只能造假 |
|
| A | Worker 走 `POST /api/v1/events`,`platform`/`anonymousId`/`sessionId` 填伪值 | **否决**。`platform` 只能填 android/ios(`V2:23`),会把服务端流量混进平台维度,**污染所有既有按平台切分的指标**;`session_id NOT NULL` 也只能造假 |
|
||||||
| B | 扩 `product_events`:platform 加 `server` 值、`anonymous_id`/`session_id` 改可空 | **否决**。需 Flyway 迁移改 CHECK + 放宽两个 NOT NULL + 5 份契约;放宽 NOT NULL 是**收紧不可逆**的反向操作,且此后每个下游查询都要处理 NULL 会话 |
|
| B | 扩 `product_events`:platform 加 `server` 值、`anonymous_id`/`session_id` 改可空 | **否决**。需 Flyway 迁移改 CHECK + 放宽两个 NOT NULL + 5 份契约;放宽 NOT NULL 是**收紧不可逆**的反向操作,且此后每个下游查询都要处理 NULL 会话 |
|
||||||
| C | 新建 `platform.server_events` 表,Worker 进程内直写(不走 HTTP) | 可行但**多余**,见下 |
|
| C | 新建 `platform.server_events` 表,Worker 进程内直写(不走 HTTP) | 可行但**多余**,见下 |
|
||||||
| **D** | **以 `creation.generation_tasks` 事实表为服务端指标权威**,经 `creationTaskId` 与客户端事件拼接 | **采纳** |
|
| **D** | **以 `creation.generation_jobs` 事实表为服务端指标权威**,经 `creationTaskId` 与客户端事件拼接 | **采纳** |
|
||||||
|
|
||||||
**采纳 D 的理由**:
|
**采纳 D 的理由**:
|
||||||
|
|
||||||
@@ -347,9 +347,29 @@ M4 的生成执行发生在 Worker(队列消费、租约、重试、超时)
|
|||||||
3. **零迁移增量**(相对方案 B/C):M4 本就要为 `creation` schema 建 V6 迁移,指标所需列并入即可,不额外建表、不改埋点管道。
|
3. **零迁移增量**(相对方案 B/C):M4 本就要为 `creation` schema 建 V6 迁移,指标所需列并入即可,不额外建表、不改埋点管道。
|
||||||
4. 客户端事件继续只承载**用户可见行为**(看到了什么、点了什么、等了多久),这是它的比较优势且不可被服务端替代(用户是否真的看到结果,服务端不知道)。
|
4. 客户端事件继续只承载**用户可见行为**(看到了什么、点了什么、等了多久),这是它的比较优势且不可被服务端替代(用户是否真的看到结果,服务端不知道)。
|
||||||
|
|
||||||
**因此这是对后端工单的硬要求**(本报告的指标口径依赖它,请在契约冻结时一并确认):`creation.generation_tasks` 须持久化以下列,且**终态行保留 ≥90 天不得物理删除**(软终态):
|
**因此这是对后端工单的硬要求**(本报告的指标口径依赖它,请在契约冻结时一并确认):`creation.generation_jobs` 须持久化以下列,且**终态行保留 ≥90 天不得物理删除**(软终态):
|
||||||
|
|
||||||
`id`(= `creationTaskId`)、`user_id`、`model_key`、`style_key`、`status`、`created_at`、`queued_at`、`first_attempt_at`、`last_attempt_at`、`finished_at`、`attempt_count`、`failure_reason`、`input_media_count`、`output_asset_count`、`idempotency_key`/`request_hash`。
|
> **列名已按正典对齐(2026-09-14 修订)**:本节初稿使用的是本角色自拟名(`generation_tasks` / `queued_at` / `first_attempt_at` / `finished_at` / `failure_reason` / `model_key`),与正典 `patbond_postgresql.sql:605` 的 `creation.generation_jobs` 不一致。下表为逐列映射,**其中一项正典确实缺失,已升级为 V6 的必加列**。
|
||||||
|
|
||||||
|
| 本报告所需事实 | 正典对应列 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id`(= `creationTaskId`) | `id` | ✅ 有 |
|
||||||
|
| `user_id` | `user_id` | ✅ 有 |
|
||||||
|
| `model_key` | `model_id` + 目录表 `generation_models.code`(或 `provider_model_snapshot`) | ✅ 有(分组需 join 或用快照列) |
|
||||||
|
| `style_key` | `style_code_snapshot` | ✅ 有(正典已反范式化,无需 join) |
|
||||||
|
| `status` | `status` | ✅ 有 |
|
||||||
|
| `created_at` | `created_at` | ✅ 有 |
|
||||||
|
| `queued_at` | **`created_at`**(行创建即 `status='queued'`,两者恒等) | ✅ 等价,不需新列 |
|
||||||
|
| **`first_attempt_at`** | **无对应列** | ❌ **缺失,V6 必须新增** —— 见下方说明 |
|
||||||
|
| `last_attempt_at` | `started_at`(当前尝试) | ✅ 有 |
|
||||||
|
| `finished_at` | `completed_at` | ✅ 有(改名) |
|
||||||
|
| `attempt_count` | `attempt_count` | ✅ 有 |
|
||||||
|
| `failure_reason` | `error_code` + `error_message` | ✅ 有(拆两列) |
|
||||||
|
| `input_media_count` | 由 `input_asset_id` 是否为 NULL 派生(0/1) | ✅ 可派生 |
|
||||||
|
| `output_asset_count` | 由 `output_asset_id` 是否为 NULL 派生(0/1) | ✅ 可派生 |
|
||||||
|
| `idempotency_key` / `request_hash` | 同名两列 | ✅ 有 |
|
||||||
|
|
||||||
|
**❌ `first_attempt_at` 缺口(本次修订新发现,非改名问题)**:正典没有这一列,而且 **`ck_generation_jobs_state` 会在重试时主动销毁该信息**——该 CHECK 规定 `status='queued'` 分支必须 `started_at IS NULL`,因此失败重排队时 `started_at` 必须被清空,首次尝试时间不可恢复。后果正是下一段所述:排队时长与纯执行耗时无法分离。**处置:V6 增加 `first_attempt_at timestamptz`(仅首次进入 running 时写入,重试不覆盖),并在状态机 CHECK 中把它排除在 queued 分支的置空要求之外。** 这是 §4.2 对后端工单的硬要求中唯一需要改动正典结构的一项(另一项是 A2 拍板导致的 `input_asset_id` 改可空)。
|
||||||
|
|
||||||
若上述任一列缺失,对应指标不可计算——`first_attempt_at` 缺失 ⇒ 排队时长与纯执行耗时无法分离(只能得到端到端耗时);`attempt_count` 缺失 ⇒ 重试率不可得。
|
若上述任一列缺失,对应指标不可计算——`first_attempt_at` 缺失 ⇒ 排队时长与纯执行耗时无法分离(只能得到端到端耗时);`attempt_count` 缺失 ⇒ 重试率不可得。
|
||||||
|
|
||||||
@@ -464,21 +484,21 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
| 1 进入创作 | `creation_started` | 客户端 | userId |
|
| 1 进入创作 | `creation_started` | 客户端 | userId |
|
||||||
| 2 选定模型 | `creation_model_selected`(末次) | 客户端 | userId |
|
| 2 选定模型 | `creation_model_selected`(末次) | 客户端 | userId |
|
||||||
| 3 提交生成 | `creation_submit_succeeded` / `creation_submit_failed` | 客户端 | userId(任务量用 creationTaskId) |
|
| 3 提交生成 | `creation_submit_succeeded` / `creation_submit_failed` | 客户端 | userId(任务量用 creationTaskId) |
|
||||||
| 4 排队与执行 | `generation_tasks`:`queued_at` → `first_attempt_at` → `finished_at`、`status`、`attempt_count` | **服务端事实表** | creationTaskId |
|
| 4 排队与执行 | `generation_jobs`:`created_at` → `first_attempt_at` → `completed_at`、`status`、`attempt_count` | **服务端事实表** | creationTaskId |
|
||||||
| 5 结果触达 | `creation_result_viewed` / `creation_failure_viewed` / `creation_cancelled` | 客户端 | creationTaskId |
|
| 5 结果触达 | `creation_result_viewed` / `creation_failure_viewed` / `creation_cancelled` | 客户端 | creationTaskId |
|
||||||
| 6 建草稿 → 发帖 | `creation_draft_created` → `post_publish_succeeded`(`creationTaskId` 非空) | 客户端 | userId |
|
| 6 建草稿 → 发帖 | `creation_draft_created` → `post_publish_succeeded`(`creationTaskId` 非空) | 客户端 | userId |
|
||||||
|
|
||||||
**join key 定型**:`creationTaskId`(UUIDv7,由服务端在创建任务时生成并在响应中返回,客户端原样上报)。
|
**join key 定型**:`creationTaskId`(UUIDv7,由服务端在创建任务时生成并在响应中返回,客户端原样上报)。
|
||||||
|
|
||||||
- 客户端事件侧位置:`props->>'creationTaskId'`。
|
- 客户端事件侧位置:`props->>'creationTaskId'`。
|
||||||
- 服务端侧:`creation.generation_tasks.id`。
|
- 服务端侧:`creation.generation_jobs.id`。
|
||||||
- 拼接 SQL 惯例(漏斗第 3~5 段):
|
- 拼接 SQL 惯例(漏斗第 3~5 段):
|
||||||
```sql
|
```sql
|
||||||
SELECT t.id, t.status, t.attempt_count,
|
SELECT t.id, t.status, t.attempt_count,
|
||||||
t.first_attempt_at - t.queued_at AS queue_wait,
|
t.first_attempt_at - t.created_at AS queue_wait,
|
||||||
t.finished_at - t.first_attempt_at AS exec_time,
|
t.completed_at - t.first_attempt_at AS exec_time,
|
||||||
v.props->>'waitedMs' AS perceived_wait_ms
|
v.props->>'waitedMs' AS perceived_wait_ms
|
||||||
FROM creation.generation_tasks t
|
FROM creation.generation_jobs t
|
||||||
LEFT JOIN platform.product_events v
|
LEFT JOIN platform.product_events v
|
||||||
ON v.event_name = 'creation_result_viewed'
|
ON v.event_name = 'creation_result_viewed'
|
||||||
AND (v.props->>'creationTaskId')::uuid = t.id
|
AND (v.props->>'creationTaskId')::uuid = t.id
|
||||||
@@ -488,7 +508,7 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
|
|
||||||
**闭环成立**:六段无断点,第 4 段的服务端事实与前后客户端事件均可经 `creationTaskId` 连上。
|
**闭环成立**:六段无断点,第 4 段的服务端事实与前后客户端事件均可经 `creationTaskId` 连上。
|
||||||
|
|
||||||
**缺口 1(接受不埋):客户端 `creation_queued`。** 排队进入/结束的时点以服务端为准(`queued_at`/`first_attempt_at`),客户端只能靠轮询观测,粒度取决于轮询周期,会系统性高估排队时长。客户端侧的「等待」由 `waitedMs` 一个用户感知量表达即可,不再补一个精度更差的重复事件。
|
**缺口 1(接受不埋):客户端 `creation_queued`。** 排队进入/结束的时点以服务端为准(`created_at`/`first_attempt_at`),客户端只能靠轮询观测,粒度取决于轮询周期,会系统性高估排队时长。客户端侧的「等待」由 `waitedMs` 一个用户感知量表达即可,不再补一个精度更差的重复事件。
|
||||||
|
|
||||||
**缺口 2(接受不埋):`creation_retried` 独立事件。** 重试即一次新提交,已由 `creation_submit_succeeded.regenerateFrom ∈ {failure, dissatisfaction}` 表达。独立事件会与提交事件双计,且让「提交总数」这个分母出现两种口径。
|
**缺口 2(接受不埋):`creation_retried` 独立事件。** 重试即一次新提交,已由 `creation_submit_succeeded.regenerateFrom ∈ {failure, dissatisfaction}` 表达。独立事件会与提交事件双计,且让「提交总数」这个分母出现两种口径。
|
||||||
|
|
||||||
@@ -517,8 +537,8 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
| 指标 | 分子 | 分母 | 数据源 | 口径要点 |
|
| 指标 | 分子 | 分母 | 数据源 | 口径要点 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| **生成成功率** | `status='succeeded'` 的任务数 | `status IN ('succeeded','failed')` 的任务数 | 服务端事实表 | **排除 `cancelled`**(用户主动取消不是系统失败,计入会让「用户不耐烦」伪装成「系统不可靠」);**排除未终态**(`queued`/`running`);按 task 去重非按 user |
|
| **生成成功率** | `status='succeeded'` 的任务数 | `status IN ('succeeded','failed')` 的任务数 | 服务端事实表 | **排除 `cancelled`**(用户主动取消不是系统失败,计入会让「用户不耐烦」伪装成「系统不可靠」);**排除未终态**(`queued`/`running`);按 task 去重非按 user |
|
||||||
| **P50 / P95 生成耗时** | —— | —— | 服务端事实表 | `finished_at - first_attempt_at`(**纯执行耗时,不含排队**);只取 `status='succeeded'`(失败任务的耗时是超时值,混入会污染尾部);P95 用 `percentile_cont(0.95)`;按模型(`model_key`)分组发布,**不发布跨模型合并的 P95**(不同模型耗时分布差一个量级,合并值无意义) |
|
| **P50 / P95 生成耗时** | —— | —— | 服务端事实表 | `completed_at - first_attempt_at`(**纯执行耗时,不含排队**);只取 `status='succeeded'`(失败任务的耗时是超时值,混入会污染尾部);P95 用 `percentile_cont(0.95)`;按模型(`model_id`,展示名 join `generation_models.code`;或直接用 `provider_model_snapshot`)分组发布,**不发布跨模型合并的 P95**(不同模型耗时分布差一个量级,合并值无意义) |
|
||||||
| **排队时长** | —— | —— | 服务端事实表 | `first_attempt_at - queued_at`;P50/P95 同上;**重试任务只取首次尝试**,否则重试等待会被计成排队 |
|
| **排队时长** | —— | —— | 服务端事实表 | `first_attempt_at - created_at`;P50/P95 同上;**重试任务只取首次尝试**,否则重试等待会被计成排队 |
|
||||||
| **端到端感知等待**(辅助) | —— | —— | 客户端 | `creation_result_viewed.waitedMs` 的 P50/P95。与「排队 + 执行」之差即客户端轮询/渲染开销——**这个差值是客户端性能债的直接读数** |
|
| **端到端感知等待**(辅助) | —— | —— | 客户端 | `creation_result_viewed.waitedMs` 的 P50/P95。与「排队 + 执行」之差即客户端轮询/渲染开销——**这个差值是客户端性能债的直接读数** |
|
||||||
| **结果触达率** | `creation_result_viewed` 的去重 `creationTaskId` | `status='succeeded'` 的任务数 | 双源 | 衡量「生成完了但用户没看到」。**这是 AI 创作特有的浪费指标**:算力已花但价值未交付。触达率低 ⇒ 需要完成通知 |
|
| **结果触达率** | `creation_result_viewed` 的去重 `creationTaskId` | `status='succeeded'` 的任务数 | 双源 | 衡量「生成完了但用户没看到」。**这是 AI 创作特有的浪费指标**:算力已花但价值未交付。触达率低 ⇒ 需要完成通知 |
|
||||||
| **生成 → 发帖转化率**(M4 核心) | 24h 内 `post_publish_succeeded` 且 `props->>'creationTaskId'` 非空 的去重 `userId` | `creation_result_viewed` 的去重 `userId` | 客户端 | 分母用**结果触达**而非提交或成功——没看到结果的人不可能发帖,用提交做分母会把系统故障算进产品转化的账 |
|
| **生成 → 发帖转化率**(M4 核心) | 24h 内 `post_publish_succeeded` 且 `props->>'creationTaskId'` 非空 的去重 `userId` | `creation_result_viewed` 的去重 `userId` | 客户端 | 分母用**结果触达**而非提交或成功——没看到结果的人不可能发帖,用提交做分母会把系统故障算进产品转化的账 |
|
||||||
@@ -545,18 +565,18 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
SELECT
|
SELECT
|
||||||
(SELECT count(DISTINCT props->>'creationTaskId') FROM platform.product_events
|
(SELECT count(DISTINCT props->>'creationTaskId') FROM platform.product_events
|
||||||
WHERE event_name = 'creation_submit_succeeded' AND server_ts >= :from) AS client_submits,
|
WHERE event_name = 'creation_submit_succeeded' AND server_ts >= :from) AS client_submits,
|
||||||
(SELECT count(*) FROM creation.generation_tasks WHERE created_at >= :from) AS server_tasks;
|
(SELECT count(*) FROM creation.generation_jobs WHERE created_at >= :from) AS server_tasks;
|
||||||
|
|
||||||
-- 对账 2:孤儿 taskId——客户端上报了但服务端无此任务(期望恒为空集,命中即 P1)
|
-- 对账 2:孤儿 taskId——客户端上报了但服务端无此任务(期望恒为空集,命中即 P1)
|
||||||
SELECT DISTINCT e.props->>'creationTaskId' AS orphan_task
|
SELECT DISTINCT e.props->>'creationTaskId' AS orphan_task
|
||||||
FROM platform.product_events e
|
FROM platform.product_events e
|
||||||
LEFT JOIN creation.generation_tasks t ON (e.props->>'creationTaskId')::uuid = t.id
|
LEFT JOIN creation.generation_jobs t ON (e.props->>'creationTaskId')::uuid = t.id
|
||||||
WHERE e.event_name LIKE 'creation_%' AND e.props ? 'creationTaskId' AND t.id IS NULL;
|
WHERE e.event_name LIKE 'creation_%' AND e.props ? 'creationTaskId' AND t.id IS NULL;
|
||||||
|
|
||||||
-- 对账 3:AI 来源帖的 creationTaskId 必须指向本人任务(期望恒为空集,命中即越权或口径错)
|
-- 对账 3:AI 来源帖的 creationTaskId 必须指向本人任务(期望恒为空集,命中即越权或口径错)
|
||||||
SELECT e.event_id
|
SELECT e.event_id
|
||||||
FROM platform.product_events e
|
FROM platform.product_events e
|
||||||
JOIN creation.generation_tasks t ON (e.props->>'creationTaskId')::uuid = t.id
|
JOIN creation.generation_jobs t ON (e.props->>'creationTaskId')::uuid = t.id
|
||||||
WHERE e.event_name = 'post_publish_succeeded' AND e.user_id IS NOT NULL
|
WHERE e.event_name = 'post_publish_succeeded' AND e.user_id IS NOT NULL
|
||||||
AND t.user_id <> e.user_id;
|
AND t.user_id <> e.user_id;
|
||||||
|
|
||||||
@@ -575,7 +595,7 @@ WHERE NOT EXISTS (SELECT 1 FROM platform.product_events p
|
|||||||
WHERE p.event_name = e.name AND p.server_ts >= :from);
|
WHERE p.event_name = e.name AND p.server_ts >= :from);
|
||||||
```
|
```
|
||||||
|
|
||||||
**未取证**:以上 SQL 未在真实数据上执行过(无数据,§0.2),且 `creation.generation_tasks` 的列名以 §4.2 的要求为准、尚未由后端契约确认;后端定稿后需回改列名并试跑一次。
|
**未取证**:以上 SQL 未在真实数据上执行过(无数据,§0.2)。**列名已于 2026-09-14 按正典 `patbond_postgresql.sql:605` 回改完毕**(映射表见 §4.2),唯一例外是 `first_attempt_at`——正典无此列且状态机会在重试时清空 `started_at`,已升级为 V6 必加列;该列落地前上述「排队时长 / 纯执行耗时」两项 SQL 不可运行。后端 V6 定稿后仍需试跑一次。
|
||||||
|
|
||||||
## 6. A/B 实验设计
|
## 6. A/B 实验设计
|
||||||
|
|
||||||
@@ -737,7 +757,7 @@ creationTaskId 非空的去重 userId / creation_result_viewed 去重 userId)
|
|||||||
| **2** | **eventVersion 口径定型** | ① 每个事件自身的 props schema 版本 ② 事件字典世代号 | **建议 ①**(§2.2 四条理由)。决定性理由是可修复性不对称:读法 ① 下客户端现行硬编码 1 **本就正确**,零改动零回填;读法 ② 下全部历史行皆错且**不可修复**(`server_ts` 无法反推事件当初属于哪代字典)。必须在 M4 新增 8 条事件**之前**定型,否则债务从 41 条规模翻到 49 条 |
|
| **2** | **eventVersion 口径定型** | ① 每个事件自身的 props schema 版本 ② 事件字典世代号 | **建议 ①**(§2.2 四条理由)。决定性理由是可修复性不对称:读法 ① 下客户端现行硬编码 1 **本就正确**,零改动零回填;读法 ② 下全部历史行皆错且**不可修复**(`server_ts` 无法反推事件当初属于哪代字典)。必须在 M4 新增 8 条事件**之前**定型,否则债务从 41 条规模翻到 49 条 |
|
||||||
| **3** | **eventVersion 是否上服务端校验** | ① 推荐档:字典 key 改 `(name, version)`,未知版本逐条 `unknown_event_version` ② 最小档:仅补契约 `minimum: 1` + DTO `@Min(1)` | **建议 ①**。§1.1 刚证明「无门禁的口径必然漂移四份文档而不可见」,纯文档纪律不足。注意实现须放**逐条拒绝路径**,不可放 DTO 校验——否则重犯 `platform` 的整批连坐错误 |
|
| **3** | **eventVersion 是否上服务端校验** | ① 推荐档:字典 key 改 `(name, version)`,未知版本逐条 `unknown_event_version` ② 最小档:仅补契约 `minimum: 1` + DTO `@Min(1)` | **建议 ①**。§1.1 刚证明「无门禁的口径必然漂移四份文档而不可见」,纯文档纪律不足。注意实现须放**逐条拒绝路径**,不可放 DTO 校验——否则重犯 `platform` 的整批连坐错误 |
|
||||||
| **4** | **platform=linux 处置** | ① 把 linux/server 加进枚举 ② 客户端本地短路 + 默认关闭的调试覆盖开关 | **建议 ②**。枚举是三层锁死(契约 × 5 份 + DTO 正则 + DB CHECK `V2:23`),加值需 Flyway 迁移且会让非产品流量污染 platform 维度;收益仅「桌面调试方便」,不成比例。**采纳 ② 的前提条件**:覆盖开关必须配 `appVersion` 可识别后缀护栏(§3.2),否则它就是档 C 造数据的后门 |
|
| **4** | **platform=linux 处置** | ① 把 linux/server 加进枚举 ② 客户端本地短路 + 默认关闭的调试覆盖开关 | **建议 ②**。枚举是三层锁死(契约 × 5 份 + DTO 正则 + DB CHECK `V2:23`),加值需 Flyway 迁移且会让非产品流量污染 platform 维度;收益仅「桌面调试方便」,不成比例。**采纳 ② 的前提条件**:覆盖开关必须配 `appVersion` 可识别后缀护栏(§3.2),否则它就是档 C 造数据的后门 |
|
||||||
| **5** | **服务端生成侧指标来源** | ① 新建 `server_events` 埋点管道 ② 扩 `product_events` 容纳服务端事件 ③ 以 `creation.generation_tasks` 事实表为权威 | **建议 ③**(§4.2)。所需事实 100% 已在任务表(M4 验收标准本就逼出这些列),事件层加不了信息还会引入双写不一致;② 需放宽两个 NOT NULL(不可逆的反向操作)+ 改 DB CHECK。**代价**:后端须承诺 §4.2 列出的列齐备且终态行保留 ≥90 天,否则对应指标不可算 |
|
| **5** | **服务端生成侧指标来源** | ① 新建 `server_events` 埋点管道 ② 扩 `product_events` 容纳服务端事件 ③ 以 `creation.generation_jobs` 事实表为权威 | **建议 ③**(§4.2)。所需事实 100% 已在任务表(M4 验收标准本就逼出这些列),事件层加不了信息还会引入双写不一致;② 需放宽两个 NOT NULL(不可逆的反向操作)+ 改 DB CHECK。**代价**:后端须承诺 §4.2 列出的列齐备且终态行保留 ≥90 天,否则对应指标不可算 |
|
||||||
| **6** | **隐私红线是否为 `creationTaskId` 开例外** | ① 维持「无内容或对方主体 ID」的字面禁止,漏斗改用 (userId, 时间窗) 拼接 ② 收敛措辞为「禁他人/内容主体 ID」,允许上报者自有资源 ID 作拼接键 | **建议 ②**。原红线意图是防止重建「谁对谁的内容做了什么」的社交图谱,`creationTaskId` 不涉第二主体、不泄露 prompt 与产物;① 的时间窗拼接在并发提交下会把 A 任务耗时配给 B 任务结果且不可察觉,对 P95 这类尾部指标是致命的。**否决**「做成第 11 个公共属性」的替代方案——为单域需求撬动跨迭代冻结的十项公共属性面不成比例 |
|
| **6** | **隐私红线是否为 `creationTaskId` 开例外** | ① 维持「无内容或对方主体 ID」的字面禁止,漏斗改用 (userId, 时间窗) 拼接 ② 收敛措辞为「禁他人/内容主体 ID」,允许上报者自有资源 ID 作拼接键 | **建议 ②**。原红线意图是防止重建「谁对谁的内容做了什么」的社交图谱,`creationTaskId` 不涉第二主体、不泄露 prompt 与产物;① 的时间窗拼接在并发提交下会把 A 任务耗时配给 B 任务结果且不可察觉,对 P95 这类尾部指标是致命的。**否决**「做成第 11 个公共属性」的替代方案——为单域需求撬动跨迭代冻结的十项公共属性面不成比例 |
|
||||||
| **7** | **M4 首个「实验」的形态** | ① 直接跑真实 A/B ② A/A 基建验证先行,真实 A/B 冻结设计待流量 | **建议 ②**(§6.1)。真实 A/B 需 ~1,560 名到达结果页用户,当前真实用户 0,按 M2 §4.3 止损纪律现在就判不可行;且三项前置为零代码。A/A 零产品风险、极小样本即可暴露分流失衡与曝光漏报——**跳过它等于把三个从未运行过的组件与一个产品结论绑在一起** |
|
| **7** | **M4 首个「实验」的形态** | ① 直接跑真实 A/B ② A/A 基建验证先行,真实 A/B 冻结设计待流量 | **建议 ②**(§6.1)。真实 A/B 需 ~1,560 名到达结果页用户,当前真实用户 0,按 M2 §4.3 止损纪律现在就判不可行;且三项前置为零代码。A/A 零产品风险、极小样本即可暴露分流失衡与曝光漏报——**跳过它等于把三个从未运行过的组件与一个产品结论绑在一起** |
|
||||||
| **8** | **A/B 前置状态是否按取证结果重置** | ① 维持 `iteration-3/29` 的「6 绿 1 半」 ② 重置为 **5 绿 3 半**并写入 ADR | **建议 ②**。#4 分流哈希、#5 Flutter 曝光封装、#7 feature flag 三者代码零实现(逐项 grep 取证,§1.4),且 #1/#2 的绿是**前瞻式**的(挂在「09-21 起自然达成」上,该前提已随 §0 失效)。这直接决定 M4 排期——**若按「6 绿 1 半」排,会在实验启动日才发现要先写三个组件** |
|
| **8** | **A/B 前置状态是否按取证结果重置** | ① 维持 `iteration-3/29` 的「6 绿 1 半」 ② 重置为 **5 绿 3 半**并写入 ADR | **建议 ②**。#4 分流哈希、#5 Flutter 曝光封装、#7 feature flag 三者代码零实现(逐项 grep 取证,§1.4),且 #1/#2 的绿是**前瞻式**的(挂在「09-21 起自然达成」上,该前提已随 §0 失效)。这直接决定 M4 排期——**若按「6 绿 1 半」排,会在实验启动日才发现要先写三个组件** |
|
||||||
@@ -751,7 +771,7 @@ creationTaskId 非空的去重 userId / creation_result_viewed 去重 userId)
|
|||||||
1. **真机执行人(首日,1 小时,无依赖,最高优先)**:`export ANDROID_HOME=~/Android/Sdk` + `flutter emulators --launch Pixel_7`,执行 M2 两项真机验证(步骤 `device-verification.md:42-92`,模拟器场景宿主机地址用 `10.0.2.2`),填 L106 执行记录。产出 `platform='android'` 事件实证,解除 A/B 前置 #1 的拦路项。**这是 §0.3 的 decisive finding 的直接落地,且不依赖任何其他工单**。
|
1. **真机执行人(首日,1 小时,无依赖,最高优先)**:`export ANDROID_HOME=~/Android/Sdk` + `flutter emulators --launch Pixel_7`,执行 M2 两项真机验证(步骤 `device-verification.md:42-92`,模拟器场景宿主机地址用 `10.0.2.2`),填 L106 执行记录。产出 `platform='android'` 事件实证,解除 A/B 前置 #1 的拦路项。**这是 §0.3 的 decisive finding 的直接落地,且不依赖任何其他工单**。
|
||||||
2. **用户 / PM(首日)**:拍板 §8 的 #1、#2、#5、#8(前两项决定后续工单形态,#5 决定后端表结构,#8 决定排期)。
|
2. **用户 / PM(首日)**:拍板 §8 的 #1、#2、#5、#8(前两项决定后续工单形态,#5 决定后端表结构,#8 决定排期)。
|
||||||
3. **数据侧(0.5 天,依赖 1)**:把 `iteration-2/06` §2.1 北极星 SQL 落成仓库内可执行载体,跑出 W37 实测分母,按 §0.4 档 B 出「基建就绪读数」。
|
3. **数据侧(0.5 天,依赖 1)**:把 `iteration-2/06` §2.1 北极星 SQL 落成仓库内可执行载体,跑出 W37 实测分母,按 §0.4 档 B 出「基建就绪读数」。
|
||||||
4. **后端(依赖 2 的 #5)**:`creation` schema 的 V6 迁移中确保 §4.2 要求的列齐备(`queued_at`/`first_attempt_at`/`attempt_count`/`failure_reason` 等)+ 终态行保留 ≥90 天纪律;与生成任务契约同批冻结 `modelKey`/`styleKey` 枚举。
|
4. **后端(依赖 2 的 #5)**:`creation` schema 的 V6 迁移中确保 §4.2 要求的列齐备(`first_attempt_at`(新增)/`attempt_count`/`error_code` 等)+ 终态行保留 ≥90 天纪律;与生成任务契约同批冻结目录 `code` 口径(正典为 `generation_models.code` / `generation_styles.code`,任务行反范式化列为 `provider_model_snapshot` / `style_code_snapshot`)。
|
||||||
5. **后端(依赖 2 的 #2#3)**:`EventDictionary` v4 增量 8 条 + 2 条改写(§4.5 代码块可直抄);eventVersion 校验按推荐档;**同批补字典条数与全名快照断言**(§7.2 同步点 4)。可与 4 并行。
|
5. **后端(依赖 2 的 #2#3)**:`EventDictionary` v4 增量 8 条 + 2 条改写(§4.5 代码块可直抄);eventVersion 校验按推荐档;**同批补字典条数与全名快照断言**(§7.2 同步点 4)。可与 4 并行。
|
||||||
6. **后端(依赖 5)**:契约 5 份副本同步 `eventVersion` 描述 + `minimum: 1` + `reason` 枚举增量;跑契约一致性矩阵。
|
6. **后端(依赖 5)**:契约 5 份副本同步 `eventVersion` 描述 + `minimum: 1` + `reason` 枚举增量;跑契约一致性矩阵。
|
||||||
7. **Flutter(依赖 2 的 #4,可与 4/5 并行)**:`analytics_service.dart` 四项改动(eventVersion 查表、注释据实、桌面短路、调试覆盖 + appVersion 护栏);两份 E2E 脚本 `eventVersion` 改 1。
|
7. **Flutter(依赖 2 的 #4,可与 4/5 并行)**:`analytics_service.dart` 四项改动(eventVersion 查表、注释据实、桌面短路、调试覆盖 + appVersion 护栏);两份 E2E 脚本 `eventVersion` 改 1。
|
||||||
|
|||||||
Reference in New Issue
Block a user