chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:29:17 +08:00
commit 50bbe68809
112 changed files with 16932 additions and 0 deletions
@@ -0,0 +1,137 @@
# Blind Prediction Protocol(盲预测协议)
被这些子 skill 引用:`cheat-predict``cheat-retro`、主 SKILL.md。
这是项目原则 #1 的完整规范。任何子 skill 在写预测前都必须执行本协议。
---
## 核心定义
**盲预测**:在预测者(人或模型)看到任何关于该作品发布后真实表现数据**之前**完成的预测。
预测一旦写入 `predictions/*.md``## 预测` 段,该段即为 **immutable**——只能在文件末尾追加 `## 复盘` 段,不能修改预测段任何字符。
---
## "见过数据"的边界(关键,常被违反)
下列任一条件成立 → 已不再 blind,**禁止写预测**:
| 信息 | 是否破坏 blind | 例外 |
|---|---|---|
| 该作品任何平台的播放数 / 阅读数 | ✗ 破坏 | 无 |
| 该作品的点赞 / 评论 / 转发数 | ✗ 破坏 | 无 |
| 该作品的具体评论内容 | ✗ 破坏 | 无 |
| 该作品的算法推荐位 / 热门榜位置 | ✗ 破坏 | 无 |
| 该作品发布后的截图 / 后台数据 | ✗ 破坏 | 无 |
| **同期发布的其他人**作品的表现 | ○ 不破坏 | — |
| **历史上类似主题**作品的表现 | ○ 不破坏(这正是锚点对比要做的) | — |
| 该作品**发布前**的稿子内容 | ○ 不破坏 | 这是预测的输入 |
| 用户口述的"我感觉这条还行" | △ 谨慎 | 用户的主观感觉不算"数据",但要在预测里标注用户偏见 |
**判断捷径**:只要这条信息**只能在作品发布后才能获得**,就算"数据"。
---
## 预测者必须主动声明的情况
子 skill 在启动 `cheat-predict` 前,必须自检并向用户**主动声明**:
1. **作品已发布超过 RETRO_WINDOW_DAYS 天**(默认 3 天)→ 必须拒绝写"预测",改记为 `**Reconstructed retrospective**`,明确标注非预测
2. **作品已发布但 < RETRO_WINDOW_DAYS 天,用户尚未透露任何数据**→ 允许 blind 预测,但在文件头部标记 `published_before_prediction: true` + `blind_status: confirmed_no_data_seen`
3. **用户在对话里已粘贴了任何后续数据**→ 同 #1 处理,记为 reconstructed
`BLIND_CHECK=strict`(默认):上述任何破坏条件命中,**拒绝执行**。
`BLIND_CHECK=lenient`:仅警告 + 强制标注,允许继续——只用于离线测试或学术演练,**不推荐用于真实校准**。
---
## Immutable 的工程边界
`## 预测` 段的不可修改是**用户体验承诺**,由 hook 层强制:
- `hooks/prediction-immutability.sh` 在 PreToolUse(Edit|Write) 上检查 `predictions/` 下文件
- 命中 `## 预测` 与下一个二级标题之间任何 diff → exit 1 阻塞
- `## 复盘` 段的追加 → 放行
**禁止的"绕开"模式**(子 skill 必须拒绝):
- "把预测段重写得更准一点" → 拒绝。如有正当理由重做,创建新文件 `<原文件名>_redo.md`,原文件保留
- "我的概率分布写错了 0.5%,让我改一下" → 拒绝。在复盘段追加 `修正:原概率分布 X% 应为 Y%,于 <date> 发现笔误`
- "我前面没考虑 SR=4,重打一下分" → 拒绝。同上路径
唯一允许编辑预测段的场景:**纯 markdown 排版错误**(标题层级错误、列表 bullet 格式错),且用户明确说明这是格式修复。这种情况 hook 仍会阻塞,需要用户显式 bypass(手动设置环境变量 `CHEAT_BYPASS_IMMUTABILITY=1` 单次)——bypass 应在 git history 留痕。
---
## 文件名约定(**三处一致**
一个内容三处文件,**用同一组 `<date>_<id>_<short>` 命名**
```
scripts/<date>_<id>_<short>.md ← pre-shoot 草稿(cheat-seed 写或用户写)
predictions/<date>_<id>_<short>.md ← immutable 预测(cheat-predict 写)
videos/<date>_<id>_<short>/ ← 拍后才建(cheat-shoot 创建)
├── script.md ← 用户提供的最终拍摄稿
└── report.md ← T+3d 数据(cheat-retro 写)
```
- `<date>`**草稿首次落盘日期**(即 `scripts/<id>.md` 的创建日),不是预测日 / 拍摄日 / 发布日。理由:保持 ID 稳定——草稿大改后 hash 变了仍想保持文件可追溯
- `<id>`12 位 sha256 前缀,对**草稿首次落盘的内容**做 hash。用户 edit 草稿后**不变**——便于跨文件引用
- `<short>`:3-8 字中文或英文短名,便于人类辨识
Reconstructed 重做:在 `<short>` 后加 `_redo`,三处都加:
- `scripts/<date>_<id>_<short>_redo.md`
- `predictions/<date>_<id>_<short>_redo.md`
- `videos/<date>_<id>_<short>_redo/`
原文件保留(不删)。
---
## 子 skill 必须做的检查清单
`cheat-predict` 启动时:
1.`BLIND_CHECK` 常量
2. 询问用户该作品当前发布状态(未发 / 已发 < RETRO_WINDOW_DAYS / 已发 ≥ RETRO_WINDOW_DAYS
3. 询问对话历史里是否提到过该作品的任何后续数据(如有,自检对话里有没有 "播放/阅读/点赞/评论" 等关键词)
4.#2#3 命中破坏条件 → 按 `BLIND_CHECK` 模式处理
5. 通过后才允许写 `predictions/*.md`
`cheat-retro` 启动时:
1. 读目标 prediction 文件
2. **先在内存里 cache 住 `## 预测` 段**——后续任何对该文件的写都必须先校验该段未变
3. 抓数据 → 追加 `## 复盘`
4. 写完后**再次校验**:写入后该文件的 `## 预测` 段哈希应等于步骤 2 的 cache。不等 → 报错并回滚
主 SKILL.md
- 用户说出"重写预测" / "改一下预测段" / "你之前预测错了我帮你改" 时,**直接拒绝并解释**,引导改用 `_redo.md` 路径
---
## 异常状态处理
| 场景 | 处理 |
|---|---|
| 预测文件不小心被人手编辑了预测段 | 不自动回滚(破坏更大)。下次 `cheat-retro` 检测到不一致 → 在复盘段追加 `**Integrity warning**: 预测段于 <ISO timestamp> 被外部修改,无法保证盲度`,校准价值降级为"参考",不计入 bump 校准池 |
| 预测文件遗失 / 被删 | git log 找回。找不到 → 在 `rubric_notes.md` 记录"<id> 预测文件遗失,校准池缺该样本" |
| 用户原本是 cold-start,半路想"补"已发作品的预测 | 一律记为 `**Reconstructed retrospective**`,不计入校准池——这是补的不是预测。可作为"观察"记录到 `rubric_notes.md` |
---
## 反模式(必须拒绝的请求)
- 「帮我预测一下,但我先告诉你播放量你来反推就行」 → 拒绝。直接破坏盲度
- 「这条已经发了 5 天数据出来了,但你假装没看到,给我做个预测看会不会准」 → 拒绝。请改用 `_redo.md` 走 reconstructed 路径
- 「上次预测算错了,帮我把概率分布改一下」 → 拒绝。在复盘段说明
- 「能不能跳过 blind check 我有特殊原因」 → 询问原因;只有"格式修复"是合法的 bypass 理由
---
## Why(为什么这套这么严)
盲预测是整个 cheat-on-content 校准循环的**唯一信号源**。一旦预测段被事后修改,所有"哪个维度被验证 / 推翻"的判断都失去基线——你不知道当初是真预测对了,还是事后改对了。
校准价值 = 预测精度 × 预测可信度。
- 预测精度可以靠 rubric 升级慢慢提升。
- 预测可信度一旦破坏不可恢复——**这是为什么 immutability 是 hook 层强制,不是君子协定**。
@@ -0,0 +1,155 @@
# Rubric Bump Validation Protocol(升级验证协议)
被这些子 skill 引用:`cheat-bump`、主 SKILL.md。
这是项目原则 #2 的完整规范——**升级 = 全量重打**。任何对 rubric 的结构性变更都必须走完本协议。
---
## "升级"的定义
下列任一变更触发完整 bump 流程:
- 公式系数变化(ER×1.5 → ×2.0)
- 维度增减(删 NA,加 MS
- 维度定义颠覆性改写("QL=金句数量" → "QL=可挪用句式")
- 归一化常数变化(除数从 8.5 → 9.0)
下列变更**不**触发 bump,但要在 `rubric_notes.md` 标注:
- 维度定义边际细化(QL=5 的门槛从"≥3 句"细化为"≥3 句且分布在不同段")
- 单维度门槛变严的备注(不改公式,只改打分时的判断习惯)
- 锚点样本更新(用新样本作为 5 分标杆)
---
## 何时**可以**提议升级(**Claude 判断为主,下面是参考触发场景**)
任一成立即可提议——但**最终是 Claude 判断观察的成熟度 + 信号强度**,下面只是 default heuristic
1. 某个跨样本观察出现**清晰、可追溯到具体数据点的支持模式**(默认参考 ≥3 样本,但 Claude 可以基于 1-2 个**特别强**的样本提议——如 6x 流量比 / 单一模因 ≥2000 赞这种异常信号)
2. 当前 rubric 在最近复盘中**系统性偏向同一方向**(默认参考 ≥3 次同向,但 Claude 可以基于 1-2 次**极端偏离**提议——如中枢 50w 实绩 5w 这种 10x 偏差)
3. 某候选维度展示了独立预测力(参考样本数:≥3 是稳,但 1-2 个强证据也算)
4. 校准池跨过某分水岭(5 / 10 / 20 / 50)触发常规复审
**Claude 提议 bump 时必须显式标注**:本次提议是 default-aligned(满足上面常规门槛)还是 judgment-driven(基于强信号但样本少)。让用户审视依据。
---
## 何时**禁止**提议升级
下面是**硬约束**——Claude 不能突破:
- 当前正处于 in-progress predictionstate file `in_progress_session != null`)—— 流程纪律
- 上次 bump 后没有任何新校准样本——必须至少 1 个新样本才能再次 bump(避免循环验证自己)
下面是**软建议**——Claude 通常应避免,但有强理由可以打破并显式标注:
- 校准池 < 5 样本时一般不 bump(除非出现强反例)
- 上次 bump 距今 < 3 篇新校准样本时一般不 bump(除非新样本含强反例)
**Claude 软违反时必须:**
1. 在提议中显式说明"我知道默认建议等到 N≥5 / 等到 ≥3 篇新样本,但本次原因是 [X 强信号]"
2. 仍走完整 5 步验证流程(包括跨模型审核)
3. 跨模型审核如果 REJECT → bump 被拒,**不允许"我觉得信号强"绕过**
---
## 完整升级流程(强制 5 步,不允许跳步)
### Step 1:写出新公式的完整方程
不能只说"ER 权重提高",必须把整个方程完整写出,所有系数和归一化常数。
示例(合规):
```
v2.0 composite = (ER×1.5 + SR×1.5 + HP×1.5 + QL + NA + AB + SAT) / 8.5 × 2.0
v2.1 composite = (ER×2.0 + HP×1.5 + MS×1.5 + QL + SR + TS + SAT) / 9.0 × 2.0
```
示例(违规):
- "把 ER 权重提到 2.0" ← 没说其他权重是否变
- "加 MS 维度" ← 没说权重和归一化常数
### Step 2:在校准池上**本地**全量重打分
校准池定义:`predictions/*.md` 中所有有完整复盘段(含 `actual_*` 数据)的文件。
每篇用新公式重新算 composite。**不重新打各维度分数**——维度分数不变(v2 的 ER=5 在 v2.1 也是 ER=5),只重算综合分。
例外:如果新公式增加了维度(如新增 MS),那对每个老样本必须**回追打 MS 分**。这一步会让升级成本随样本数线性上升,是设计上的"升级阻尼"。
### Step 3:计算排序一致性
新 composite 排序 vs 各样本实绩 bucket 排序,必须满足:
- 在 ≥`THRESHOLD` 比例样本上一致(默认 4/5 = 80%)
- 不能把旧公式做对的任一对样本顺序搞反(pairwise no-regression
排序一致性的算法:
1. 用新 composite 给所有校准样本排序
2. 用 actual_plays 给所有样本排序
3. 计算 Spearman rank correlation;同时检查每对 (i, j) 的相对顺序是否颠倒
4. 输出对照表:每个样本的 [新 rank, actual rank, delta]
### Step 4:跨模型独立审核(**强制**,除非 escape hatch
`CROSS_MODEL_AUDIT=true`(默认)时:
1. 调用 `mcp__llm-chat__chat`,把以下打包发给外部 LLM
- 旧公式 + 新公式
- 校准池所有样本的:维度分数、composite(新+旧)、actual_plays、actual_likes、actual_comments、actual_shares
- Step 3 的排序对照表
2. 外部 LLM 独立判定两件事:
- 排序一致性是否真的 ≥ THRESHOLD?
- 新公式相比旧公式是否解释力更强?
3. 外部 LLM 必须输出 **PASS** / **REJECT** + ≥ 100 字理由
4. 本地判定与外部判定**两个都通过**才能进入 Step 5
`CROSS_MODEL_AUDIT=false`:跳过外部审核——**仅在离线/无网时使用**。state file 标记 `last_bump_self_audited: true``cheat-status` 持续提示用户配置外部审核。
### Step 5:升级后清算(Cleanup pass
bump 落地必须一次性完成:
1.`rubric_notes.md` 顶部更新 `**当前版本**` 和**版本速查表**新增一行
2.`rubric-memo.md` 追加"vN → vN+1 升级 Memo" 段写完整 memo(含触发观察 + 证据数据 + 诊断 + 新公式 + 已知局限)。⚠️ 不写入 `rubric_notes.md`——它是 blind sub-agent 白名单,含实绩数据会污染盲评通道
3. 删除驱动本次升级、且已被吸收为正式维度的所有"观察记录"段条目
4. 删除升级过程中被推翻的"观察记录"段条目
5. 仍未解决的"观察记录"段条目 → 移到新版本的"待验证假设"段(保留)
6. 更新所有校准样本的 prediction 文件**底部**追加一行:`**Re-scored under <new-version> on YYYY-MM-DD**: composite=X.XX → Y.YY`(不动预测段、不动复盘段,只追加这一行)
清算不是可选的。rubric_notes.md 是工作台,不是博物馆——见 `observation-lifecycle.md`
---
## bump 被拒后的处理
任一步失败 → bump 拒绝,按下列规范处理:
| 失败位置 | 处理 |
|---|---|
| Step 3 本地排序不一致 | 候选公式回到"待验证"区。**不允许**默默放宽 THRESHOLD(如从 4/5 改到 3/5)—— 那是诚实的 self-deception。THRESHOLD 是协议刚性,与"何时可以提议 bump"的样本数门槛不同(后者可以软违反) |
| Step 4 外部审核 REJECT | 把外部 LLM 的理由完整记录到 `rubric-memo.md` 的"被拒升级 log"段 |
| Step 4 外部审核与本地判定冲突(一个 PASS 一个 REJECT) | 视为 REJECT。冲突意味着至少一方对数据的解读不稳定,不应升级 |
| Step 5 清算无法完成(如某观察既不能删也不能保留) | bump 回滚到 step 0。Note:这意味着新公式还有未捕获的"未解决观察",方案不成熟 |
---
## 升级阻尼(Why this protocol is intentionally hard
校准池每多一个样本,bump 成本线性上升:
- 校准池 3-5 个 → 几乎零成本
- 校准池 20-30 个 → 可感成本
- 校准池 50+ → 痛苦
这是**故意的设计**。频繁 bump = rubric 在追噪声。一个稳定 rubric 的特征是:**bump 越来越罕见,bump 越来越大**——单次 bump 解释多个累积观察,而不是一观察一升级。
参考视频分析项目:v1 → v2 用了大约 4 周(从首次发布到第一次 bump),v2 → v2.1 候选已 4 周仍未升正(在等 6 样本联合验证)。这是健康节奏。
---
## 反模式(必须拒绝的请求)
- 「跳过校准池重打,直接换公式」 → 拒绝。Step 2 不可跳
- 「外部 LLM 审核太麻烦,跳过」 → 仅当 `CROSS_MODEL_AUDIT=false` 显式设置时允许
- 「这次只调一个权重,不算升级吧」 → 任何系数变化都算。Step 1 必须写完整方程
- 「反正是我自己用,不用走全套流程」 → 拒绝。这套流程的价值在你**未来回看**时——你将来想问"为什么 v2.3 没采纳",必须有完整 memo
- 「能不能把 THRESHOLD 从 4/5 改到 3/5 让这次过了」 → 拒绝。改 THRESHOLD 本身是元层级 bump,需要单独走流程
+194
View File
@@ -0,0 +1,194 @@
# Cadence Protocol(节奏协议)
被这些子 skill 引用:`cheat-status``cheat-recommend``cheat-shoot``cheat-publish`、SessionStart hook。
固化"哪天该做什么"——避免用户驱动每一步。让 Claude 在会话开场就能回答"我现在该拍 / 该发 / 该复盘"。
---
## 三层节奏
### 日级(每天 / 每次会话开场)
1. SessionStart hook 自动渲染 4-6 行报告:
- 📦 Buffer 状态(颜色 + 数量)
- ⏰ 待复盘到期项
- 🎯 候选池 top 3(粗排)
- 📅 上次抓热点时间
- ⚠️ 关键 to-do
2. 不主动开始任何动作——等用户决定
### 事件级(T+`RETRO_WINDOW_DAYS` 天到期)
- 任何已发未复盘 + 时间到 → SessionStart 顶部高亮
- 用户给数据(粘 / URL)→ `/cheat-retro` 自动跑
### 周级(用户决定的"集中处理日")
- 抓热点(`/cheat-trends`)刷新候选池
- 检查 rubric bump 触发条件
- 清理 STATUS.md / rubric_notes.md 是否需要清算
---
## Buffer 警戒规则
**Buffer = `state.shoots` 数组长度** = 已拍但未发布的视频数。
`/cheat-shoot` 把视频加进 `state.shoots``/cheat-publish` 移除——两个事件分开使 buffer 跟踪准确。
### 颜色阈值(按 `target_publish_cadence_days` 派生)
`buffer_days = buffer_count × target_publish_cadence_days`
| buffer_days | 颜色 | 含义 | 行动 |
|---|---|---|---|
| < 1 | 🔴 **红** | 警戒——下个发布日可能断更 | **今天必须拍**,且只拍稳分(top 1,不冒险) |
| 1-2 | 🟠 橙 | 偏低 | 应该拍 1-2 条 |
| 3-5 | 🟢 绿 | 正常 | 节奏稳定,可以拍可以休 |
| > 5 | 🔵 蓝 | 积压 | **暂停拍摄**,全力发布存货 + 复盘 |
**示例**
- 用户 cadence = 1(日更),buffer count = 0 → buffer_days = 0 → 🔴
- 用户 cadence = 7(周更),buffer count = 1 → buffer_days = 7 → 🔵(一篇够发七天)
- 用户 cadence = 1buffer count = 4 → buffer_days = 4 → 🟢
### 灵活节奏(target_publish_cadence_days = null
用户在 cheat-init 选"灵活/不固定" → buffer 监控**关闭**。SessionStart 报告只显示"已拍未发:N 条",不显示颜色,不警戒。
---
## 选题策略(`/cheat-recommend` 推 ≥ 2 个时)
每次推荐 2 条时遵循 **1 稳分 + 1 实验性** 原则:
### 第 1 条(稳分)
- 排序 top 1-3
- 类目与最近 N 条已发**不重复**N = max(3, target_publish_cadence_days × 3),避免审美疲劳)
- composite 高 + 议题安全(非 risky
### 第 2 条(实验性)
- 候选池里能验证某个**待验证假设**的样本(如新维度的 A/B 对照)
- 或验证某个**新 pattern**[script_patterns.md](script_patterns.md) 的 Pattern N
- composite 不一定 top,但有"信息价值"——复盘后能让 rubric / pattern 库前进
### Buffer 颜色对推荐的覆盖
| Buffer 颜色 | 推荐策略覆盖 |
|---|---|
| 🔴 红 | **只推稳分 top 1**——不推实验性。"今天能拍出来就行" |
| 🟠 橙 | 1 稳 + 1 实验,但建议优先拍稳分 |
| 🟢 绿 | 标准 1+1 |
| 🔵 蓝 | **暂停推荐**——回 "你 buffer 积压了,先发存货 + 复盘" |
**关键约束**(任何颜色都遵守):
- 同一 category 连发 ≤ 2 条
- 已发过的 candidate(标 done)不推
- 用户主动跳过的 candidate(标 skip6 个月内不推
---
## 节奏元规则
按优先级(高→低):
1. **Buffer 优先于评分**:红色警戒时不要因为"等更好的选题"而断更——拍 composite 7.5 的稳分比"等明天的 9.0"安全
2. **复盘优先于新拍**T+RETRO_WINDOW_DAYS 到期当天**先复盘再考虑拍新的**——否则数据信号丢失,rubric 校准受损
3. **同步优先于积压**:buffer 满(蓝色)时不要再拍,先发掉再说——已拍议题的时效性会衰减
4. **实验性最多 1/天**:每天拍 2 条时至少 1 条是稳分。**不要全实验**——冷启动期实验失败率太高,伤校准节奏
---
## 标准化"今日工作流"模板
### 情况 1buffer 充足 + 没到 T+3d 复盘
```
SessionStart 报告 → user 决定拍/不拍
├─ 拍 → "推荐选题" → cheat-recommend 推 2 个 →
│ user 选 → /cheat-seed 写 draft (cold-start) 或 user 自己写 →
│ user 改写 → script.md → user 拍 → "拍了 videos/<...>/" → cheat-shoot
└─ 不拍 → 等
```
### 情况 2buffer 充足 + 到 T+3d 复盘
```
SessionStart 报告含 ⏰ 复盘提醒 → user 给 video URL 或粘数据 →
cheat-retro 自动跑 → 写复盘段 → 检查 bump 触发条件
├─ 触发 → 提议 /cheat-bump(不强制,用户决定)
└─ 未触发 → 等下个验证样本
```
### 情况 3buffer 红色警戒
```
🔴 SessionStart 第一行警戒 → user 决定
├─ 拍 → cheat-recommend 只推 v 当前 top 1 稳分 → 立即拍
└─ 接受断更风险 → user 自负,cheat-status 持续提示
```
### 情况 4buffer 蓝色积压
```
🔵 SessionStart 报告"积压" → user 决定
├─ 发 → "已发布 https://..." → cheat-publish → buffer -1
├─ 复盘 → 见情况 2
└─ 拍新 → cheat-recommend 拒绝:"你 buffer 已 N 条,先发掉 ≤3 条再来"
```
### 情况 5:周期性集中处理日(用户主动触发)
```
user 说"抓热点" → cheat-trends → 候选池更新
+ user 说"看看 rubric 是不是该升了" → cheat-status 检查同向偏差累计
+ user 说"看看 rubric_notes 行数" → cheat-status 健康度检查
```
---
## 兜底:流程偏离时
如果某天违反节奏(buffer=0 但用户强行不拍 / 积压 ≥10 但用户继续拍),SessionStart 报告**显式标注**
```
❌ 你已 N 天没发新内容(最后一次发布:YYYY-MM-DD),
buffer = 0,你的频道目前处于"事实断更"状态
```
或:
```
❌ 你 buffer 已 N 条但还在新拍,
过去 N 条里有 N 条已超过 X 天未发——存在时效性流失风险
```
**不会自动尝试补救**——只显式报告,由 user 决定如何回到节奏。
---
## 子 skill 责任表
| Skill | 节奏责任 |
|---|---|
| `/cheat-init` | 问 cadence;写 `target_publish_cadence_days`;装 SessionStart hook |
| `/cheat-shoot` | 把 video folder 加 state.shootsbuffer +1 |
| `/cheat-publish` | 从 state.shoots 移除对应项,buffer -1 |
| `/cheat-status` | 计算 buffer + 颜色,输出报告 |
| `/cheat-recommend` | 按 buffer 颜色 + 选题策略给推荐 |
| `/cheat-retro` | 复盘后更新 STATUS(自动 trigger /cheat-status |
| SessionStart hook | 调 /cheat-status 渲染 4-6 行报告,写到 STATUS.md |
---
## 关键差异:cheat-on-content vs 视频分析
| 维度 | 视频分析 | cheat-on-content |
|---|---|---|
| Cadence 来源 | 默认日更(CADENCE.md 硬编码) | 用户自填(cheat-init 问,4 档:日/隔日/周/灵活) |
| Buffer 阈值 | 0/1/2/3-5/6+(按"篇"| 0/1-2/3-5/>5(按"buffer_days"——按用户 cadence 派生) |
| 推荐 2 条策略 | 1 稳 + 1 实验 | 同 |
| SessionStart 报告 | CLAUDE.md 文字约束 + Claude 自觉 | hook 强制 + Claude 读 hook 输出 |
+169
View File
@@ -0,0 +1,169 @@
# Candidate Schema(候选项统一 schema
被这些子 skill 引用:`cheat-trends``cheat-recommend``cheat-init`、所有 `adapters/`
任何"待决定要不要做"的内容素材——不管来自手粘列表 / RSS / Notion / 平台热点抓取——都必须 normalize 成本 schema 之后才进入候选池。这是 `adapters/` 的输出契约。
字段设计参考博主项目的 `articles` 表 schema(私有项目,工具的方法论由此抽象而来)。
---
## 必填字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | string (12 chars) | 稳定 hash`sha256(source + normalized_title + url_path)[:12]`。同一条素材在不同时间被抓到 → 同 id |
| `title` | string | 候选项的人类可读标题 |
| `source` | string | 来源标识,格式 `<adapter-type>:<source-name>`,例:`trend:hackernews``pool:notion-mybook``paste:manual` |
| `snapshot_text` | string | 候选项的全文或摘要——**这是打分的输入**,不是 url。adapter 必须负责把 url 拓展成可读文本 |
| `snapshot_at` | ISO 8601 | 抓取/录入这条 item 的时间 |
---
## 可选字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `url` | string | 原始链接(便于追溯) |
| `tier` | enum | `tier1` / `tier2` / `tier3` / `skip` / `risky` / `done`。粗分类,用于过滤 |
| `read_status` | enum | `unread` / `skimmed` / `deep_read` / `done`。处理状态 |
| `category` | string | 自定义分类标签(如"社会"、"家庭"、"学术" |
| `composite_score` | float | 用当前 rubric 打分得到的综合分(如已打) |
| `dimension_scores` | object | 各维度的整数分,键名对齐当前 rubric 的维度(如 `{"ER": 5, "HP": 4, ...}` |
| `scored_under_rubric_version` | string | 打分时用的 rubric 版本号 |
| `predicted_bucket` | string | 粗预测桶(如 `30-100w`),**注意:不是正式预测**——选题阶段的粗略估计,与 `predictions/*.md` 的 immutable 预测完全独立 |
| `predicted_reason` | string | 一句话理由 |
| `note` | string | 自由文本备注,例如"等节点再发"、"待重读"、"风险议题" |
| `rejected_at` / `rejected_reason` | ISO 8601 / string | 用户主动跳过此候选时记录 |
---
## JSON 范例
### Markdown 列表 adapter 输出
```json
{
"id": "a3f2c1d4e5b6",
"title": "为什么我们都讨厌主动联系朋友",
"source": "pool:markdown-list",
"snapshot_text": "[用户从 candidates.md 复制的全文]",
"snapshot_at": "2026-05-04T08:30:00+08:00",
"url": null,
"tier": "tier1",
"read_status": "skimmed",
"category": "社交",
"composite_score": 7.4,
"dimension_scores": {"ER": 4, "HP": 4, "QL": 5, "NA": 3, "AB": 5, "SR": 3, "SAT": 3},
"scored_under_rubric_version": "v0",
"predicted_bucket": "5-30w",
"predicted_reason": "ER=4+QL=5 强金句感,AB=5 普适,但 SR=3 议题不够强",
"note": ""
}
```
### Trend adapter 输出(HN
```json
{
"id": "8c4d92e1f0b3",
"title": "Show HN: I built a tool that predicts whether your video will go viral",
"source": "trend:hackernews",
"snapshot_text": "[文章全文 + 评论 top 5 的摘要]",
"snapshot_at": "2026-05-04T09:15:00+08:00",
"url": "https://news.ycombinator.com/item?id=12345678",
"tier": null,
"read_status": "unread",
"category": "tech-meta",
"composite_score": null,
"dimension_scores": null,
"scored_under_rubric_version": null
}
```
打分前 score 字段全部为 null——是预期的。`cheat-trends` 抓回来后会调 `cheat-score` 给每条算 composite。
---
## Markdown 表示(用户可见的存储格式)
候选池的默认存储是 `candidates.md`(人类可读),不是 JSON。每条 item 是一个 H3 entry
```markdown
### [tier1] 为什么我们都讨厌主动联系朋友
- **id**: a3f2c1d4e5b6
- **source**: pool:markdown-list
- **snapshot_at**: 2026-05-04
- **category**: 社交
- **composite (v0)**: 7.4 — ER=4 HP=4 QL=5 NA=3 AB=5 SR=3 SAT=3
- **predicted bucket**: 5-30w
- **note**:
> [snapshot_text 段,如有]
```
升级到 SQLite 之后(见 `cheat-status` 的升级触发),同样字段走 `articles` 表存储,markdown 视图自动从 DB 渲染。
---
## ID 稳定性的关键规则
**同一条素材在不同时间被不同 adapter 抓到 → 必须算出同 id**。这是去重的基础。
算法:
```python
import hashlib
def candidate_id(source: str, title: str, url: str = None) -> str:
normalized_title = title.strip().lower().replace(" ", "")
url_path = url.split("?")[0].rstrip("/") if url else ""
raw = f"{source.split(':')[0]}|{normalized_title}|{url_path}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:12]
```
注意:
- `source` 取冒号前的 adapter type`trend:hackernews``trend`),不是具体 source name——同一标题被 HN 和 Reddit 都抓到,应判定为同一候选(避免重复打分)
- title 做了 lowercase + 去空格——避免 "Hello World" 和 "hello world" 被算成不同 id
- url 砍掉 query string—— `?utm_source=xxx` 不影响内容
---
## 去重协议
`cheat-trends` / `cheat-recommend` 在写入 `candidates.md` 前必须执行:
1. 计算新 item 的 id
2. 检查 `candidates.md` 是否已含此 id → 跳过
3. 检查 `predictions/*.md` 是否含此 id(已发过)→ 跳过
4. 检查 `.cheat-cache/trends-history.jsonl` 是否含此 id 且 `rejected_at != null` → 跳过(用户已主动拒绝过)
5. 通过则写入
`.cheat-cache/trends-history.jsonl` 是抓取历史的去重缓存,每行一个 JSON recordappend-only。被用户拒绝的候选会在这里保留 6 个月;之后允许重新出现(也许素材在新 rubric 下评估不同)。
---
## tier 的语义
| Tier | 含义 | 对应行动 |
|---|---|---|
| `tier1` | 强候选,应推荐 | 进入 `cheat-recommend` 排序池 |
| `tier2` | 中等,备选 | 进入排序池但权重低 |
| `tier3` | 弱候选,备而不用 | 不进推荐池,留作长尾 |
| `skip` | 用户主动跳过 | 不再出现 |
| `risky` | 议题敏感 / 平台风控风险 | 推荐时额外标注,需用户确认 |
| `done` | 已发布 | 移出候选池,由 prediction file 接管 |
**Cold-start 期间所有 item 默认是 `unread`/`null tier`**——直到用户或 `cheat-score` 给出 composite 后才能粗分类。**未打分的 item 不应出现在 `cheat-recommend` 输出**——避免推荐没读过的素材。
---
## adapter 实现契约
任何 `adapters/` 下的 adapter 都必须:
1. 实现 `fetch() → List[Candidate]` 接口(伪签名,实际是 markdown 文档描述的协议)
2. 输出符合本 schema 的 items
3. 自己负责把 url / 短摘要拓展成可读 `snapshot_text`——**adapter 不输出"光秃秃的 url"**
4. 优雅降级:如配置缺失(API key、cookie),返回空列表 + 在 stderr/log 写明原因,**不抛异常**
详见 `adapters/HOWTO.md`(待批次 3 写)。
+149
View File
@@ -0,0 +1,149 @@
# Data Source Routing — 热点工具的触发与路由协议
被 cheat-seed / cheat-trends 引用。规定**何时**调热点工具、**调哪个**、**不调时怎么办**。
---
## 核心哲学
> **热点工具是"前置素材库",不是"主菜单"。**
>
> - 用户在**内省**(讲自己的经历 / 思考动机)→ **不调**,避免外部信息污染
> - 用户在**找素材**(没想法 / 要批量 / 显式抓热点)→ **调**,按 content_form 路由数据源
> - 用户在**确认 angle**(讲了时事话题)→ **不主动调**,让用户决定要不要外部数据作参考
设计目的:保护 cheat-seed 的核心论点——"好内容来自用户的真实经历,AI 不凭空 brainstorm"——同时不让"完全没想法"的新博主卡死。
---
## 触发矩阵(被 cheat-seed Phase 1 引用)
| cheat-seed Mode | 默认调? | 触发条件 |
|---|---|---|
| **Mode A**(用户给了具体经历/topic) | ❌ 默认不调 | 仅当用户讲的本身是时事话题(含产品名/人名/事件名 + 时间词)+ 用户**主动同意** |
| **Mode B**(方向不具体,问"为什么") | ❌ **永远不调** | 这阶段用户在内省,外部素材是噪音 |
| **Mode C**(完全没想法) | ✅ 默认调 | Mode C 的核心动作就是把外部素材摆出来 |
| `--batch N` | ✅ 默认调 | 批量 brainstorm 必须有 anchor |
| `/cheat-trends` 显式 | ✅ 调 | 主入口,无需解释 |
| `/cheat-recommend` | ❌ 默认不调 | 已有 pool;除非 pool >7 天没更新 → 提示先 trends |
---
## 时事话题判定(Mode A 灰色场景用)
让 Claude 判断,**不写正则白名单**:
| 信号 | 含义 |
|---|---|
| 含**专有名词**(人名 / 产品名 / 事件名) | 强信号——可能是时事 |
| 含**时间词**"今天" / "刚" / "最近" / "刚刚发生" | 强信号 |
| 含**结构词**"对比" / "回应" / "事件" | 弱信号 |
| 仅含通用名词 + 个人经历词("我" / "昨天" / "我同事") | 反信号——是长青个人经历,**不是时事** |
判定结果:
- **强信号** → 询问用户"要不要拉一下这话题的舆论风向作参考"
- **弱信号 / 模糊** → 不主动询问,直接进 Mode A 深挖
- **反信号** → 100% 不调
跟 [bump-validation-protocol.md](bump-validation-protocol.md) 的"软规则、Claude 判断"哲学一致。
---
## 数据源路由(按 content_form
[adapters/trend-sources/](../adapters/trend-sources/) 目前有两个一等公民 + 一个保底:
| Adapter | 适合的 content_form |
|---|---|
| [`aihot`](../adapters/trend-sources/aihot.md) | `tutorial-builder` / AI 行业评论 / AI 教程 / AI 产品测评 |
| [`trendradar-mcp`](../adapters/trend-sources/trendradar-mcp.md) | `opinion-video` / `long-essay` / `short-text` / `podcast` / `other`(生活/职场/文化) |
| `manual-paste` | 永远的 fallback——用户粘 URL/标题列表 |
### content_form → 主调 + 备调矩阵
| content_form | 主调 | 备调 | 不调 |
|---|---|---|---|
| `opinion-video` | trendradar-mcp | aihot(仅当话题与 AI 行业相关) | — |
| `long-essay` | trendradar-mcp | aihot(同上) | — |
| `short-text` | trendradar-mcp | aihot(同上) | — |
| `podcast` | trendradar-mcp | aihot(同上) | — |
| `tutorial-builder` | **aihot** | trendradar-mcp(仅当涉及通用工具/产品发布) | — |
| `mixed` | 两个都调 | — | 由 Claude 判断每条候选属于哪个垂类 |
| `other`(美食/妆教/剧情/...| trendradar-mcp | — | aihot(与 AI 无关) |
### 用户层覆盖
`.cheat-state.json``enabled_trend_sources` 字段是**显式开关**
```json
"enabled_trend_sources": ["aihot", "trendradar-mcp", "manual-paste"]
```
数组里有的才会被调。空数组 → 仅走 manual-paste。
cheat-trends 显式调用时支持 override`/cheat-trends — sources: aihot`(仅这次用 aihot)。
---
## 失败降级链
```
[cheat-seed Mode C 触发拉热点]
[按 content_form 选主调]
├─ 主调成功 → 拿数据 → 进流程
├─ 主调失败(API down / MCP 没装 / 超时)
│ ↓
│ [按 content_form 选备调]
│ ├─ 备调成功 → 拿数据 → 提示用户"主调用不了,用了备调"
│ └─ 备调也失败 → 走 manual-paste 兜底
│ ↓
│ [询问用户:"今天看到啥可以拍的?粘几条 URL/标题给我"]
└─ 用户当前没启用任何 source → 提示如何启用 + 这次直接走 manual-paste
```
**关键纪律**:所有失败都**不抛异常**。cheat-seed 永远能跑——区别只是有没有外部素材。
---
## Token 成本意识
热点 API 调用**有成本**aihot 是 token / trendradar-mcp 是 MCP 调用 + LLM context)。判定原则:
| 场景 | 调用频率 |
|---|---|
| Mode C 触发 | 每次会话最多 1 次(拿数据后 cache 在内存) |
| Mode A 灰色场景 | 用户同意才调,1 次 |
| `--batch N` | 1 次拿足够候选 |
| 用户连说"再来一批" | 第二次允许,第三次提示"要不要换 query 角度" |
不要在同一会话里反复调同一个端点——那是浪费。
---
## 与 candidates.md 的关系
热点工具拉回的数据**最终落到** [candidate-schema.md](candidate-schema.md) 定义的 `candidates.md`
```
[trend tool] → items
→ 去重(vs candidates.md / predictions/ / .cheat-cache/trends-history.jsonl
→ 粗打分(cheat-seed 内联 rubric
→ 写入 candidates.md(带 source 字段标明来自哪个 adapter
```
cheat-seed Mode C 拿到数据后**不**直接进 brainstorm,先入 candidates.md,再让 Claude 从池子里选。这样数据可追溯、可被后续 cheat-recommend 复用。
---
## 给 maintainer 的扩展指南
新增一个 trend source
1.`adapters/trend-sources/<name>.md`,按现有 aihot.md / trendradar-mcp.md 的格式
2. 在本文件"数据源路由"段加一行——明确该 adapter 适合的 content_form
3. 不需要改 cheat-seed 内部逻辑——按 `enabled_trend_sources` 自动启用
4. CHANGELOG 标 MINOR
不要把硬编码"aihot"/"trendradar-mcp" 写进 cheat-seed SKILL.md——保持 adapter 模型可扩展。
+149
View File
@@ -0,0 +1,149 @@
# Migration Protocolschema 演进哲学)
`cheat-migrate` skill / `cheat-init` / SessionStart hook / 维护者引用。规定如何安全演进 `.cheat-state.json` schema 而不让老用户被打断。
---
## 核心原则
1. **每个 release 必须能让老用户的旧 state 工作**——通过 migrate 升级,或通过 `state.get(field, default)` 兼容
2. **MINOR 改动 = 加字段 / 软化 enum**;老 state 不跑 migrate 也能工作(字段缺失用默认值),跑了让 state 完整
3. **MAJOR 改动 = 删字段 / 重命名 / 改语义**;老 state **必须**跑 migrate,否则 skill 读到不一致字段会出错
4. **不允许跳版**;多版升级必须按顺序应用每个 step。每步幂等
5. **失败停在原地**;不回滚,让用户在断点修复后继续
6. **schema_version 是单调递增**;不允许降级(如需降级,cp 历史 git 快照)
---
## 何时算 MINOR vs MAJOR
### MINOR 范围(不需要 migrate 也能跑老 state
- 新增字段(默认值定义良好,老 skill 不读它也不出错)
- 软化 enum 取值(如 `"strict" / "lenient"` 加第三个 `"adaptive"`,老值仍合法)
- 给字段加新可选取值(如某个 list 字段加新元素)
- 改默认值(不改语义)
### MAJOR 范围(必须跑 migrate
- **删除字段**——老 skill 仍写它,新 skill 不读它,会出歧义
- **重命名字段**——老/新 skill 看不到对方写的
- **改字段语义**(如 `mode` 从 enum 改为整数;`baseline_plays` 从 int 改为 list
- **改 enum 取值**(如 `"opinion-video"` 改成 `"opinion_video"`,老值不再合法)
- **拆字段 / 合字段**
> 模糊地带建议**保守判定为 MAJOR**——多写一份 migration 文件比让用户的 state 出错更好。
---
## 维护者 checklistbump schema 时必做的 4 件事
每次准备 release 时如果改了 state schema
### 1. 改 cheat-init 写新 state 的硬编码 schema_version
```diff
- "schema_version": "1.1",
+ "schema_version": "1.2",
```
位置:`skills/cheat-init/SKILL.md` Phase 3 的 state 写入段。
### 2. 改 migrations/registry.md 的 LATEST_SCHEMA 标记位
```diff
- LATEST_SCHEMA = "1.1"
+ LATEST_SCHEMA = "1.2"
```
并在"版本链"表追加新行:
```
| 1.1 | 1.2 | NO/YES | [1.1-to-1.2.md](1.1-to-1.2.md) | 一句话描述 |
```
### 3. 写 migrations/<old>-to-<new>.md
4 段必填(参考 `1.0-to-1.1.md` 模板):
- WHAT changed
- WHY
- HOW (Claude steps for /cheat-migrate)
- Manual fallback
> 写不出 4 段 = 改动太复杂没想清楚 = 不该 release 这次 schema bump。
### 4. CHANGELOG.md 标版本号 + 链接
```markdown
## [0.2.0] — YYYY-MM-DD
### BREAKING / MINOR
- schema_version 1.1 → 1.2: <一句话描述>。迁移指南:[migrations/1.1-to-1.2.md](migrations/1.1-to-1.2.md)
- ...
```
MINOR 用 `### MINOR`MAJOR 用 `### BREAKING`,要醒目。
---
## skill 内部怎么读 state(防御式编程)
每个 skill 读 state 时**必须**用 `state.get(field, default)` 模式:
```python
# 好
target_cadence = state.get("target_publish_cadence_days", None)
benchmark_status = state.get("benchmark_status", "none")
shoots = state.get("shoots", [])
# 坏(老 state 没这字段会 KeyError
target_cadence = state["target_publish_cadence_days"]
```
理由:
- MINOR 升级时老 state 缺新字段——`get` 模式让 skill 自动用默认值
- 用户手改 state 删了字段——同上
- 减少 skill 内"必须先迁移才能跑"的强依赖
**例外**:核心标识字段允许直接索引(如 `state["schema_version"]``state["rubric_version"]`)——这些缺失意味着 state 文件根本不合法,应该明确报错。
---
## SessionStart hook 的角色
hook 在每次会话开始时检测:
```bash
state_schema=$(jq -r '.schema_version // "unknown"' "$STATE_FILE")
if [[ "$state_schema" != "$LATEST_SCHEMA" ]]; then
echo "⚠️ schema 版本不一致:state=$state_schema, skill 期望=$LATEST_SCHEMA"
echo " 建议跑 /cheat-migrate 升级(不阻塞继续工作)"
fi
```
**非阻塞**:用户可以选择"先继续工作,回头再跑 migrate"。MINOR 不一致时大部分功能仍能跑;MAJOR 时部分 skill 可能报错——这时再跑 migrate 也来得及。
---
## 给开发者:避免 schema 频繁 bump 的实践
不是每个改动都需要 schema bump。下面是哲学:
- **优先 MINOR**:能加字段就加字段,少删字段。删字段让老用户不爽
- **批量 bump**:积攒 3-5 个 MINOR 一起 release 比每次小改都 bump 要友好
- **延迟 bump**MINOR 字段如果 90% 用户用不到,**不**急着 bump schema——可以让该字段 `state.get(field, default)` 默默 work,等下次 release 顺路 bump
- **避免 MAJOR**:能用 MINOR 解决的绝不上 MAJOR。例:与其重命名字段,不如保留旧字段 + 加新字段(旧的标 deprecated,下个 MAJOR release 才删)
---
## 备份保留策略
`/cheat-migrate` 写之前会备份到 `.cheat-state.json.backup-<timestamp>`
备份保留多久:
- 用户跑 `/cheat-status` 时,如果有备份 + state 已稳定运行 N 天 → 提示"可以清理 N 个旧备份"
- `/cheat-init` 重 init 时清理所有旧备份(既然要重 init,老备份意义不大)
- 用户手动 `rm .cheat-state.json.backup-*` 永远 OK
不入版本控制:`.cheat-state.json.backup-*` 应在 `.gitignore` 里(已含 `.cheat-state.json` 通配规则)。
+210
View File
@@ -0,0 +1,210 @@
# Observation Lifecycle(观察生命周期协议)
被这些子 skill 引用:`cheat-retro``cheat-bump`、主 SKILL.md。
这是项目原则 #3 的完整规范——**rubric 是工作台,不是博物馆**。任何对 `rubric_notes.md` 中观察条目的增删都必须遵循本协议。
---
## 三个生命阶段
每条观察都在下列状态之一:
```
[新增] → [观察记录] → [跨视频观察] → [规律沉淀] / [被吸收为维度] / [被推翻]
[待验证假设](暂存)
```
| 阶段 | 在 rubric_notes.md 的位置 | 触发 |
|---|---|---|
| **观察记录** | `## 观察记录` 段 | 单次复盘后写入。每篇视频复盘对应一条 |
| **跨视频观察** | `## 重大跨视频观察` 段 | 同一个 pattern 在 ≥2 个样本里出现 |
| **规律沉淀** | `## 规律沉淀区(高置信度)` 段 | 已有 ≥2 样本支持且**通过升级验证流程**(即被吸收进维度或被显式确认) |
| **待验证假设** | `## 待验证假设` 段 | 单样本观察暂存,等更多样本 |
---
## 升级到下一阶段的门槛(**Claude 判断为主,下面是参考默认**)
| 从 → 到 | 默认参考门槛 | Claude 判断信号强度(可软违反) |
|---|---|---|
| 观察记录 → 跨视频观察 | 同 pattern ≥2 样本 | 1 样本 + 评论区 ≥2000 赞的强模因证据也可升 |
| 跨视频观察 → 待验证假设 | 单样本 + 强信号但还没复现 | — |
| 跨视频观察 → 规律沉淀 | ≥2 样本 + 不需要改公式 | 1 样本 + 强反例(≥3x 偏差)也可升(标 `**Single-sample, high-confidence**` |
| 跨视频观察 → 维度(不再单独存在) | ≥3 样本 + 通过 bump 流程被吸收 | 同 cheat-bump 的 READINESS_HEURISTIC |
| 任意 → 删除 | 被新数据推翻 / 被吸收为维度 / 被沉淀为规律 | `cheat-bump` cleanup pass,或单独操作 |
**核心原则**:样本数是**信号强度的代理指标**,不是信号本身。3 个清晰、可追溯到具体数据点的样本 > 10 个零碎、低置信度的样本。
**Claude 软违反的纪律**
- 标注 `**Promoted with N samples (default expects M)**: <为什么仍然成立>`,让用户审视
- 不连环软违反——如果 Claude 在最近 3 次升格里有 2 次以上软违反,cheat-status 提示"你的观察升格判断可能太激进,建议回到默认门槛 review"
### Sample size → 允许的动作(细化分级)
按校准池**总样本数**决定能做什么改动——粗暴改大动作需要更多证据:
| 校准池规模 | 允许的动作 | 备注 |
|---|---|---|
| **1 个** | 记录"单次观察" | 单点不能触发任何规则改动,只能作为种子 |
| **2-4 个** | 提炼"候选规律" + 升级到"跨视频观察"段 | 仍不能改公式 |
| **5-9 个** | 修正维度定义(**定性变更**——如把 SR=5 的门槛说更严)| 不动权重数值,只改判断习惯。**第一次正式 bump 也在这一档**(rubric 形态首次成型) |
| **10-19 个** | 微调权重 ±0.2(**定量变更**| 如 ER ×1.5 → ×1.7;新增 / 删除维度仍属"定性大动作",需要更多样本 |
| **20+ 个** | 用回归反推权重(**数据驱动**| Spearman correlation 之类,可作为 bump 依据 |
**关键纪律**
- 不要在 N=5 时就用回归——5 个点拟合 7 维公式必过拟合
- 不要在 N=20 时还用直觉调权重——已经有数据信号了
- bump 协议(`bump-validation-protocol.md`)的 `MIN_SAMPLES_FOR_BUMP=5` 是**形态首次成型**的最低门槛,不是"开始数据驱动"的门槛——后者要 N≥20
---
## 删除规则(最容易做错的部分)
**两类条目必须删,不能保留**
### 类型 A:被吸收为维度的观察
例:v2.1 升级时,"观察 E(致谢段二创量级是 ER=5 的最强外部证据)"被吸收为新维度 **MSMemetic Shareability**
→ 升级落地后,**删除**"观察 E"这条观察记录。维度 MS 本身就是新归宿。
**理由**:保留观察 = 同一概念在文件里出现两次(一次作为维度,一次作为观察)。读者会困惑:这是历史还是仍生效的规则?
### 类型 B:被新数据推翻的观察
例:观察 X 提议"长视频天花板低",后来 4 个长视频样本都破 50w → 推翻。
→ **删除**这条观察。
**理由**:保留 = 让未来的你或其他读者基于错误规则打分。
---
## 必须保留的条目
下列条目**不删**,留在原处或迁移到合适段:
- **未解决的观察**(既未被吸收也未被推翻)→ 跨升级时迁移到新版本的"待验证假设"段
- **历史校准事件**(如"v1 → v2 升级是因为房价 259w 严重低估")→ 保留在版本日志的"升级 Memo"段,**不**保留在观察段
- **本版本仍生效的规律**(已沉淀到"规律沉淀区")→ 保留
---
## 不允许的反模式(**必须拒绝**)
下列模式都是"博物馆冲动"——把 rubric_notes.md 当历史档案:
| 反模式 | 为什么不行 |
|---|---|
| `~~ER 权重 1.5~~` `**改为 2.0**`(带删除线的旧值) | 用 git history 看旧值,不要在文件里堆 |
| "我曾经以为 SR 重要,但其实..." | 这种考古条目让读者读完不知道当下规则是什么 |
| "v1 时代 NA 是关键,v2 后发现不是" | 同上。删掉 NA 相关条目,留版本 memo 解释为什么 NA 被砍 |
| "保留这条观察作为反例" | 反例的位置是版本 memo 的"被推翻假设"小节,不是观察段 |
| "下个 bump 再删,先放着" | 拒绝。bump 落地的同一次操作里删干净 |
git history 才是真正的归档。`rubric_notes.md` 是当前生效规则的快照——读者打开它应该看到**今天该怎么打分**,而不是过去几个月的演化史。
---
## Blind channel leak guard**任何 skill 写 rubric_notes.md 时必须遵守**
`rubric_notes.md` 是 cheat-score-blind sub-agentchannel B)的**白名单**——sub-agent 读这个文件给 script 打分时,文件内容不能含**已发布作品的实绩数据**,否则 sub-agent 的"盲"被破坏。
**禁止写入 rubric_notes.md 的 pattern**
| 模式 | 例子 | 替代位置 |
|---|---|---|
| 真实视频标题 + 实绩 | "「停止期待」播放 71.1w" | rubric-memo.md |
| 派生证据带命名锚 | "派生证据:「她不一样」MS=5 → 实绩 124.8w" | rubric-memo.md |
| 校准池重打表 | bump 时的样本对照表 | rubric-memo.md |
| 跨模型审核引用含数字 | "audit: 「老板废话」rank 一致 ✓" | rubric-memo.md |
| 含 `\d+w` / `\d+万` / `\d+M` / `\d+k` 的数字(**除 bucket 边界** | "实绩 13.7w" | rubric-memo.md |
**允许的 pattern**(通用语言):
- 公式:`(ER×2.0 + HP×1.5 + ...) / N × M`
- 维度定义:`ER(情感共鸣):0=无情绪 / 3=中等共鸣 / 5=极端共鸣`
- 派生证据**抽象化**`派生证据:高抽象密度样本 → CC=1 → 低 reach`
- Bucket 边界(数字属于规则不属于实绩):`5-30w / 30-100w / 100-150w / >150w`
- 观察 ID + 一句话:`观察 E:开头 5 秒含问句 → ER+`(不带样本名 / 不带实绩)
**强制 cheat-bump Phase 5 末尾自检**:写完 rubric_notes.md 后跑 `grep -E '\\d+\\s*[wWmMkK万]|播放|实绩|实际'` → 命中 → abort + 回滚。这是硬约束。
历史背景:PR #11 引入 cheat-score-blind 时漏算了这条——cheat-bump 把 Memo 写进 rubric_notes.mdsub-agent 通过白名单读到实绩。PR #12 修复(拆 file)+ 加本段约束防止将来再犯。
---
## cleanup pass 的强制时机
`cleanup pass` = 把所有满足"可删除"条件的观察一次性清掉。
强制触发:
- **bump 落地的最后一步**(见 `bump-validation-protocol.md` Step 5
- 用户显式触发"清算"操作(罕见)
非强制但建议:
- 校准池每加 5 个样本,主动审视"观察记录"段,看哪些已经可以升格 / 删除
- `cheat-status` 看板检测到 `rubric_notes.md` 行数超过 500 时,建议清算
---
## 行数预算
健康 `rubric_notes.md` 的体积参考(目标):
| 校准池规模 | 健康行数 | 警戒行数 |
|---|---|---|
| 0-5 篇 | 100-200 | >300 |
| 5-20 篇 | 200-400 | >500 |
| 20-50 篇 | 300-500 | >700 |
| 50+ 篇 | 400-600 | >800 |
行数超警戒 → 必跑一次 cleanup pass。这不是为了好看,是为了**读者打分前能在 60 秒内读完核心规则**。
---
## 与"观察记录"模板的对齐
每条观察记录的标准模板(`rubric_notes.template.md` 也用这个):
```markdown
### YYYY-MM-DD [标题简称] (id) — [一句话定性,如"验证 ER 主导"]
- 预测:composite=X.XXbucket=Y
- 实绩:播放 / 点赞 / 评论 / 转发(带 T+Nd 标注)
- Top 评论关键词:[简短摘录 + 赞数]
- 判断:哪个维度被验证 / 推翻?为什么?
- Rubric 调整:[如果有,写明 "下次打 XX 类文章时改 YY"]
- 详见:[predictions/<file>.md]
```
跨视频观察的模板:
```markdown
### 观察 X — [一句话规律]
**证据**
- 样本 1(composite, 实绩):核心数据
- 样本 2(composite, 实绩):核心数据
- 样本 3(composite, 实绩):核心数据
**假设**:[如果规律成立,意味着什么 rubric 调整]
样本数:N 个,[已超 / 待补]。
```
---
## 与 bump 协议的衔接
`cheat-bump` 在 Step 5cleanup pass)必须按以下顺序处理观察段:
1. 列出所有 `## 观察记录` 段条目
2. 对每条问 3 个问题:
- 它是否被本次 bump 吸收为正式维度?→ 删
- 它是否被本次 bump 的新数据推翻?→ 删
- 它是否仍未解决?→ 迁移到"待验证假设"段
3. 列出所有 `## 重大跨视频观察` 段条目
4. 对每条问相同 3 个问题
5. 把"已被验证但不需改公式"的规律 → 迁移到"规律沉淀区"
6. 最后**重新阅读** `rubric_notes.md` 全文,确保读者打开能在 60 秒内理解当下打分规则
+332
View File
@@ -0,0 +1,332 @@
# Prediction Anatomy(预测日志解剖)
被这些子 skill 引用:`cheat-predict``cheat-retro``templates/prediction.template.md`
**所有预测都用统一格式**——7 个必备组件 + 复盘段。Confidence 等级(基于 `calibration_samples` 派生,见 [state-management.md](state-management.md) 的 confidence 表)作为 header 字段标注,告诉用户这次预测有多可信,**但不改变预测格式本身**。
> **为什么不分 cold-start 简化版 / complete 完整版(旧设计的弃疑)**:
> 把 cold-start 切成简化版是基于"前 5 篇 bucket 数字是 false precision"的担忧。但更好的解决方案是**显示 confidence 等级**——天气预报永远报具体温度,再标置信度,不会因为预报员经验少就不给数字。
> 双版本切换还引入了"第 5 篇突然解锁完整版"的复杂度跃迁——用户体验割裂。统一格式 + 渐进信心标注更平滑。
参考真实样本:参考博主项目(私有,含 25+ 视频校准)—— 工具的设计灵感与 rubric 权重均来自此实测。
---
## 7 个必备组件
### 组件 1File header(文件头)
```markdown
# <标题> — 预测日志
**Article ID**: <12 位 hash> (sha256 of scripts/<id>.md initial content, 取前 12)
**Title**: <作品完整标题>
**Rubric Version**: **v0** | **v1** | **v2** | ...
**预测时间**: 2026-05-04(基于最终稿)
**Script Path**: scripts/2026-05-04_<id>_<short>.md
**Script Hash**: <sha256:12 of script content at predict time>
**Target Duration (s)**: 240 (state.typical_duration_seconds 派生)
**Actual Script Length**: 980 字 (从 Script Path 文件读)
**Calibration Samples (at predict time)**: 3
**Confidence**: 🟡 偏低 (中枢 ±40%,可作为参考之一)
**Prediction Basis**: pre_shoot ← 或 `post_shoot_pre_publish`v2 段)
**Scored By**: claude ← 或 `claude+user_override`
**BlindScored By**: subagent-v1 ← 或 `main-claude-self` / `mixed`
**BlindScore Disagreement**: <inline JSON 见下方>
**User Override**: none ← 或列出被覆盖字段
**预测时数据状态**: **blind**(未看任何 <平台> 实际播放数据)
```
`BlindScore Disagreement` 字段是 inline JSON 数组,**每维度一行****delta=0 也必须记**
```json
[
{"dim": "ER", "blind": 5, "self": 5, "delta": 0, "decided_as": 5},
{"dim": "SR", "blind": 3, "self": 4, "delta": 1, "decided_as": 3},
{"dim": "AB", "blind": 2, "self": 4, "delta": 2, "decided_as": 4, "user_decision": "b"},
{"dim": "HP", "blind": 5, "self": 5, "delta": 0, "decided_as": 5}
]
```
- `blind`sub-agent 给的分
- `self`:主 Claude 自估(Phase 2 末尾 internal 估值,不落盘的那份现在落盘了——这是必要的诚实代价)
- `delta`|blind - self|
- `decided_as`:进入 composite 计算的最终值
- `user_decision`(如有):Phase 2.5 用户裁定时的选项 `a` / `b` / `c <number>`——只在 delta ≥ DISAGREEMENT_THRESHOLD 时出现
字段必填规则:
- `Rubric Version` 必填——将来 v3 时代回看 v2 预测,没有版本号就无法公平对比
- `预测时数据状态` 必填——明确声明 blind 是 immutable 承诺的前提
- `Script Path` 必填——指向 `scripts/<id>.md`pre-shoot 草稿)
- `Script Hash` 必填——cheat-shoot 时再 hash `videos/<id>/script.md`,不一致 → 复盘段加 integrity warning
- `Calibration Samples` + `Confidence` 必填——告诉读者这次预测有多可信。**Confidence 自动派生**自 calibration_samples(见 state-management.md
- `Prediction Basis` 必填——`pre_shoot` 为标准盲预测;`post_shoot_pre_publish` 为 v2 拍后改稿重判(仍未见数据,但软盲)
- `Scored By` 必填——告诉读者这次预测是 Claude 全自动还是用户介入改过:
- `claude`Claude 主动打分 + bucket + 概率,用户 review 后回 "ok" 接受
- `claude+user_override`:用户在 review 阶段挑刺改了某些字段
- **`BlindScored By` 必填**——本次维度分由谁打:
- `subagent-v1`:通过 Task tool 调 cheat-score-blind sub-agent 拿到的盲打分(默认,Phase 2 路径)
- `main-claude-self`:用户 `--skip-blind` flag 或 Phase 2.5 选 b(信主 Claude 自估)——同时 `state.last_prediction_self_scored=true`
- `mixed`Phase 2.5 用户选 c 给个别维度自定分,其他维度仍走 sub-agent
- **`BlindScore Disagreement` 必填**——上方 JSON。**所有维度必记**(即使 delta=0),不允许"只记差异大的"。理由:复盘时按 delta 分布分析"哪类维度 sub-agent 与主 Claude 系统性分歧"是 rubric 演进的重要信号
- `User Override` 必填(如有覆盖)——列出哪些字段从 X 改成 Y,附用户给的理由。复盘时这个字段帮诊断:用户的覆盖被实绩验证(用户直觉准)→ rubric 可能漏了什么
---
### 组件 2:输入快照(Input snapshot
记录**预测时**的稿子状态——尤其是用户的最终改动。
```markdown
## 输入快照
**分数 (vN)**: ER5 / HP5 / QL5 / NA3 / AB5 / SR2 / SAT4 → composite=**8.24**
**用户改写要点 vs Claude 草稿(如有)**:
- **开头**user 砍掉 EWDM 模型名和铺垫
- **砍掉**:[具体段落 / 概念名 / 铺垫]
- **保留**:[关键的金句 / 致谢段 / 主体结构]
- **节奏**:比草稿 [紧 / 松] 约 N%
```
> 如果是用户从零写的(没用 cheat-seed),这一段写"用户原创稿,无 Claude 草稿对照"。
---
### 组件 3:预测主体(Prediction)⭐ immutable 段
这是 immutable 段的核心。`hooks/prediction-immutability.sh` 拦截这段往后到下一个 `##` 的所有 Edit。
```markdown
## 预测 v1
**Bucket**: `30-100w`
**内心概率分布**:
- `<5w` → 3%
- `5-30w` → 22%
- **`<headline bucket>` → 55%**(中枢 ~50w
- `>100w` → 17%
- `>150w` → 3%
**一句话 reason**:
> ER=5+AB=5 暗恋普适受众;IS 直接锁定;7.3天+零信号反转+MVP金句情绪曲线完整;SR=2 无社会议题托底是天花板瓶颈;预计 40-60w 中枢。
```
强制要求:
- **Bucket** 必须是预定义的 5 个之一
- **概率分布** 必须加起来 100%——这是逼你诚实的工具
- **中枢** 是该 bucket 内的点估计,便于复盘判断"偏高 / 偏低"
- **一句话 reason** 浓缩到 DB 字段,便于跨样本检索
**关于 cold-start 期的 bucket**calibration_samples 少 → 概率分布**应该更平**(如 30/30/20/15/5 而非 5/40/45/8/2)。Confidence 低不代表跳过 bucket,而代表对 bucket 该有合适的不确定度。
---
### 组件 4:推理因素表(Reasoning factors
每个驱动判断的因素 + 方向 + 置信度 + 说明。
```markdown
## 推理因素
| 因素 | 方向 | 置信度 | 说明 |
|---|---|---|---|
| ER=5 | 强 + | 高 | "半夜三点翻聊天记录"极端具象 |
| IS 钩子 | 强 + | 高 | "仅影响 X 的人"一句锁定受众 |
| SR=2 | 强 - | 高 | 无社会议题托底,纯个人情感天花板有限 |
| 数据+金句路线 | 中 ? | 低 | 对算法友好度未验证 |
```
**置信度** 分三档:高(强证据 + 多锚点支持)、中(有理由但样本少)、低(凭直觉)。
- 复盘时如果"低置信度"因素被验证 → 直觉强
- 如果"高置信度"因素被推翻 → rubric 有 bug
---
### 组件 5:锚点对比(Anchor comparison
找 2-4 个 composite 接近的旧样本,列出它们的实绩。
```markdown
## 锚点对比
| 对照样本 | composite | 实绩 | 异同 |
|---|---|---|---|
| 仓鼠 | 9.41 | ~150w | composite 低 1.17,但路线差异大(类比讲解 vs 数据+金句) |
| 房价 | 9.41 | 259w | SR 差 3 分(2 vs 5 |
| 谁问你了 | 8.24 | T+8d 11.7w | 同 composite 但 ER 5 vs 3SR 2 vs 4 |
```
锚点的价值:抓出公式抓不到的错误。
**校准样本不够时**< 2 个 composite 邻近的已发样本):
```markdown
## 锚点对比
校准池只有 N 个样本,无 composite ±0.5 邻近样本。**锚点对比 N/A**——
注意这次预测的 confidence 标注是 🟡 偏低 / 🔴 极低,bucket 中枢仅供参考。
```
**仍然写出这段**——告诉读者锚点为何缺。不是把段落删掉。
---
### 组件 6:反事实场景(Counterfactual scenarios
每个可能的 bucket 写一段"如果落在这里,意味着什么"。
```markdown
## 反事实场景
**如果爆 `>X w`**X% 预期):
- [验证什么假设]
- [推翻什么假设]
- [可能新增什么 rubric 维度]
**如果落在 `headline bucket`**X% 预期):
- [基准线验证什么]
**如果跌到 `<X w`**X% 预期):
- [推翻什么核心判断]
**如果 `<<X w`**X% 预期):
- [极端场景的可能解释]
```
为什么必填:复盘时**实际落在哪个 bucket** 直接告诉你 rubric 的哪个假设被测试。没有反事实场景,复盘退化为"这次准 / 不准"——没有诊断价值。
---
### 组件 7:关键校准假设(Critical calibration hypothesis
可选但强烈推荐:把这次预测当成一次实验,明确写下"如果 X 发生,证明 Y"。
```markdown
## 关键校准假设(对比谁问你了)
两篇同 composite=8.24,差异:
- 怎么停止期待:ER=5 / SR=2
- 谁问你了:ER=3 / SR=4
**我押:本篇 > 谁问你了(比率 1.5-2x**
如果反过来 → rubric 里 SR 权重应上调,ER 权重应下调
如果差距 < 1.3x → rubric 基本 OK,差异在噪声范围
```
校准假设是 rubric 升级的种子——单条假设被 ≥3 样本验证 → 进入 bump 候选。
**校准样本不够时**:写"无可对照样本——仍写下我对这次的核心赌注(即使没有锚点)",然后写一两条这次想测的事。**不要删掉这段**。
---
### 组件 ∞:复盘段(Retrospective)— 仅追加
发布后 T+N 天复盘时追加。**不修改预测段任何字符**。
```markdown
## 复盘
**复盘时间**: 2026-05-07(发布 T+3d
**抓取时间**: 2026-05-07 09:30
**数据来源**: manual paste / adapter:douyin-session
### 实绩数据
- 播放:71.1w(落在 `30-100w` 桶内偏高,相对中枢 50w **+42%**
- 点赞:2.4w(赞播比 3.38%
- 分享:1.8w(分播比 2.53%,强)
### Top 评论关键词
- 「她不一样」模因爆发:2266 赞独占榜首,全文 12+ 次变体
### 哪些预测被验证 / 推翻
**被验证 ✅**:
- 关键校准假设完全成立:本篇 71.1w / 谁问你了 11.7w = 6.07x
- ER=5 主导情感传播力 → H1 强证据
**被推翻 ❌**:
- 中枢 50w 被超出 +42%
- 我对 SR 的押注反向被推翻:SR 在情感向场景应下调
### 需要写进 rubric-memo.md 的新观察
1. ER 在情感向场景的真实权重应 ≥ ×2.0
2. 议题分享冲动 (TS) 是隐藏维度
```
---
## 完整结构总览
```
file: predictions/YYYY-MM-DD_<id>_<short>.md
# 标题 — 预测日志 ← 组件 1: header(含 confidence + script_hash + Prediction Basis + BlindScored By + BlindScore Disagreement
metadata block
## 输入快照 ← 组件 2
scores + 用户改写要点 vs Claude 草稿)
## 预测 v1 ← 组件 3 ⭐ IMMUTABLE 起点(基于 pre-shoot 草稿)
bucket + 概率 + 中枢 + 一句话 reason
## 推理因素 ← 组件 4
(带方向 + 置信度的表)
## 锚点对比 ← 组件 5(校准池不够时仍写"N/A 段"
## 反事实场景 ← 组件 6
(每 bucket 一段"意味着什么"
## 关键校准假设 ← 组件 7
(这次预测作为实验的明确赌注)
## 预测 v2 (replaces v1) ← (可选) 拍后改稿 ≥30% 时由 cheat-shoot 触发,append 不覆盖
(同 7 组件结构 + 头部含 Diff vs v1 摘要)
## 复盘 ← 仅追加,IMMUTABLE 边界
(实绩 + top 评论 + 验证/推翻 + 新观察)
```
### v1 / v2 段约定
- **新建文件**cheat-predict 写 `## 预测 v1`(不再裸 `## 预测`——为 v2 留 schema 一致性)
- **legacy 兼容**v0.1.0 时期写的 `## 预测` 文件不动;hook 与 cheat-retro 都识别
- **v2 触发条件**cheat-shoot 检测拍摄稿与 `scripts/<id>.md` 的 line-diff ≥ 30%[V2_TRIGGER_THRESHOLD](../skills/cheat-shoot/SKILL.md)),调用 `/cheat-predict — mode: v2 — prediction-file: <path>`
- **append 而非覆盖**v2 段插在 `## 复盘` 之前。v1 段**绝不**修改(hook 物理强制)
- **校准用谁**cheat-retro 读最后一个 `## 预测 vN` 算偏差;v1 留作历史档案
- **diff 学习**v1 vs v2 的字段差异(如 ER 4→5)就是用户改稿带来的判分变化,是 rubric 升级证据
### Prediction Basis 字段
prediction header 必含 `Prediction Basis`
- `pre_shoot`v1 默认,标准盲预测)
- `post_shoot_pre_publish`(v2,软盲预测——拍后改稿但发布前重判)
cheat-retro 用此字段在 score-curve / bump 校准时区分两条数据线,避免混样。
---
## 子 skill 验收标准
`cheat-predict` 写完一份预测后,必须自检 7 个组件齐全:
- 组件 5 / 7 在校准样本不足时仍写"N/A 段 + 解释"**不允许直接跳过**
- header 的 `Calibration Samples` + `Confidence` 必填——读者一眼看到这次预测多可信
`cheat-retro` 写复盘段时,必须先校验该文件的 7 个组件:
- 缺组件 → 警告"该 prediction 不规范,复盘价值打折"
- 复盘段格式与 confidence 等级**无关**——任何阶段复盘都是同一格式
- diff `Script Hash` 与当前 `videos/<id>/script.md` 的 hash → 不一致则在复盘段加 `**Script changed between predict and shoot**` 警告
---
## 与旧设计的对照(v1 用户迁移参考)
| v0 设计(已弃) | v1 设计(当前)|
|---|---|
| `prediction_complexity = cold-start-simple` 用 3 组件简化版 | 删除字段。所有预测都用 7 组件统一版 |
| `prediction_complexity = complete` 用 7 组件 | 同上——一直就是 7 组件 |
| 第 5 次复盘"解锁完整预测" | 不需要解锁——一直完整。Confidence 等级随 calibration_samples 自动提升 |
| Cold-start 期跳过 bucket / 锚点对比 / 反事实 | **不跳过**——锚点不够就显式标"N/A",bucket 该写还写(概率分布需诚实平摊) |
| 用 mode=cold-start 字段判断流程分支 | 删除字段。所有 skill 走同一流程,按 calibration_samples 渐进显示 confidence |
+346
View File
@@ -0,0 +1,346 @@
# State Management(状态文件读写约定)
被所有子 skill 引用。`.cheat-state.json` 是各子 skill 共享上下文的**单一来源**——任何运行时状态、累计指标、模式标记都从这里读、写回这里。
---
## 文件位置
```
<user-content-project>/.cheat-state.json
```
**绝不**放到全局 `~/.claude/` 或 cheat-on-content 自己的目录——一个用户可能维护多个内容项目,每个项目独立状态。
---
## 完整 schema
```json
{
"schema_version": "1.4",
"skill_version": "1.0.0",
"rubric_version": "v0",
"content_form": "opinion-video",
"typical_duration_seconds": 240,
"target_publish_cadence_days": 2,
"rubric_form_mismatch": false,
"benchmark_status": "none",
"benchmark_name": null,
"benchmark_sample_count": 0,
"baseline_plays": null,
"calibration_samples": 0,
"calibration_samples_at_last_bump": 0,
"data_collection": "manual",
"pool_status": "none",
"data_layer": "markdown",
"hooks_installed": false,
"enabled_trend_sources": ["manual-paste"],
"enabled_perf_adapters": [],
"last_bump_at": null,
"last_bump_self_audited": false,
"last_published_at": null,
"last_published_file": null,
"last_retro_at": null,
"last_trends_run_at": null,
"last_trends_added_count": 0,
"last_prediction_self_scored": false,
"last_self_scored_at": null,
"consecutive_directional_errors": [],
"pending_retros": [],
"shoots": [],
"in_progress_session": null,
"initialized_at": "2026-05-04T15:00:00+08:00"
}
```
### 关键变更(v1.4
相比 v1.3**MINOR but BREAKING for blind channel integrity**——老用户必须跑 migrate):
- **rubric 文件拆分**`rubric_notes.md``rubric_notes.md`(公式 + 通用维度定义;blind 白名单)+ `rubric-memo.md`(升级 Memo 含证据 + 派生证据;blind 硬禁读)
- **state 字段不变**——仅 `schema_version` bump 标识老用户须跑迁移把现有 rubric_notes.md 拆成两份文件
- 配合 [skills/cheat-score-blind/SKILL.md](../skills/cheat-score-blind/SKILL.md) 的 `blocked_rubric_memo` refusal_code + cheat-bump Phase 5 leak guard 自检
- **不跑 migrate 的后果**blind sub-agent 仍会读到 rubric_notes.md 里的实绩,sub-agent 会自报 `non_blind_warning` 并降所有 confidence 到 medium——可用但不再是"真盲"
- 详见 [migrations/1.3-to-1.4.md](../migrations/1.3-to-1.4.md)
### 关键变更(v1.3
相比 v1.2MINOR,兼容):
- **新增 `last_prediction_self_scored: bool`**——`true` 仅当上一次 `/cheat-predict` 走了 `--skip-blind` flag 或 Phase 2.5 用户选 b(信主 Claude 自估)。cheat-status / SessionStart hook 据此 nag"上次预测没走 blind sub-agent,已 N 天"
- **新增 `last_self_scored_at: ISO 8601 / null`**——`last_prediction_self_scored` 触发时的时间戳;走 sub-agent 时一起清回 null
- 配合 [skills/cheat-score-blind](../skills/cheat-score-blind/SKILL.md) 的 channel B 隔离协议——把 contamination 跟踪从"靠 git history"升级为"靠 state 字段"
- 老 state 缺这两字段 → 兜底 `false` / `null`**MINOR 兼容**
### 关键变更(v1.2
相比 v1.1MINOR,兼容):
- **`shoots[]` 项 schema 扩展**——新增 `scripts_path``script_consistency``script_diff_pct``v2_prediction_written``script_hash_at_shoot` 字段。语义见 cheat-shoot Phase 4。这些字段记录"拍后改稿是否触发 v2 预测重判"cheat-retro 据此决定读 `## 预测 v1` 还是 `## 预测 v2`
- 老 state 缺这些字段 → skills 用 `state.get(field, default)` 兜底(`script_consistency` 默认 `"consistent"``v2_prediction_written` 默认 `false``script_diff_pct` 默认 `null`)。**不强制跑 migrate**——但跑了让 state 字段对齐 schema 文档
### 关键变更(v1.1
相比 v1.0
- **删除 `mode`**"cold-start" / "calibration" 二元)→ 用 `calibration_samples` 整数判断状态
- **删除 `prediction_complexity`**"cold-start-simple" / "complete" 二元)→ 所有预测都用统一完整 7 组件结构,**confidence 等级派生自 calibration_samples**
- **删除 `bucket_scheme`**"ratio" / "absolute" / "absolute_with_ratio" / "percentile" 四档)→ bucket 边界由单一算法**自动派生**:有 `baseline_plays` → 按倍数;无 → 平台通用默认;样本 ≥10 → 重算 baseline
理由:硬模式切换是设计者的猜测,不是用户体验该有的样子。统一流程 + 渐进信心标注更符合"频道是不断进化的连续光谱,不是离散阶段"的事实。
---
## 字段说明(每个字段的语义 + 谁写谁读)
### 元数据
| 字段 | 类型 | 写入者 | 读取者 | 说明 |
|---|---|---|---|---|
| `schema_version` | string | cheat-init / cheat-migrate | 所有 skill | "1.1"。schema 升级时 bump;老用户由 [/cheat-migrate](../skills/cheat-migrate/SKILL.md) 升级。详见 [migration-protocol.md](migration-protocol.md) |
| `skill_version` | string | cheat-init | 所有 skill | cheat-on-content 当前版本 |
| `initialized_at` | ISO 8601 | cheat-init | cheat-status | 首次初始化时间,永不变 |
### 模式与配置
| 字段 | 类型 | 取值 | 写入者 | 读取者 |
|---|---|---|---|---|
| `rubric_version` | string | "v0" / "v1" / "v2" / ... | cheat-init / cheat-bump | cheat-score / cheat-predict / cheat-retro |
| `content_form` | enum | "opinion-video" / "long-essay" / "short-text" / "podcast" / "other" / "mixed" | cheat-init | cheat-predict / cheat-recommend |
| `typical_duration_seconds` | int | 用户视频典型时长。决定 cheat-seed 写 draft 的字数 + cheat-predict 锚点优先同时长 | cheat-init | cheat-seed / cheat-predict |
| `target_publish_cadence_days` | int / null | 用户目标发布频率(1=日更 / 2=隔日 / 7=周更 / null=灵活)。决定 buffer 警戒颜色阈值 | cheat-init | cheat-status / cheat-recommend / cheat-shoot / cheat-publish / SessionStart hook |
| `rubric_form_mismatch` | bool | true 表示 content_form ≠ opinion-video 但仍用 opinion 内置 rubric 起步——提示用户 bump 时调权重 | cheat-init | cheat-status(持续提示) |
| `benchmark_status` | enum | "none" / "imported" / "pending"(用户答应等下找)| cheat-init / cheat-learn-from | cheat-seedbrainstorm 时读 benchmark.md/ cheat-statuspending 时持续提醒) |
| `benchmark_name` | string / null | 对标账号名(如"蜗牛学长留学");none 时为 null | cheat-learn-from | cheat-status / cheat-seed |
| `benchmark_sample_count` | int | 已导入的对标视频条数 | cheat-learn-from(写入 / append | cheat-statusN≥10 时提示 benchmark 影响淡出) |
| `baseline_plays` | int / null | 用户基准播放数;首次 init 时若有抓取历史→中位数;无→null;后续 cheat-retro 第 1 篇有实绩时回填 | cheat-init / cheat-retro / cheat-bump (--bucket-only) | cheat-predict(派生 bucket 边界) |
| `data_collection` | enum | "manual" / "adapter" | cheat-init | cheat-retro(决定 DATA_SOURCE 默认值) |
| `pool_status` | enum | "none" / "markdown" / "notion" / "sqlite" | cheat-init / cheat-recommend | cheat-recommend / cheat-status |
| `data_layer` | enum | "markdown" / "sqlite" | cheat-init / md-to-sqlite.py | 所有读 predictions 的 skill |
| `hooks_installed` | bool | true / false | cheat-init | cheat-status(持续提示) |
| `enabled_trend_sources` | array of string | trend-source adapter 名列表(如 `["weibo-hot", "zhihu-hot"]` | cheat-init / 用户手动 | cheat-trends |
| `enabled_perf_adapters` | array of string | perf-data adapter 名列表(如 `["douyin-session"]`)。空 → cheat-retro 走 manual paste | cheat-init / 用户手动配置后 | cheat-retro |
### 累计计数
| 字段 | 类型 | 写入者 | 用途 |
|---|---|---|---|
| `calibration_samples` | int | cheat-retro(每次复盘 +1 | cheat-status 进度条 / cheat-bump 门槛 |
| `calibration_samples_at_last_bump` | int | cheat-bump | "距上次 bump 多少新样本" |
### 时间戳(last_X_at
| 字段 | 类型 | 写入者 |
|---|---|---|
| `last_bump_at` | ISO 8601 / null | cheat-bump |
| `last_bump_self_audited` | bool | cheat-bumpCROSS_MODEL_AUDIT=false 时 true |
| `last_published_at` | ISO 8601 / null | cheat-publish |
| `last_published_file` | string / null | cheat-publish |
| `last_retro_at` | ISO 8601 / null | cheat-retro |
| `last_trends_run_at` | ISO 8601 / null | cheat-trends |
| `last_trends_added_count` | int | cheat-trends |
| `last_prediction_self_scored` | bool | cheat-predict`--skip-blind` 或 Phase 2.5 选 b 时 true;下次走 sub-agent 时清回 false |
| `last_self_scored_at` | ISO 8601 / null | cheat-predict(跟随 `last_prediction_self_scored` 同步) |
### 列表队列
| 字段 | 类型 | 写入者 | 读取者 | 协议 |
|---|---|---|---|---|
| `consecutive_directional_errors` | array of "high"/"low" | cheat-retropush / cheat-bump(清空) | cheat-status / cheat-retro 自检 | 最近 N 次复盘的偏差方向;连续 3 同向触发 bump 提议 |
| `pending_retros` | array of file path | cheat-publishpush / cheat-retroremove | cheat-status | 等待复盘的预测文件路径 |
| `shoots` | array of {video_folder, prediction_file, shot_at, ad_hoc, scripts_path, script_consistency, script_diff_pct, v2_prediction_written, script_hash_at_shoot} | cheat-shootpush / cheat-publishremove | cheat-status / cheat-recommend / SessionStart hook | 已拍未发队列。`len(shoots) = buffer count``buffer_days = buffer × target_publish_cadence_days` 决定颜色。v1.2 新增字段语义见 cheat-shoot Phase 4 |
### 会话状态
| 字段 | 类型 | 写入者 | 读取者 | 协议 |
|---|---|---|---|---|
| `in_progress_session` | object / null | cheat-predict(创建) / cheat-publish(清除) | cheat-publish / cheat-status | 见下方"in_progress_session 子结构" |
#### `in_progress_session` 子结构
```json
{
"type": "prediction",
"file": "predictions/2026-05-04_a3f2c1d4e5b6_停止期待.md",
"started_at": "2026-05-04T14:00:00+08:00",
"rubric_version": "v2"
}
```
`type`:当前只有 `"prediction"`。未来可能加 `"bump"` 表示长流程 bump 在进行中。
---
## 读写协议
### 读(任何 skill
```python
# 伪代码
import json, os
state_path = os.path.join(os.getcwd(), ".cheat-state.json")
if not os.path.exists(state_path):
# 不存在 = 用户没初始化,路由到 /cheat-init
raise NeedsInitError()
with open(state_path) as f:
state = json.load(f)
# 检查 schema_version 兼容
LATEST_SCHEMA = "1.1" # see migrations/registry.md
if state.get("schema_version") != LATEST_SCHEMA:
# 不直接 raise — 提示用户跑 /cheat-migrate(非阻塞)
log_warning(f"schema 版本不匹配:state={state.get('schema_version')}, 期望={LATEST_SCHEMA}。建议跑 /cheat-migrate")
# MINOR mismatch 通常仍能继续;MAJOR 时部分字段读取可能 KeyError → 用 .get(field, default) 兜底
```
**关键纪律**
- 读完不立刻关心字段缺失——用 `state.get(field, default)` 容错。新版 skill 引入新字段时旧 state file 会缺该字段,应优雅默认而非崩溃
- **绝不**在内存里 mutate state 后忘记写回——下游 skill 读到的是磁盘版
### 写(任何 skill
```python
# 伪代码 — read-modify-write 模式
state = read_state()
state["calibration_samples"] += 1
state["last_retro_at"] = now_iso()
write_state(state)
def write_state(state):
state_path = os.path.join(os.getcwd(), ".cheat-state.json")
tmp_path = state_path + ".tmp"
with open(tmp_path, "w") as f:
json.dump(state, f, indent=2, ensure_ascii=False)
os.replace(tmp_path, state_path) # atomic rename
```
**关键纪律**
- **原子写**:写到 .tmp → rename。避免半写损坏的 state file
- **永远 indent=2**:人类可读,便于用户手改 + git diff
- **ensure_ascii=False**:保留中文字符不转 \uXXXX
- **写完再继续后续操作**:避免下游 skill 读到旧值
### 并发模型
预期场景:**单用户 + 单 Claude Code 会话**。不做锁。
如果两个会话并行操作同一个项目(罕见且不推荐):可能出现写覆盖。**未来需要时**可加文件锁(`fcntl.flock`);当前不加,避免引入复杂度。
---
## 字段写入责任表(防止"谁该写这个字段"歧义)
| 字段 | 唯一写入者 | 何时写 |
|---|---|---|
| `rubric_version` | cheat-init / cheat-bump | init 写初值;bump 升版 |
| `baseline_plays` | cheat-init / cheat-retro / cheat-bump (--bucket-only) | init 时若有 adapter 抓回历史→中位数;无历史→null;retro 第 1 篇有实绩→回填;bump --bucket-only→重新计算 |
| `calibration_samples` | cheat-retro | 每次复盘成功落盘 +1 |
| `pending_retros` | cheat-publishpush/ cheat-retroremove | publish 时 push 本次;retro 完成时 remove |
| `consecutive_directional_errors` | cheat-retropush/ cheat-bump(清空) | retro 判定偏差方向时 pushbump 落地时清空 |
| `in_progress_session` | cheat-predict(创建)/ cheat-publish(清除) | predict 写完文件时创建;publish 登记时清除 |
| `last_bump_at` | cheat-bump | bump 落地时 |
**绝不允许**多个 skill 写同一字段——会导致状态语义破碎。如果未来需要新字段,先想好"谁是唯一写者"。
---
## state file 损坏 / 不一致的处理
| 症状 | 处理 |
|---|---|
| 文件不存在 | 提示"未初始化,请跑 /cheat-init"**不**自动创建 |
| JSON 解析失败 | 提示"state file 损坏:path/to/.cheat-state.json",建议手动修复或备份 + 重新 init |
| schema_version 不识别 | 提示版本号 + 建议跑 [/cheat-migrate](../skills/cheat-migrate/SKILL.md)。SessionStart hook 会自动检测并提示 |
| `pending_retros` 含已删除的文件 | cheat-status 检测时安静移除,不报错 |
| `in_progress_session` 文件已不存在 | cheat-status 检测到 → 询问用户是否清理 |
| `calibration_samples``predictions/` 实际复盘数不一致 | cheat-status 报告差异。临时手改 state 即可;持续不一致是 bug,应在下个 minor 版本里加入 cheat-migrate 的 reconciliation step |
---
## 与 git 的关系
`.cheat-state.json` **应该**被纳入 git
- ✅ 它是项目配置 + 累计指标的快照
- ✅ git history 提供状态演化的完整轨迹
- ✅ 多设备同步靠 git push/pull
- ❌ **不**含敏感信息(cookie / API key 应放 `.env``.cheat-secrets.json`,单独 gitignore
`.cheat-cache/` 目录**不应该**被纳入 git
-`usage.jsonl`meta-logging 钩子的本地日志)
-`trends-history.jsonl`trend 抓取的去重缓存)
- 也可能含 adapter 调试文件(如 `douyin-session-debug/`
- 这些是设备本地状态,跨设备同步无意义
`/cheat-init` 应自动在用户项目根追加(不覆盖)`.gitignore`
```
.cheat-cache/
.cheat-secrets.json
```
---
## 升级路径
完整哲学和 maintainer checklist 详见 [migration-protocol.md](migration-protocol.md)。简版:
未来 schema 变化时:
1. bump `schema_version`(如 "1.1" → "1.2"
2.`migrations/<old>-to-<new>.md`4 段:WHAT/WHY/HOW/Manual fallback
3.`migrations/registry.md``LATEST_SCHEMA` 标记位 + 版本链表
4. SessionStart hook 检测到不一致时自动提示用户跑 `/cheat-migrate`
5. **绝不**让 skill 静默兼容旧版 schema 的删字段或重命名——那会让"哪个版本下哪个字段是什么含义"成谜
新增字段(MINOR,不破坏兼容):
-`state.get(field, default)`
- 老 state file 自动获得 default
- **仍需 bump schema_version + 写 migrations 文件**——保证状态文件最终一致;但用户可以延迟跑 migrate
删除 / 重命名 / 改语义(MAJOR,破坏兼容):
- 必须 bump schema_version + 写迁移文件
- CHANGELOG 标 `BREAKING`
---
## 用户手改 state file 的边界
允许手改的字段:
- `enabled_trend_sources`(数组,决定 cheat-trends 用哪些源)
- `data_collection`(切换 manual ↔ adapter
**不**建议手改的字段(会破坏不变量):
- `calibration_samples` / `pending_retros` / `consecutive_directional_errors`(应通过 retro 流程更新)
- `rubric_version`(应通过 bump 流程更新)
- `in_progress_session`(应通过 predict/publish 流程更新)
如用户确实想重置:建议**删除整个 .cheat-state.json + 重跑 /cheat-init**——这比手改单字段安全。
---
## Confidence label 派生表(**单一真值**
被 cheat-predict / cheat-status / cheat-recommend / SessionStart hook 等共同使用。从 `calibration_samples` 派生,所有 skill 用同一逻辑:
| `calibration_samples` | confidence emoji + 标签 | 数值含义 | 用户该如何用 |
|---|---|---|---|
| 0 | 🔴 极低 | "占星级别,纯纪律训练" | 不要基于 composite 决定要不要发;写 prediction 是为了**采集数据**,不是为了**做决策** |
| 1-2 | 🟠 低 | "中枢 ±50%,方向感优于绝对数字" | 信"A 比 B 流量好"的方向,不信具体数字 |
| 3-5 | 🟡 偏低 | "中枢 ±40%,可作为参考之一" | bucket 排序可用,中枢点估计仍是猜测 |
| 6-10 | 🟢 中 | "中枢 ±25%,可参与决策" | 可作为"要不要发"的依据之一 |
| 11-20 | 🟢 较高 | "中枢 ±15%rubric 形态稳定" | 可信中枢估计 |
| 21+ | 🔵 高 | "中枢 ±10%,可数据驱动 bump" | 进入数据驱动阶段 — bump 用回归而非直觉 |
> 上表的 ±X% 是**经验值**(基于参考博主的真实校准曲线),不是数学严格保证。新人账号的真实 ±X% 要等自己跑出 score-curve.png 才能验证。
**不要用这个表来 gating 任何功能**——所有 skill 在所有 calibration_samples 下都跑相同流程,只是输出里**显示**当前 confidence 等级。这是新设计的核心原则。