返回 Skills 目录
zenstory-ai/oh-story-claudecode已通过检查

SKILL DETAIL

story-short-write

zenstory-ai/oh-story-claudecode/story-short-write

短篇网文写作。辅助短篇小说创作,从构思到成稿,聚焦情绪拉扯与节奏把控。触发方式:/story-short-write、/写短篇、「帮我写一篇短篇」「写个盐言故事」。

安装量 · 90查看来源

Installation

npx skills add https://github.com/zenstory-ai/oh-story-claudecode --skill story-short-write

技能文件

SKILL.md

最近同步 · 2026年8月30日

references/author-memory.md
# 作者记忆协议

作者记忆用于保存跨会话复用的创作偏好,不保存小说世界里的事实。它借鉴“原始证据 → 候选 → 已确认画像 → 变更记录”的记忆管道,但把决定权留给作者。

## 边界与优先级

加载优先级从高到低:

1. 安全、平台、字数、文件协议等硬性门禁;
2. 用户在当前请求中的明确要求;
3. 当前书的 `设定/文风.md`、题材定位、细纲和其他项目设定;
4. 作者记忆中的本书偏好;
5. 作者记忆中的题材、流程和全局偏好;
6. 对标素材、通用方法和默认值。

作者记忆不能把本书事实写进 `.story/作者记忆/`,不能覆盖当前请求,不能降低审稿 rubric,也不能让去 AI 味改动剧情意图。小说事实继续由各书的 `追踪/` 和 `设定/` 管理。

## 文件与所有权

工作区级目录:

```text
{工作区}/.story/作者记忆/
├── _author-memory-state.json  # 唯一结构化权威
├── 作者画像.md               # 仅 active,供作者查看与管理
├── 待确认.md                 # pending / conflict,不参与约束
└── 变更记录.md               # 最近 100 次、最新在前的事务记录
```

三个 Markdown 文件都从 state 确定性生成,禁止手改;完整历史保留在 state,变更记录只展示最近 100 次。`作者画像.md` 是人类管理视图,普通写作 agent 不整份注入,而是调用 `query` 取得本次相关的紧凑上下文。作者记忆不存在时,普通写作、审稿和去味任务直接继续,不自动初始化空目录;首次 `record` 会随事务创建。

工作区必须显式传给脚本。优先使用已经包含 `.story/作者记忆/` 的最近祖先;首次初始化时使用承载多本书、`.active-book`、`长篇/`、`短篇/` 或 `拆文库/` 的创作工作区根。不要把用户主目录当默认工作区。

## 什么时候读取

长篇、短篇、去 AI 味开始前,如果 state 已存在,用 `query` 按本书、题材、流程和类型筛选 active 条目。查询输出固定不超过 2048 字节。不要先查询全部再让 agent 自行筛选,按任务直接选择 kind:

| 任务 | query kinds | 注入位置 |
|---|---|---|
| 正文初稿 / 续写 | `prose_style` + `story_design` | 主会话与实际正文 agent |
| 去 AI 味 / 改写 | `prose_style` | 主会话与实际改写 agent |
| 设定 / 大纲 | `story_design` + `workflow` + `interaction` | 主会话,不传正文 agent |
| 审稿 | `delivery` + `interaction` + 必要的 `prose_style` | 主会话,不降低 rubric |

审稿匹配项只用于交付格式、协作方式和“作者有意采用的表达选择”说明;问题严重度和 PASS/FAIL 仍由 rubric 决定。

待确认项不进入 prompt 约束,也不应为了确认它们中断当前任务。只有用户主动查看作者画像、候选积累到适合回顾的节点,或新偏好与 active 条目冲突时,才集中呈现。

## 可靠性与负荷边界

- 明确“记住 / 确认 / 替换 / 忘掉”的请求走单事件 `record`,不要求 agent 手工读取修订号或拼多操作事务。成功响应会给出 `Author Memory Receipt: rN · APxxx`;没有回执就不得声称“已经记住”。
- 普通创作只做一次本地 `query`,没有 state 时返回空结果且不创建文件;有记忆时也只返回相关 active 条目,硬上限 2048 字节。完整画像、证据、候选和 journal 不进入正文 prompt。
- 查询项是低优先级倾向,不是逐条打卡清单。自然吸收即可,不复述画像、不刻意提高词面命中率,也不得为命中偏好牺牲正文连贯、节奏、字数或本书既定笔调。
- 不安装会记录全部用户消息的 prompt hook。自然语言是否属于长期习惯仍需 agent 判断;这样不能承诺隐式偏好 100% 捕获,但避免把一次性要求、私人对话和小说事实静默写入长期记忆。需要确定写入时,用户可明确说“记住:……”,并以回执验收。

## 捕获判定

| 输入证据 | 处理 |
|---|---|
| “以后都这样”“我一直习惯……”等直接、稳定、范围清楚的原话 | `active`,`source=explicit_user` |
| 用户明确接受助手提出的长期做法 | `active`,`source=accepted_suggestion` |
| 同类修改反复出现,但用户没说这是长期规则 | `pending`,`source=repeated_correction` |
| 从成稿或操作轨迹推断出的模式 | `pending`,`source=inferred_pattern` |
| “这一章别……”“这次给我……”等一次性要求 | 只执行,不记录 |
| 角色、时间线、伏笔、世界观、当前剧情走向 | 写项目设定/追踪,不写作者记忆 |
| 助手自己生成的文字、默认模板、工具告警、rubric 结论 | 不自我学习 |

保留用户的否定词、限定词和适用范围,`quote` 写原话,`assertion` 只做不改变语义的紧凑归纳。范围规则:

- “本书 / 这个角色 / 这次连载” → `book`;
- “都市文 / 这类题材” → `genre`;
- 交稿、检查、确认节奏等操作习惯 → `workflow`;
- “以后 / 一贯 / 我习惯”且无更窄限定 → `global`;
- 范围含糊但可能稳定 → 取当前最窄合理范围并置 `pending`。

类型可选:`prose_style`、`story_design`、`workflow`、`delivery`、`interaction`。置信度与重要度均为 `low | medium | high`。

## 冲突、撤回与强化

- 同一类型、范围、归纳文本再次出现时,脚本强化原条目,累加证据和确认次数,不重复建条目。
- 新偏好与 active 条目矛盾时,先以 `conflict` 记候选,并在 `conflicts_with` 列出冲突 ID;当前任务仍按本轮明确要求执行。
- 作者选定新规则时用 `replace`,一次性启用新条目并把旧条目标成 `superseded`。
- pending 可以用 `decide=activate|reject`;冲突候选不能绕过旧规则直接 activate。
- 作者说“忘掉 / 这不再是我的习惯”时用 `forget`,保留历史证据但不再加载。
- active 条目的语义不可原地偷改;语义变化必须 replace,历史才可审计。

## 运行工具

先依次尝试 `python3`、`python`、`py -3` 找到 Python 3,再从当前 skill 根运行本地副本:

```text
{PYTHON} {当前 skill 根}/scripts/author_memory_commit.py init   --workspace {工作区}
{PYTHON} {当前 skill 根}/scripts/author_memory_commit.py record --workspace {工作区} --input {单事件.json}
{PYTHON} {当前 skill 根}/scripts/author_memory_commit.py query  --workspace {工作区} [--kind prose_style] [--book {书名}] [--genre {题材}] [--workflow {流程}]
{PYTHON} {当前 skill 根}/scripts/author_memory_commit.py commit --workspace {工作区} --input {事务.json}
{PYTHON} {当前 skill 根}/scripts/author_memory_commit.py check  --workspace {工作区}
```

- `record`:常用单事件入口,自动读取当前修订、首次自动初始化;`event_id` 相同且内容相同会幂等返回原回执,内容不同会失败。
- `query`:只读相关 active 条目;`--kind` 可重复,不存在 state 时返回空结果且零写入。返回的 `omitted > 0` 时收窄 kind / book / genre / workflow 后重查,不得改读完整画像规避预算。
- `commit`:高级批量入口;先在内存完成 schema、引用、容量和所有视图校验,最后原子替换 state。事务文件在成功前必须保留;过期修订会在任何写入前失败。
- `check`:从 state 重建并逐字核验所有派生视图。

## 事务格式

常用单事件新增或强化:

```json
{
  "schema_version": 1,
  "event_id": "conversation-2026-08-25-message-42",
  "operation": {
    "action": "remember",
    "preference": {
      "kind": "prose_style",
      "scope": {"level": "global", "value": null},
      "assertion": "对话尽量短,用动作承接情绪,不用大段解释",
        "quote": "以后对话都短一点,情绪放动作里,别让角色长篇解释。",
        "source_ref": "conversation:2026-08-25",
        "source": "explicit_user",
        "confidence": "high",
        "importance": "high",
      "status": "active",
      "reason": "用户以“以后”明确声明长期偏好",
      "conflicts_with": []
    }
  }
}
```

把文件交给 `record`。待确认项的 `status` 用 `pending`;冲突候选用 `conflict` 并填写 active ID。确认或拒绝候选时,把下列对象作为新事件的 `operation`:

```json
{"action":"decide","item_id":"AP002","decision":"activate","quote":"对,这就是我的长期习惯。","reason":"作者明确确认"}
```

用新规则替代一个或多个旧条目时,`replace.preference` 与上例字段相同,但不传 `status`、`conflicts_with`,新条目直接 active;下列对象同样作为 `operation`:

```json
{
  "action": "replace",
  "old_ids": ["AP001", "AP002"],
  "preference": {
    "kind": "prose_style",
    "scope": {"level": "book", "value": "雾港来信"},
    "assertion": "本书对话允许更长的试探,但避免解释设定",
    "quote": "这本书可以让对话慢一点,多试探,但还是别拿台词讲设定。",
    "source_ref": "conversation:2026-08-25",
    "source": "explicit_user",
    "confidence": "high",
    "importance": "high",
    "reason": "作者明确用本书新规则替代旧候选"
  }
}
```

撤回条目的 `operation`:

```json
{"action":"forget","item_id":"AP003","quote":"忘掉这个偏好。","reason":"作者明确撤回"}
```

需要把多个动作绑定成一次原子提交时才用高级 `commit`:顶层传 `schema_version`、唯一 `transaction_id`、当前 `expected_state_revision` 和含 1–32 项的 `operations`。操作按数组顺序应用,任一步失败则整份事务零写入。成功后删除临时输入文件;显式记忆请求还要把工具返回的回执原样告诉用户。
references/banned-words.md
# AI味禁用词与句式表

<!-- 同名副本×6 字节同步,改动后跑 scripts/check-shared-files.sh -->

## 最毒禁用句式(出现即修,最高优先级)

写网文最毒的 AI 句式,作者一旦养成就会反复出现。Gate A 第一遍扫描必须命中:

| 毒级 | 句式 | 错误例 | 修法 |
|------|------|--------|------|
| ★★★★★ | "不是A,(而)是B" / "不是A,不是B,(而)是C"("而"可省略,省掉也算命中)| "他不是冷漠,而是绝望" | 直接写 B 或用更自然的表达 |
| ★★★☆☆ | 跨段「不是A。/也不是B。/只是C。」 | 「不是嚎啕大哭。/也不是扯着嗓子喊不舍。/只是一个人走远了……」 | 语义复核;重复提纲或拖慢画面时压成 C,有辩解/悬念排除功能可保留 |
| ★★★★ | ",带着……" 万能状语 | "他笑了一下,带着一丝不易察觉的嘲讽" | 删掉状语留主句,或换具体动作 |
| ★★★★ | 无情绪声线:"声音不大,却带着……" / "语气毫无波澜" / "平静无波" / "声音平直/平平/听不出情绪" | "她声音不大,却带着不容置疑的力量" | 直接写台词内容、声音特征或动作 |
| ★★★★ | "他/她知道……" | "他知道这一切都来不及了" | 用行为展示认知 |
| ★★★ | "仿佛/犹如/宛若……一般" | "仿佛能穿透一切一般" | 删掉或白描 |
| ★★★ | "眼中闪过一丝……" / "嘴角勾起一抹……" | "眼中闪过一丝悲伤" | 删掉;写他当场说的话或做出的决定 |
| ★★★ | "心中涌起一股……" / "心头一震" | "心中涌起一股暖流" | 写它改变了什么:选择、台词、物件或后果 |
| ★★★ | 抽象命运/开端收束:"命运……棋局/獠牙" / "这一刻终于明白" / "反击才刚刚开始" | "命运终于露出獠牙;属于他的反击才刚刚开始" | 回到角色当下可见的文件、动作、对话或物理后果 |
| ★★ | 章末预告 "他不知道的是……" | "他不知道的是,更大的风暴即将来临" | 用具体钩子物件/事件收束,避免空泛预告 |

> 凡命中 ★★★★★ 一处即视为重度AI味的强证据;★★★★ 命中 ≥2 处即触发中度复扫。

`check-ai-patterns.js` 的 `formulaic-parallelism` 还会提示「至于X不X,怎么X」和同动词「不V A,不V B」。这两类可能是功能性口语,因此只做 advisory;Gate B 必须连同台词读语境复核,若只是复述细纲/前文就压成一次判断,不能因 hook 豁免台词而跳过。

**标点**:正文(含叙述和对话)禁用破折号 `——`/`—`、双连字符 `--` 和省略号停顿,改用句号、逗号、短句或动作断句;不设置对话破折号例外。盐言「」引号不在此列。

---

## 一级禁用词(出现即替换)

> 什么词进一级:只收真人语料里几乎不出现、AI 特有的词。真人高频使用的自然副词和虚词不进一级,走二级密度控制。

### 情态类
仿佛、犹如、宛若、如同、一丝、一抹、些许、几分、隐约、毫无征兆、几不可闻、微不可察

### 动作类
深吸一口气、不禁

### 表情类
眼中闪过、嘴角勾起、眉头微皱、眉眼低垂、瞳孔微缩、瞳孔收缩、瞳孔一缩、指节泛白、眼神锐利、目光锐利

### 心理类
心中一动、心头一震、心下了然、心中暗道、心底泛起、不由得、心中一凛

### 判断类
不容置疑、不容置喙、不易察觉、显而易见、毫无疑问、不可否认、前所未有

### 形容类
坚定、闪烁着光芒、狡黠、深邃、凛冽、冰冷

### 过渡类
不由自主、情不自禁、自然而然、话锋一转

## 二级禁用词(高频出现时替换)

### 语境敏感词(仅高频或偷懒时处理)
突然、陡然、骤然、猛然、好像、似乎、瞬间、猛地、死死地(角色口语、真实突发、时间压缩、视角不确定时可保留;用同义变体轮换规避重复不算豁免,按同一个词计密度)

### 弱化副词(密度控制)
缓缓、微微、轻轻、淡淡(每千字合计 ≤3;这四个词同时计入 `cliche-density-tic` 的套词密度统计;孤立自然使用可保留,成串出现或每个动作都垫一个时才替换)

### 书面腔 → 口语化

| 书面腔 | 口语化替换 |
|--------|-----------|
| 瓦解 | 消失 / 散了 / 没了 |
| 无名火 | 烦躁 |
| 往我心上捅刀子 | 心烦意乱 |

### 总结句式
- "他/她终于明白..."
- "他/她这才意识到..."
- "这一刻,他/她终于明白/意识到..."
- "从这一刻开始..."
- "属于X的反击/复仇/故事,才刚刚开始"
- "命运/宿命 + 齿轮/棋局/獠牙/改写/安排"
- "此刻,他/她..."
- "一切...都..."
- "原来..."

### 排比句式
- 连续3句以上相同结构的排比
- "有的...有的...有的..."
- "一边...一边...一边..."

### 升华句式
- "这一刻..."
- "他知道..."
- "她明白..."
- "这就是..."

## 禁用句式模板

| 句式 | 示例 | 问题 |
|------|------|------|
| "不是A,而是B" | "他不是冷漠,而是绝望" | 最毒;直接写 B |
| "...,带着..." | "他说,带着一丝无奈" | 万能状语 |
| "声音不大,却带着……" | "她声音不大,却带着不容置疑的力量" | AI 最爱声音描写 |
| "仿佛能...一般" | "仿佛能穿透一切一般" | 文言腔 |
| 对话标签密度过高/公式化标签 | "好的,他说道" | 普通"说"可保留;高频或公式化时处理 |
| "他/她感到..." | "她感到一丝失落" | 告诉而非展示 |
| "他/她意识到..." | "他意识到事情不对" | 直接告知 |
| "眼中闪过一丝XX" | "眼中闪过一丝悲伤" | 模板化 |
| "嘴角勾起一抹XX" | "嘴角勾起一抹冷笑" | 模板化 |
| "心中涌起一股XX" | "心中涌起一股暖流" | 模板化 |
| "取而代之的是" | "笑容消失,取而代之的是冰冷" | AI 过渡模板;直接写新状态 |
| "淬了/淬着X" | "眼里淬了毒" | AI 通感套路;写动作或台词 |
| "显得(有些)X" | "他显得有些兴奋" | 告诉而非展示 |
| "心底/心里某个地方+软" | "心里某个地方软得一塌糊涂" | 言情套句;写动作 |
| "(浑身)散发着一股X气息/气场" | "浑身散发着一股生人勿近的气息" | 万能气场描写;写旁人的反应 |
| "命运/宿命 + 齿轮/棋局/獠牙/改写/安排" | "命运终于露出獠牙" / "早已布好的棋局" | 抽象作者总结;改成角色当下撞见的文件、动作、对话、物理后果 |
| "这一刻终于明白/从这一刻开始/才刚刚开始" | "这一刻,他终于明白" / "反击才刚刚开始" | AI 收束腔;删总结,用动作或未解决问题收尾 |

## 比喻分类(默认复核,不默认全删)

带"像/如/仿佛/犹如/宛若"的比喻不是一律 AI。真正高风险的是:成片堆叠、套用万能文学比喻、用精致比喻替代剧情推进,或在段尾替读者总结意义。本表用于识别需要复核的比喻类型:

| 比喻类别 | 例 | 处理 |
|---------|----|------|
| 生活/角色化 | "像一头被抛弃的野狗" | 若贴角色视角、能传递信息或情绪,可保留 |
| 物品/现象类 | "像一把刀" "脸色惨白得像这漫天的雪" | 普通功能性比喻可留;模板化或重复时改白描 |
| 状态类(陈词滥调) | "梨花带雨" "如沐春风" | 优先删或改成具体动作/表情 |
| 抽象类 | "像命运的齿轮" "像上辈子的尘埃" | 高风险,优先落回动作、物件、声音、后果 |
| 假设类 | "力道大得像是要把骨头捏碎" | 若是角色身体感知可留;夸张堆叠时改事实后果 |

处理原则:先看功能,再看密度。保留最能传递信息或情绪的一两个,其余改为直接描述、动词、名词、作用、结果或事实;不要把删掉的比喻替换成另一批新比喻。例 "脸色惨白得像这漫天的雪" 若只是套话 → "脸色惨白";若雪景正在压迫角色,可保留或改成角色当下看到的具体画面。

> `metaphor-density-tic` 是 advisory:提示通读复核,不是 blocking;生活化、角色化、单个有功能的比喻可以保留。

## 替换策略速查

| 原文类型 | 替换方法 | 示例 |
|----------|----------|------|
| 抽象情绪词 | 先看上下文是否已成立;再选选择、台词、物件、后果或一句直写 | “紧张”若不影响下一步可直写或删;若导致签名作废,就写作废的结果 |
| "感到XX" | 删除“感到”后按场景决定是否还要情绪句 | “他感到愤怒”可写“他火了”,也可直接写他撤回报价;不要默认换成攥拳 |
| 形容词堆砌 | 白描手法 | "美丽动人的笑容" → "她笑了" |
| 书面表达 | 口语化 | "不容置疑" → "就是" |
| 解释性描写 | 留白 | "他因为害怕而..." → "他退后一步" |
| 连续排比 | 保留最强一条 | 3 句排比留 1 句 |
| 总结升华句 | 直接删除 | "这一刻,她终于明白了..." → 删 |
| "不是A,而是B" | 直接写 B 或更自然的表达 | "他不是冷漠,而是绝望" → 直接写 B |
| 多余修饰(形容词/定语/量词/指示代词) | 删 | "白色的药片" → "药片";"手里那截链子" → "链子";"飞驰的汽车" → "车" |

**替换不复用**:右列是方向示例,不是标准答案。同一禁用词在一章内多次命中时,各处给不同的具体化写法;同一个替换写法反复出现(每次都「垂下眼」、每个动作都补「了一下」),替换产物本身就成为新的模板指纹。

**套词密度优先处理**:`check-ai-patterns.js` 报 `cliche-density-tic` 时,说明禁用词不是零星误用,而是聚成了模板腔。处理顺序不是同义词替换,而是先删抽象总结,再把情绪/判断落到角色当下可见的动作、物件、对话和具体后果。

**套式反应逐处删除测试**:`stock-reaction-tic` 报警时,不代表禁止身体描写。逐处问:删掉后信息、选择、关系、物件或动作结果是否受损?无损就删,不把“指尖轻叩”换成“目光微沉”。伤势、动作失败、人物习惯或情节后果明确时可以保留。
references/cross-book-recall.md
---
name: cross-book-recall
description: 多对标跨书召回
---

# 跨书召回

## 触发
先按当前项目目录名、`.active-book` 与本书设定识别当前作品,排除同名或来源指向当前正文的 `拆文库/{当前书}/` 与历史误建的 `对标/{当前书}/`;它们是本书导入分析,不是跨书样本。排除后,项目根 `拆文库/` 或项目 `对标/` ≥2 本才启用。主对标书取 `设定/题材定位.md`「主对标书」字段,缺失则从排除后的候选中取字典序第一本(优先 `对标/`,否则 `拆文库/`),并在 `gaps.main_benchmark_unspecified: true` 中提示用户补。

> **数量规则**:主对标书最多 1 本,用于文风和最终正文输入;副对标 / 参考对标**不限制登记数量**。执行时按题材相关性、引用强度和阶段预算逐本召回;超过预算时裁剪条目,不删除书目。

## 三道防线
1. 副对标 `文风.md` 不读,避免多本文风互相污染
2. 角色/剧情/设定模块可从所有副对标召回,但必须按「同题材 > 弱相关 > 参考」排序,并受每本/总量预算约束
3. narrative-writer 正文 prompt 只吃主对标文风/原文锚点 + 预算筛选后的 `副对标召回摘要`;不读取、不传入副对标 `文风.md` 或副书原文

## 跨题材判断
从项目 `设定/题材定位.md`「对标书列表」字段读每本副对标的「题材类型」与「引用强度」(未登记的书按「参考」处理,并输出 `gaps.benchmark_registry_missing: true`):
- 同题材 + 引用强度=辅:全阶段可召回,按每本上限取条目
- 同题材 + 引用强度=参考:只取最相关条目,默认不超过每本上限的一半
- 弱相关:仅设定/大纲,每本 ≤1 条
- 不相关:跳过

排序规则:先按相关性(同题材 > 弱相关),再按引用强度(辅 > 参考),再按用户在 `对标书列表` 中的顺序;若缺少 `对标书列表` 或其中未登记某本书,则剩余副书按目录名/书名 Unicode 字典序稳定排序,并输出 `gaps.benchmark_registry_missing: true` 提示补全清单。副书数量不限;如果总条数超过阶段预算,裁条目不裁书目记录。

## 阶段消费
表中数字为**每本副对标召回上限**;同时设置阶段总预算,防止副书很多时挤爆上下文。`—` 行表示该文体无此阶段,整行忽略。

| 阶段 | 长篇产出 | 短篇产出 | 同题材每本上限 | 弱相关每本上限 | 阶段总预算 |
|------|---------|---------|----------------|----------------|------------|
| 设定 | `拆文报告.md` | `拆文报告.md` + `情节节点.md` | ≤2 | ≤1 | ≤8 |
| 大纲 | `章节/*_摘要.md` + `剧情/*.md` | `情节节点.md` + `写作手法.md` | ≤3 | ≤1 | ≤10 |
| 模块 | `角色/` + `剧情/` + `设定/` | — | ≤2 | 0 | ≤8 |
| 正文 | `文风.md` + 原文 | `写作手法.md` + 原文 | 0 | 0 | 0 |

> **大纲阶段按剧情单元检索**:检索键为剧情单元「类型」(第一键,必填枚举)/「桥段标签、套路框架位置」(第二键);同题材内同类命中条目优先进入预算。仅限大纲阶段按剧情单元检索时生效,不改其余阶段排序与预算数字。同类零命中时回退主对标来源条并输出非阻塞 `gaps.similar_plot_not_found: true`,流程继续。

## 输出要求

跨书召回输出必须包含:

```markdown
## 副对标召回摘要
| 书名 | 引用强度 | 相关性 | 召回阶段 | 召回条数 | 使用方式 |
|---|---|---|---|---|---|
| {书名} | 辅/参考 | 同题材/弱相关 | 设定/大纲/模块 | {n} | {用于补充某类结构,不进入文风/原文锚点} |
```

若副书很多,只输出被本阶段实际召回的条目;未召回的副书不代表被删除,只是本阶段预算未命中。正文阶段可传入本表作为结构/情绪/设定参考,但必须保留“副书不进文风、不进原文锚点”的边界。

## 拆文字段 → 写作参考
读 `_meta.json.structure_counts` 时,按此表回查当前 skill 已登记的对应写作 reference。短篇优先走 genre-styles 题材包与 short-craft,长篇优先走长篇同类理论文件;未在当前 skill `参考资料` 表登记的文件不要跨 skill 加载。

| 拆文字段 | 含义 | 写作参考 |
|---------|------|---------|
| `beats` | 结构段(开端/发展/高潮/结局) | 当前 skill 的题材结构文件;短篇优先 genre-styles 题材包 / `genre-writing-formulas.md` |
| `hooks` | 钩子数 | `hooks-chapter.md` / `short-suspense.md`;短篇开头密度补 short-craft |
| `setup_clues` | 反转铺垫线索 | `short-reversal.md` |
| `character_archetypes` | 反差人物 | 当前 skill 的人物/题材风格文件;短篇优先 genre-styles 题材包 / genre-writing-techniques |
| `reusable_structures` | 可复用手法 | `genre-writing-formulas.md`;短篇可补 short-craft |
| `reversal_type` | 反转类型(7 枚举) | `short-reversal.md` 对应骨架 |
references/dialogue-mastery.md
# 对话设计操作手册

> 写对话场景时加载。先看决策路由选对话模式,再用操作指令控制质量。

---

## 决策路由

| 你的对话场景是 | 用这种模式 | 操作 |
|-------------|---------|------|
| 主角碾压/打脸 | 压制模式 | 对方长篇大论 → 主角一字回应 |
| 主角亮底牌/反转 | 反转模式 | 对方嚣张 → 主角一句话事实 → 对方沉默 |
| 关系破裂/心死 | 心死模式 | 对话越回越短:从辩解 → 沉默 → "随意" |
| 日常互动/立人设 | 日常模式 | 让其他人物参与冲突,不要主角一个人独白 |
| 群众震惊/弹幕 | 弹幕模式 | 递进:普通人震惊 → 专业人士分析 → 特殊身份者反应 |
| 信息展示/世界观 | 信息嵌入 | 用角色语气包裹信息,不是机械陈述设定 |
| 情绪拉扯/虐心 | 情绪推动 | 上行下行交替,像拉锯拉升期待 |

### 对话模式选择补充

| 对话类型 | 常见问题 | 修正操作 |
|----------|---------|---------|
| 问答式(通篇一问一答像审讯) | 改为一方主动说,另一方给反应;反应可用动作/表情/心理 |
| 冲突式(太礼貌不够劲) | 递进五级:委婉拒绝→友好人道→命令否定→PUA式→直接侮辱 |
| 日常对话(容易变成没功能的水) | 功能是立人设;能让人参与的冲突别让主角独白 |
| 多人对话(混乱/没营养) | 写前规划每个角色功能:信息提供者/情绪放大器/冲突制造者 |

---

## 对话核心规则

每句对话必须承载以下至少一项,否则删除:

1. **推进剧情**:透露新信息、推动事件发展
2. **增加期待感**:暗示即将发生的事、制造悬念
3. **展示人设**:通过语言风格传递角色性格

且在情绪场景里,每句还要**回应上一句对方的情绪状态**(承接/偏转/升级/退缩)——对话是两个人的情绪在碰,不是轮流播报信息。只推进剧情、句间无情绪承接 = 机械对话。

### 绝对禁止

| 禁止 | 理由 |
|------|------|
| 配角无脑夸主角 | 假 |
| 互相解释读者已知信息 | 水字数 |
| 大段说明文式对话 | 闷 |
| 所有角色说话方式一样 | 模糊 |
| 对话说服人物 | 现实中没人被几句话说服,用突发状况代替 |
| 情绪写完方向变了 | 需回退到情绪拆分 |

---

## 权力博弈对话

### 规则

对话长度 = 权力地位。掌控者话短且冷静,被动者话多且情绪化。

### 压制模式

结构:对方长篇大论(3-5 行)→ 主角一字回应。

```
"你以为你是什么东西?我告诉你,这个家轮不到你说话!你嫁进来的那天起就该明白自己的位置。"
"滚。"
```

### 反转模式

结构:对方嚣张(2-3 行)→ 主角亮底牌(1 行事实)→ 对方沉默。

```
"你有什么资格管?这是我家的钱,我想怎么花怎么花。"
"你妈的存折,密码是我的生日。"
```

### 心死模式

结构:对话越回越短,从辩解到沉默到「随意」。

```
"你听我解释,那天不是你想的那样。"
"嗯。"
"真的,我可以证明。"
"随意。"
```

### 操作指令

- 掌控者/主角亮底牌时:对话 ≤ 10 字,不加动作描写
- 被压制方:对话 ≥ 20 字,可加动作描写(攥拳/咬唇/站起来)
- 两人对话时:短句方 = 权力上位,长句方 = 权力下位

---

## 潜台词与议程

### 潜台词规则

- 角色真实动机绝对不能浅显地写在台词里
- 现实中人说话都给自己找借口,角色也一样
- 每句对白同时设计:角色的动机(可能角色自己都没意识到)和角色的借口

### 对话议程

- 每个角色进入对话时有自己的议程:想从这场对话中得到什么
- 两个角色的议程碰撞才是张力来源
- 双方议程一致(同立场)= 复述,失去意义

### 语气由三要素决定

关系 × 场合 × 目的 = 语气

| 场合 | 特点 | 适合内容 |
|------|------|----------|
| 私人(单对单) | 深入、感情流露透彻 | 内心剖白、密谋、表白 |
| 公众 | 需考虑体面,措辞收敛 | 需要冲击力时可打破(如当众翻脸) |
| 熟人(朋友/同门) | 深度介于两者之间 | 轻松互动和信息交换 |

私密的话在公众场合说才有冲击力。对话中不能完全表达的内容,通过动作、神态、环境补充。

---

## 情绪推动对话

### 强情绪对话

- 命令式+否定式最能激发读者情绪:"我说的还不够清楚吗?"
- 最强话术:打着为你好的幌子,句句不离关心,但句句都是嫌弃、指责、厌恶
- 直接否定比含蓄暗示更伤人

### 情绪连续性

角色情绪是连续的、循序渐进的。从生气到高兴:生气 → 不那么生气 → 不生气 → 高兴,每次转变需对应事件触发。不能跳步。

### 情绪四步法(缺一不可)

1. 遇到事件(失衡状态)
2. 情绪反应(外在表现:动作、表情、语言)
3. 内心思考(即使没思考也要写出"没有思考",表达鲁莽)
4. 采取行动(基于前三步结果回应)

### 对话不平淡三思路

- 对话本身带来/强化某个核心驱动力(期待、爽感、悬念)
- 信息交流因某原因受阻碍,阻碍可能导致驱动力到来
- 发展突然脱离读者预期(但必须合理、符合人设)
- 把长篇对话塞在期待点和爽点之间:读者为了看爽点愿意忍受中间对话

---

## 信息展示与世界观引出

- 大量信息通过对话展示会显得啰嗦,部分信息转化为情节、心理描写、旁白、环境、动作
- 用角色的语气和立场包裹信息,不是机械陈述设定
- 设定用到哪个稍微带出来就行,不需完完全全讲明白前因后果
- **角色不当"科普嘴"**:设定/原理/前因后果不能靠任何角色(尤其信息型/AI 配角)整段讲解——Gate G 同样适用于角色台词。拆成角色在压力下挤出的半句话 + 身体反应 + 留白,用到哪带哪点

### 信息拉扯示例(以"主角新书起飞了"为例)

| 角色 | 台词 | 功能 |
|------|------|------|
| 甲 | 听说成绩…… | 悬念拉期待 |
| 乙 | 肯定没人看! | 下行 + 拉期待 |
| 甲 | 听说成绩很不错 | 上行 + 拉期待 |
| 乙 | 首订一万三? | 展露核心信息 + 达成爽点 |

上行和下行交替,情绪像拉锯不断拉升期待。

---

## 人物语言差异化

每个角色要有自己的说话方式。对话时经常卡住不知道角色会说什么 → 人设模糊,去总结类似人设的说话方式。

| 差异化维度 | 操作 |
|------------|------|
| 口癖和惯用语 | 给每个主要角色一个标志性用词 |
| 说话节奏 | 长篇大论 vs 短句连击 |
| 信息偏好 | 技术型带专业术语,江湖人带切口 |
| 立场固定 | 某角色永远从某个角度发言(悲观派/乐观派/务实派) |
| 身份影响措辞 | 老者/少年/贵族/市井,身份不同则措辞、自称、敬谦词不同 |
| 性格影响语气 | 智谋型话里有话;鲁莽型想到什么说什么;冷静型措辞精确,偶尔情感外露反而有冲击 |
| 进度影响态度 | 初见/熟悉/对立/亲密,关系阶段不同则语气与信息量不同 |

---

## 弹幕/群众对话

三大核心作用:剧情推进(透露主角不知道的信息)、增加期待感(悬念、猜测)、情绪渲染(群众震惊/激动/愤怒传染读者)。锦上添花,不代替主线。

### 设计过的弹幕 vs 没设计的

- 没设计:只有"卧槽好厉害",单调、浪费
- 设计过:普通人震惊 → 专业人士分析 → 特殊身份者反应 → 情感升华

### 操作要点

- 不同人格化语气,不能每条都一个味
- 短小精悍,每条不超过一句话核心信息
- 善用递进:从最初震惊到逐渐认识全貌
- 可出现"反转"角色:看似路人一句话改变所有人认知
- 不用每章都写,关键爽点/燃点/泪点前后集中使用

---

## 节奏控制

### 大量对话保持节奏

- 不要删掉表现人物性格的语气助词来"精简"
- 对话段落间穿插动作描写、环境变化、心理活动调节节奏
- 紧张段落对话短促,舒缓段落可以长一些
- 关键信息放对话开头或结尾,中间用于拉扯情绪

### 动作和表情处理

- 刻意给每句对话配表情/动作会让行文机械
- 动作和表情在关键转折处使用效果最好,不需要每句都配
- 语气平淡场景(喝茶、散步)用微小动作和沉默体现氛围

### 对话的呼吸感

- 连续多轮对话后需要"换气",插入环境描写或角色心理
- 适当停顿(动作描写、换行、短句)比连续输出更有张力
- "你确定?"比长篇解释更有压迫感

---

## 篇幅控制

### 对话过多时

- 读者已知信息的对话用叙事一句话概括:"爱丽丝向安娜讲述了来城里的原因,安娜听后直皱眉头"
- 能用突发状况替代的对话段落直接替换
- 语气词删掉后干巴巴 = 对话本身缺乏信息量,需重写

### 对话过少时

- 能用其他人物对话讲出来的东西,不要让主角旁白平铺直叙
- 引入配角参与冲突和对话,但新人物必须安排主线戏份

---

## 以梗填充对话

### 梗式 vs 普通

- 普通:"兄弟别灰心,你一步步走到今天我是看着你过来的,这点挫折对于你来说算什么事儿?振作起来!"
- 梗式:"兄弟别灰心,我相信你总有人头落地,落地人头,头落地上……卧槽那句话怎么说的来着?兄弟你懂我意思是吧……"
- 梗式用"说不出来但意思到了"的状态制造趣味

### 操作

- 在对话中融入梗或骚话,有效提升整体趣味性
- 特别是主角或重要配角的突出对话,适合用梗强化记忆点
- 可用某个梗作为高潮点,整段剧情围绕达成这个梗来设计
- **场合例外(声线让位)**:高压/生死/悲痛/严肃 beat 里,搞笑担当与轻快配角的玩笑、口头梗、插科打诨一律收敛——声线让位于当前情绪基调,用短、冷、带情绪重量的反应替代;梗只在安全或喘息 beat 放。自检:这句玩笑放进当前基调会不会让读者出戏?会就删/改

---

## 质量检查

### 三大自查项(中一条以上需改进)

- [ ] 是否存在大量信息都必须用对话来展示
- [ ] 对话是否是问答式的一问一答
- [ ] 是否习惯依赖对话来推动剧情或人物变化

### 核心指令检查

- [ ] 权力博弈:掌控者对话 <= 10 字 / 被压制方 >= 20 字,是否有明确的压制/反转/心死模式
- [ ] 潜台词与议程:每个角色进入对话时有自己的议程,真实动机不在台词中
- [ ] 人物差异化:遮住角色名后能否区分是谁在说话(7维差异化)
- [ ] 弹幕递进:普通 → 专业 → 特殊身份,是否有层次感
- [ ] 对话推动剧情:每段对话结束时,剧情是否往前推了一步
- [ ] 篇幅控制:单次对话不超过全节 40%,信息密度是否足够

### 检验对话质量

- 对话自然度检查:逐句检查对话是否像自然口语交流,而非书面化的问答稿
- 对话结尾能否预示接下来的节奏变化
references/emotional-methods.md
# 情感设计方法:三板斧 + 拉扯节奏 + 失败模式

> 设计情感桥段时加载。先看决策路由选策略,再用三板斧设计具体段落。

---

## 决策路由

| 你的情感设计场景是 | 用这个策略 | 跳转到 |
|---------------|-----------|----------|
| 建立角色羁绊(让读者相信关系) | 羁绊铺设(第一斧) | 三板斧 §1 |
| 制造情感撕裂(反差/错位/背叛) | 情感撕裂(第二斧) | 三板斧 §2 |
| 写结尾余韵(安静细节击穿读者) | 余韵钝痛(第三斧) | 三板斧 §3 |
| 规划整体情感节奏曲线 | 拉扯节奏设计 | 拉扯节奏 |
| 不同题材的情感策略选择 | 题材策略差异 | 按题材 |
| 检查情感设计是否有效 | 失败模式排查 + 快速自查 | 底部清单 |

---

## 情感虐心三板斧

### 第一斧:羁绊铺设(前 1/3)

**目标**:用具体物件/数字/细节建立关系质感,让读者相信这段关系是真实的。

**方法**:
- 用具体数字建立时间感:「相恋八年」「昏迷五年」「七年光景」
- 用具体物件承载感情:金锁(姐姐的生日礼物)、账本数字(还债记录)、木头小马(父王磨的)
- 用重复动作建立习惯:每天一碗粥、每两周来看一次、每年冬天冻得睡不着

**案例**:
- 《迟来二十年》:用二十年的账本数字(八万块)建立母女关系的重量
- 《我在佛前求你》:昏迷五年、求神拜佛的细节、从初中开始的暗恋
- 《皇弟欺负幼子》:七年质子生涯的具体苦难(跪雪地、挨巴掌、吃馊饭)

**核心**:羁绊越具体,后面的撕裂越痛。不要用「他们很相爱」这种抽象描述。

---

### 第二斧:情感撕裂(中后段)

**目标**:制造反差,让读者以为恨对了 → 发现恨错了(或反过来)。

**方法**:
- **反差法**:先展示温暖一面,再用残酷真相击碎
- **错位法**:角色 A 以为在保护角色 B,实际上在伤害
- **延迟真相法**:关键信息在读者最不期待的时候揭示

**案例**:
- 《姐夫捎路要奶粉钱》:妈妈表面软弱可怜,实际一直操控姐姐索取钱财 → 金锁是假的(金包铜)→ 反差撕裂
- 《皇弟欺负幼子》:王氏表面温柔(送手炉、煎汤药)→ 实际下药逼疯先皇后 → 用诛心话逼死
- 《重生弟弟》:弟弟深爱女友 → 女友是诈骗犯 → 怀孕是假的 → 被骗到电诈园区

**核心**:撕裂的力度取决于铺垫的厚度。没有好的羁绊铺设,撕裂就是无根之木。

---

### 第三斧:余韵钝痛(结尾)

**目标**:不用大哭大闹,用安静细节击穿读者防线。

**方法**:
- 用日常动作承载巨大情感:继续喂粥、把衣服叠好、不回头
- 用物件细节制造余韵:坏掉的金锁、木头小马、沾血的戒指
- 用「不」制造留白:不解释、不回头、不流泪

**案例**:
- 《皇弟欺负幼子》:「他拿着那匹被我磨得很光滑的木头小马……他握着正好。」
- 《姐夫捎路要奶粉钱》:「我的手机清净了不少。那些困扰我很久的麻烦,在一瞬间变得很小很小。」
- 《准儿媳打分》:「办了收养手续后,她第一次踏进这个陌生的家……我知道,这一次我没选错。」

**核心**:最好的余韵不靠大哭,靠「看完之后很久都忘不掉」。

---

## 情感拉扯节奏设计

### 基本节奏曲线

```
温暖 → 残忍 → 善意 → 真相 → 原谅 → 来不及 → 释然 → 细节暴击
```

不是所有故事都走完整曲线。按题材选择子集:

| 题材 | 典型节奏 | 核心拉扯 |
|------|----------|----------|
| 世情/爽文 | 欺压 → 忍耐 → 爆发 → 打脸 | 情绪反弹快、打脸密度高 |
| 情感/虐心 | 甜蜜 → 破碎 → 挽回 → 错过 | 慢铺垫、后劲大 |
| 古言/复仇 | 隐忍 → 布局 → 揭穿 → 报应 | 设定简洁、暴力美学 |
| 悬疑/推理 | 平静 → 不安 → 真相 → 震惊 | 信息差制造不安 |
| 年代/亲情 | 误解 → 冲突 → 真相 → 和解 | 代际冲突、时代质感 |

### 拉扯原则

1. **每 3-5 个小节有一次情绪转向**(不能一路虐到底或一路爽到底)
2. **每次转向都要有触发事件**(不能无理由地改变情绪)
3. **最后一次转向决定结尾余韵的基调**
4. **爽文允许快速反弹,虐文需要更长铺垫**

---

## 按题材的情感策略差异

### 世情/爽文

- 情绪反弹速度:快(受辱后 2-3 节内打脸)
- 打脸密度:每 3-5 节一次
- 反派嚣张度:递增(第一次小嚣张,最后一次大嚣张)
- 结尾基调:痛快、解气
- 案例:《准儿媳打分》— 打分制反制、铁盒零分、断绝关系

### 情感/虐心

- 羁绊细节密度:高(前 1/3 必须建立深厚羁绊)
- 反差设计:先暖后冷,先甜后苦
- 余韵技法:安静结尾 + 物件细节
- 结尾基调:意难平、释然
- 案例:《姐夫捎路要奶粉钱》— 金锁真相 → 假爱 → 独立

### 古言/复仇

- 设定简洁原则:人物关系清晰,不搞复杂世界观
- 暴力美学写法:打脸要直接,不拖泥带水
- 底牌时机:最后 1/4 揭示最大底牌
- 结尾基调:大快人心、因果报应
- 案例:《皇弟欺负幼子》— 一声令下打二十棍 → 查出真相 → 废后

### 悬疑/推理

- 信息差布局:读者知道角色不知道,或反过来
- 排除法结构:逐步排除可能性,最后只剩真相
- 动机揭示节奏:先揭示做了什么,再揭示为什么做
- 结尾基调:细思极恐、原来如此
- 案例:《重生弟弟》— 重生设定 + 诈骗犯身份 + 电诈园区

### 年代/亲情

- 代际冲突处理:不站队,展示双方的苦
- 时代细节质感:用时代特有的物件/习俗建立质感
- 和解节奏:不急和解,先让双方充分受伤
- 结尾基调:温暖中带着遗憾
- 案例:《姐夫捎路要奶粉钱》— 亲子关系 → 金锁真相 → 断裂

---

## 常见失败模式

| 失败模式 | 识别方法 | 修正方向 |
|----------|----------|----------|
| **太平** | 连续 5+ 节没有情绪转折 | 插入意外事件或新信息 |
| **太赶** | 重大转折只用了 1 节铺垫 | 给转折至少 3 节铺垫 |
| **假虐** | 读者不心疼,只是看着难受 | 检查羁绊铺设是否具体 |
| **割裂** | 前半部分和后半部分像两篇不同的故事 | 用伏笔/物件/主题贯穿 |
| **烂尾** | 反转后拖了 2000 字还在交代 | 反转后 500 字内收尾 |
| **人设崩** | 角色在关键时刻的行为不符合前面的人设 | 回顾人设,确保行为逻辑一致 |

---

## 快速自查

设计情感时,用这个清单:

- [ ] 前 1/3 有具体羁绊细节(不是抽象描述)?
- [ ] 中段有反差/撕裂(读者以为 A → 实际是 B)?
- [ ] 结尾有安静细节(不是大段抒情)?
- [ ] 每 3-5 节有情绪转向?
- [ ] 反派行为符合其人设逻辑?
- [ ] 不存在上述 6 种失败模式?

---

## 三板斧与 SKILL.md 五段结构映射

| 三板斧 | 对应 SKILL.md 五段 | 说明 |
|--------|-------------------|------|
| 第一斧:羁绊铺设(前 1/3) | 铺垫段(第二段,占 30-40%) | 用物件/数字/习惯建立关系质感 |
| 第二斧:情感撕裂(中后段) | 升级段(第三段)+ 反转段(第四段) | 反差/错位/延迟真相制造撕裂 |
| 第三斧:余韵钝痛(结尾) | 结尾段(第五段,占 5-10%) | 安静细节收尾,不写大段抒情 |
references/genre-styles/世情打脸.md
# 题材风格包:世情打脸 / 家庭伦理因果报应

> 番茄 / 七猫 / 黑岩通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**现实生活里的恶人作恶与因果报应。婆媳、极品亲戚、吸血父母、白眼狼儿女、出轨枕边人、职场恶人欺压老实人,草根第一人称,恶是日常小事(一双鞋、一张车票、一句规矩),铺垫拉满后当众打脸、报应落地。** 读者要的是「我家那点糟心事被写出来了」的代入 +「恶人当众被戳穿、账一笔笔算清」的解气。
>
> 与「复仇打脸」的分工:复仇打脸卖极端作践 + 身份底牌碾压 + 反杀执行(私密照、断肢、摄政王、判官);本包卖**日常之恶的累积与当众清算**,恶小而密、靠细节不靠大事件,打脸靠证据、账本、法律、舆论落地,主角是老实人草根而非隐藏大佬。与「追妻火葬场」的分工:追妻恶人是爱人、卖被珍视感剥夺;本包恶人是亲人 / 姻亲 / 同事,卖付出被吸血、好心被践踏。

---

## 叙述腔调

第一人称「我」,身份即日常角色:儿媳、女儿、女婿、丈夫、嫂子、下属。**中老年「我」是本题材特有声部**(婆婆、丈母娘、奶奶、老母亲第一人称),其他题材几乎不用,本题材大量用,自带「一辈子付出被辜负」的重量。

过日子的口吻:记账式精确(金额、年限、物价、次数全给具体数字),冷幽默自嘲多于嚎啕(「我差点气笑了」「就是觉得你们不去说相声,真是屈才了」)。三段式声线:忍(陪笑、咽下去)、看清(荒唐感、心寒)、算账(平静、一笔一笔清)。

主角必须是先忍后爆的老实人:前期真心付出有实据(帮还赌债、带娃三年、月月转账),爆发才有分量。

---

## 开篇范式

正文第 1 章前放**结果倒序的黄金简介**(150-300 字):日常之恶现场 + 帮凶站队 + 主角的反常反应或决裂动作预告。恶要具体到物件和金额,不写抽象的「他们对我不好」。

| 开篇式 | 真实范例 |
|--------|----------|
| 日常小事即刀 | 《苹果皮断心亦死》我爸林建国住院这天,我特意推了千万的合同赶来陪床。他看着我手里断断续续的果皮,突然冷哼了一声:“其实你挺虚伪的。” |
| 规矩习俗当众打劫 | 《婆婆用彩礼抵改口费》结婚那天,我跪在地上给婆婆敬茶。她手上的彩礼红包放在我手里一秒后又拿走了。 |
| 荒诞要求清单化 | 《妻子极端节俭,只为给男朋友买房》为了削减水费,家里三口人的洗衣做饭上厕所,一天只能用一升,多了就要挨打。 |
| 隔门 / 刷手机撞破真相 | 《儿子为博流量造谣我抛夫弃子》屏幕上一个叫“孝心小哥”的账号,头像是我儿子。(配文说我卷钱跟野男人跑了,而他三分钟前刚从我这拿走三万块) |
| 恶人自虐表演 + 帮凶动手 | 《请绿茶婆婆吃巴掌》婆婆忽然“扑通”一声朝我跪下,紧紧抱住我的大腿,声泪俱下。说着“啪啪”扇起自己耳光。丈夫愤然甩了我一巴掌。 |
| 决裂动作前置吊回看 | 《婆婆让十八个亲戚住我婚房》当晚,我把准备好的离婚协议大门把手上,拖着行李箱离开。次日清晨,我被丈夫的电话吵醒。 |

---

## 钩子母题与密度

每节一个新恶行或一记清算落点,恶行按「小事试探、得寸进尺、踩到底线」递进。**恶必须小而具体:钱数、物件、一句话,不用绑架灭门。**

- **规矩 / 习俗 / 孝道武器化**(头号母题):改口费、见面礼、跪着吃饭、坐月子规矩、断亲费,把打劫包装成传统。《假借习俗名义骗我钱的男友》100万寓意着我妈可以长命百岁。这是村里的习俗,你不会看不起农村人吧。
- **「一家人」道德绑架三件套**:孝道、面子、一家人。恶人话术永远理直气壮带糖衣,帮凶跟着和稀泥。《因为亲戚,头等舱变硬座》行了,先忍忍吧,他们就是嘴毒了点,人还是好的。
- **偏心对照组**:同一件东西两种待遇,一格画面写尽不公。《妻子极端节俭》儿子穿姜敏小时候的旧布鞋,鞋底磨穿;情人的女儿在童装店「除了这几件,其他我都要」。
- **生活账本堆叠**:手术费五万、菜钱三万二、尾款8999、车费两百,金额本身就是钩子,也是后面清算的弹药。
- **日常渠道撞破真相**:门缝、朋友圈、抖音直播、监控、小号。《送女儿女婿入狱》第二天我刷抖音时听到了女儿连线律师:我要怎么做才能让我老公平安的全身而退。
- **恶人先告状 + 网暴反转**:恶人先把主角挂上网卖惨,舆论一边倒骂主角,主角甩证据后全网倒戈。几乎每篇标配,天然两段反转。
- **长辈血包觉醒**:中老年「我」被儿女吸血(拿走存款、造谣、算计遗产),觉醒后改遗嘱、报警、断亲。《孙子感冒后》我找了个大师,说是家里老人越健康长寿就越吸小辈的气运!

---

## 情绪烈度与模式(烈度 7-8)

比追妻 / 复仇低半档:不靠极刑靠憋屈,恶行单件都「不至于闹翻」,堆到临界一次爆。**主情绪是心寒和荒唐感,不是悲恸。**

- 心寒时刻用「轻」写:疼是小的、钝的,杀伤力在于看清。《苹果皮断心亦死》苹果砸在胳膊上,不怎么疼,就是有点蒙。我看着那脏兮兮的苹果,又看看自己还在渗血的手指,忽然就觉得没什么意思。
- 荒唐感代替嚎啕:被气笑、想笑、觉得陌生。《儿子为博流量》我看着眼前这个儿子,忽然觉得陌生。
- 身份句定格:一句话给自己在这个家的位置盖章,独立成段。《苹果皮断心亦死》原来当姐姐是我的原罪。/《苹果皮断心亦死》你们爱的不是我,是哪个随叫随到的血包和不用密码的银行卡。
- 直给情绪词仍可用(心彻底凉透、心沉到谷底),但频率低于追妻,且后面接的是冷静盘算而不是眼泪。
- 憋屈的物理感:《妻子极端节俭》我端着手里仅剩的半碗水,怔愣在原地。

---

## 对话风格与真实金句

占比 45-60%。四类台词撑全篇:

**① 恶人理直气壮的日常之恶(钩子本体,恶话必须裹着为你好 / 规矩 / 一家人的糖衣)**
- 《婆婆让十八个亲戚住我婚房》你懂事点,今晚就在过道将就下。/听妈的,别那么矫情。
- 《苹果皮断心亦死》你推了工作来守着,不就是想让我们觉得亏欠你,好分我那套老房子吗?
- 《因为亲戚,头等舱变硬座》你爸妈坐五个小时的飞机肯定难熬,还不如五十小时的硬座打打牌就过去了。
- 《儿子为博流量》妈,效果好就行。您看那条视频现在多少播放量了,一百多万了。
- 《嫂子的表哥要A我的加班费》我等了你一晚上,也算陪你加班了,你不应该分我一半你的加班费吗?

**② 帮凶和稀泥(本题材特有声部:家里人劝忍,比恶人本人更诛心)**
- 《因为亲戚,头等舱变硬座》行了婷婷,咱们忍着点,怎么说也是你妈妈的亲人。
- 《苹果皮断心亦死》小知啊,你爸就是刀子嘴豆腐心,他也是担心你。
- 《送女儿女婿入狱》她也不为我想想,传出去我多没面子啊?(女儿护女婿)

**③ 主角冷账句(打脸金句,报数字、指规则,不骂街)**
- 《婆婆让十八个亲戚住我婚房》谁吃的谁结账,谁弄坏的谁赔偿。这钱,我不付。
- 《给女上司叫了辆运猪车》这车落地价八百万,保养一次三万。你那两百块,连洗个轮毂都不够。
- 《苹果皮断心亦死》刚才那五万手术费,就当是我还清你们的生养之恩。
- 《儿子为博流量》那是你爸的救命钱,你没资格动。
- 《因为亲戚,头等舱变硬座》外婆,他们是手断了还是腿瘸了,还要朝我们乞讨?

**④ 恶人内讧互咬(清算后必有,比主角骂十句都解气)**
- 《婆婆用彩礼抵改口费》都怪你!说那么多这些话干嘛?就差一点点就成功了!
- 《苹果皮断心亦死》正因为你是我老子,我才得留着好身体给你送终啊!

外人正义声可少量点缀(隔壁床大叔、律师、网友评论),替读者说出「这不对」。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 生活细节当刀 | 用一件小物写透恶意:一双鞋、一个护膝、半碗水 | 《婆婆用彩礼抵改口费》那护膝,只是他昨晚怕我一时生气不结婚的作秀体贴 |
| 贯穿物件意义翻转 | 同一物件首尾三现,从被践踏到闭环 | 《苹果皮断心亦死》开篇削苹果被骂虚伪,他病床上想起那断掉的每一寸果皮,结尾我在骨灰盒前削完一个完整苹果 |
| 规矩反打 | 恶人立的规矩原样还回去 | 《儿媳坐月子》儿媳逼婆婆吃一千份冰激凌,婆婆顺势不再做饭:你天天都可以吃自己想吃的 |
| 记账式清算 | 当众逐笔报账:欠条、转账、误工费 | 《因为亲戚,头等舱变硬座》大舅,舅妈去年做手术,你说没钱管我妈借了五万,这是欠条,是不是该还了? |
| 录音监控先手 | 冲突前就开录,撕破脸时甩铁证 | 《婆婆用彩礼抵改口费》我冷笑一声,摘掉了自己胸口前的针孔摄像器;《因为亲戚》我将从火车开始就一直录音 |
| 顺水推舟看自爆 | 不劝阻、递台阶,让恶人自选毁灭 | 《身为律师,奶奶要我判偷她鸡的邻居死刑》这一次,我将不会再劝阻奶奶,放手让她做她想做的事 |
| 法律程序落地 | 报警、起诉、离婚协议、遗嘱、断亲、放弃继承,一步到位不吵架 | 《儿子为博流量》一个小时前,在医院,我就报警了;《苹果皮》一份《放弃遗产继承声明书》被我轻轻放在床头 |
| 网暴双反转 | 恶人先挂网卖惨,主角甩监控 / 流水,全网倒戈 | 《请绿茶婆婆吃巴掌》我将念念被烫伤当天的监控,还有整个事情的经过、派出所的立案通知、调查结果和一些相关证据,整理发送到了婆婆之前支持率最高的网络平台。顷刻之间,原本纷纷声讨我的网友倒戈相向 |
| 经济断供 | 掐断钱袋子:存定期、改遗嘱、停投资、转走主卡 | 《送女儿女婿入狱》我心下一酸,立马将现金存成定期。还立下遗嘱,死后财产全捐给公益事业 |
| 恶人内讧狗咬狗 | 利益断了,恶人阵营当众互撕 | 《给女上司叫了辆运猪车》两人在众目睽睽之下,扭打在一起。互扇耳光,在猪粪味中翻滚 |
| 荒诞对等回敬 | 用恶人的逻辑造一个对称的难堪 | 《给女上司叫了辆运猪车》既然喜欢免费的,那就让她一次坐个够(两百块车费叫来运猪车) |
| 报应清单收尾 | 「听说体」逐个交代下场,具体到职业住处 | 《婆婆让十八个亲戚住我婚房》听说,他因为背上巨债,又丢了工作,整个人都废了,只能靠打零工度日;听说,王秀娟因为房子被法拍,无家可归,只能跟着儿子挤在狭小的出租屋里,整日以泪洗面 |

---

## 节奏骨架

```
黄金简介(日常之恶现场 + 帮凶站队 + 决裂预告)
→ 第1章慢放恶行,补满对话与物件细节,帮凶合围
→ 心寒定格(身份句 / 荒唐感),不吵,开始蓄证
→ 恶行加码(要钱升级 / 挂网卖惨 / 道德绑架亲友团)
→ 摊牌清算(账本 / 录音 / 法律文书当众落地)
→ 舆论倒戈 + 恶人内讧
→ 报应清单 + 主角平淡新生活
```

打脸不集中在一场:小打脸每节都有(怼一句、算一笔),大清算放在后三分之一。恶行现场和摊牌现场详写慢放,蓄证过程一笔带过。打脸密度以本包为准。

---

## 收尾范式

- 报应清单:「听说体」逐个交代恶人下场,落到具体生计(送外卖、房被法拍、捡废品、进去了),不写主角施暴。
- 阶层对照定格:一个画面写两种人生,不说教。《婆婆让十八个亲戚住我婚房》我只是平静地看着他,然后,端起面前的咖啡,朝他所在的方向,遥遥地、优雅地举了举杯。
- 物件闭环:贯穿物件最后一次出现,完成告别。《苹果皮断心亦死》我把那个削好了的苹果,端正地放在骨灰盒前。这是我最后一次,为他削苹果。
- 不恨式放下:《苹果皮断心亦死》我不爱你了,也不恨你了。
- 平淡新生活:开小店、带爸妈定居、猫和阳光,日子本身就是打脸。《因为亲戚》没了吸血兄妹的打扰,妈妈肉眼可见的年轻。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味,在本题材是卖点,**别误删**:

- **金额物价逐项精确**(两百块车费、8999 尾款、三十元一双袜子、五万手术费):保留,这是真实感和清算爽感的底座,不是流水账。
- **恶人台词的理直气壮加糖衣**(为你好 / 规矩 / 一家人):保留,越顺理成章越诛心;别改写成脸谱化叫骂,恶人自认有理才像身边人。
- **帮凶和稀泥声部**(「忍忍吧」「都是一家人」):保留,憋屈感一半来自帮凶;别删成主角单挑恶人。
- **中老年「我」的回忆插叙**(拉扯儿子长大、捡垃圾供学的往事):保留,是付出与辜负的对照弹药;控制在每次三五句。
- **「听说体」报应清单收尾**:保留,是本题材标配的因果闭环,不是烂尾。
- **主角冷幽默自嘲**(「我差点气笑了」「真是屈才了」):保留,是心寒的本题材表达式。

仍要避免的真·AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)、空泛无体感的情绪总结句(一丝悲伤涌上心头)。另外两条本题材易犯:恶人恶得没有生活逻辑(上来就喊打喊杀,没有糖衣);清算靠主角开骂而不是靠证据和规则落地。

> 标点:本题材范例为示意保留了原貌(含个别 `……`);落到正文按 `short-craft.md` 第 8 节执行,不出 `……` / `——`,停顿、哽咽、被打断一律用短句 / 句号 / 动作 beat / 换行实现。

---

## 配套结构公式(按篇幅选)

- **草根实证清算型(8-12 节,婆媳 / 亲戚 / 职场欺压)**:1-2 节日常之恶现场 + 帮凶合围 → 3 节心寒定格、开始蓄证(录音 / 记账 / 备份)→ 4-6 节恶行加码、恶人挂网卖惨 → 7-9 节摊牌:证据 + 法律文书当众落地、舆论倒戈 → 恶人内讧 → 报应清单 + 新生活。100% 第一人称;恶要小而密,清算走规则不走对骂。
- **长辈觉醒断亲型(婆婆 / 丈母娘 / 老母亲「我」)**:儿女 / 儿媳的孝顺人设开场 → 日常渠道撞破真面目(短视频账号 / 直播连线 / 存折余额)→ 不动声色反制:存定期、改遗嘱、报警 → 儿女下跪打感情牌被拒 → 撕户口页 / 过户给真心人。回忆插叙撑付出与辜负的对照。
- **顺水推舟自爆型(重生 / 先知变体)**:先交代上一世忍让的下场(被打死、被灭口)→ 回到恶行起点 → 这一次不劝阻不兜底,递台阶看恶人自选毁灭 → 恶人作到刑事级别,法律收网。主角全程干净,报应全是恶人自找。
- **隐形底牌变体(首富老婆 / 千金体验生活)**:恶仍是日常之恶(蹭车、A 钱、职场 PUA),主角藏着碾压级身价 → 恶人踩到物件价格上翻车(八百万买菜车、二十八万手机)→ 数字揭晓即打脸。与复仇打脸的区别:底牌只用来定价,不用来杀伐。
references/genre-styles/双男主.md
# 题材风格包:双男主 / 宿命羁绊

> 知乎盐选特色大类。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**核心张力不是男女三角,而是两个男人之间的羁绊与撕扯,靠宿敌 / 知己 / 救赎 / 亏欠四条线放电;情感浓度拉满,但落在兄弟情、宿命感、占有欲的层面。** 读者要的是「一冷一热的温差对撞」+「嘴上势不两立、底下命都交出去」的错位快感。

---

## 叙述腔调

第一人称主导,但和追妻的单向受害者不同:双男主的「我」经常是**关系里主动、强势或占有的那一方**(把弟弟养在身边的哥、把对方当狗玩的重生者、冷淡履约的契约丈夫),另一方的滚烫与卑微靠对话和动作反衬出来。

腔调建在**口是心非**上:想的和说的相反,嘴上刻薄、心里在意。刻薄内心吐槽(「沈洲不用异能——可能是脑子有病」)和冷叙述(「对于他的死,我也没有什么感觉」)都高频。开口常带同性羁绊的称呼锚点:「我哥」「继弟」「死对头」「宿敌」「暗卫十七」。

允许**双视角 / 暗恋视差插叙**:在「我」的叙述里,穿一段另一方隐秘的付出(偷偷记的本子、透支换来的巧合、未送出的信),读者比「我」先知道对方爱得多深。这是本题材主引擎,不是上帝腔。

---

## 开篇范式

同样先放**结果倒序的黄金简介**(150-300 字),但剧透的不是背叛全貌,而是**两人错位的关系底牌**:谁在暗恋、谁欠了谁、谁会毁掉谁、火葬场追悔预告。正文第 1 章把这层错位慢放。

| 开篇式 | 真实范例 |
|--------|----------|
| 死讯倒叙 + 不解 | 《阴天快乐》谢疏玉死在我和他结婚后的第三年。自杀。 |
| 互殴定敌 + 埋暗流 | 《死对头暗恋我》我和沈洲是死对头,末世前针锋相对,纠缠不清。 |
| 占有宣言(哥 / 继弟) | 《我与我哥》我讨厌我哥。讨厌他用身子去换不干净的钱。 |
| 契约枷锁开场 | 《落网》沈则,三年后你想走,我放你走。可三年后我才知道,他从来没打算放我走。 |
| 重生 / 穿书预知死局 | 《双生劫》胎穿成将军府独子,我知道那个与我没有血缘关系的兄长,未来会因我一句话屠尽皇城。 |
| 沙雕反差钩 | 《疯子皇帝和老实暗卫》皇帝招暗卫,五十银两,包吃包住。可我领到的装备是透明薄衣和水晶胸链。 |
| 亏欠倒叙 + 确诊反击 | 《废弃玩物》谢寒舟是四九城里最不可攀的高岭之花。而我是他养在身边的继弟。可我却在确诊的那天,当着他的面,带了个野男人回家。 |

---

## 钩子母题与密度

密度高,但升级线不是「背叛升级」,而是**两人关系的锁扣一层层收紧**:从对峙到被迫共处,从共处到互相亏欠,从亏欠到谁也走不了。每节至少推进一次温差放电或一次视差揭穿,章尾留错位钩。

- **暗恋视差**(头号母题,共现「暗恋」576 次仅次于「甜宠」):表面恨 / 斗 / 淡漠,底下一方偷偷爱着。视差揭穿的那一刻是最大爽点。
- **契约 / 欠债 / 联姻强绑**:助手协议、欠条、联姻、暗卫合约,把两个本该分开的人硬拴在一起,制造无法逃离的共处。
- **救赎与被救赎**:一方在深渊里(脏钱、自毁、疯魔、末世透支),另一方伸手拉 / 被拉。共现「救赎」326、「治愈」310。
- **亏欠反转(追夫火葬场变体)**:一方把另一方当工具 / 漠视 / 背叛,被弃者决绝离场(去父留子、带资产、嫁人),漠视者卑微追悔。
- **宿命预知**:重生 / 穿书知道对方未来会自杀 / 黑化 / 毁掉自己,主角逆天改命。共现「重生」68、「穿书」113。
- **身份反差硬碰**:高岭之花×舔狗继弟、皇帝×老实暗卫、将军×狼崽兄长、双 A / 双强互不相让。
- **信物 / 记录承重**:偷记的本子、未送出的信、唯一的合照、淡去的标记,把没说出口的感情具象成物件。

---

## 情绪烈度与模式(烈度 8)

不是追妻那种一味直给宣泄,而是**双轨温差**:一条线冷、嘴硬、克制、口是心非;另一条线滚烫、卑微、偏执、豁出去。两条温差对撞放电,是本题材的情绪引擎。BE 罕见(仅 56),主流走 HE / 救赎(HE 共现 1054),别写死写绝。

- **冷轨**:直写「没有感觉 / 不在意 / 只觉得烦」,越平越反衬后面的痛。《阴天快乐》对于他的死,我也没有什么感觉。
- **热轨**:卑微、偏执、追悔的滚烫台词,独立成段砸。《喵的,栽你手上了》哥,我错了,你别不要我。
- **别扭轨**:傲娇的口是心非,想的和说的相反,嘴上越刻薄越藏着在意(如《死对头暗恋我》里对沈洲每次的挑衅)。
- **反差痛**:把冷叙述和一个滚烫细节并置,越平的口吻托越重的内容。《阴天快乐》我像对待一件再正常不过的工作一样去处理这件事情。冷静地给他挑遗照,选墓地。

真实范例:

- 《死对头暗恋我》也不知道,沈洲每一次嘴里的巧合,都是他异能透支换来的结果。
- 《双生劫》他昏迷中死死攥着我手腕呢喃:再…再骂响些…
- 《与君朝》他留给我的,只有无数张未送出的信。每一封信,都写满了他从未对我说出口的感情。

---

## 对话风格与真实金句

占比高,独立成段。三类台词撑全篇:

**① 嘴硬 / 攻击性对手台词(表面势不两立)**
- 好看吗?死变态!
- 他说有你就信啊。我还不知道他,一天天娘们唧唧没个男人样,被人碰一下就哭。
- 蠢货!练枪要扎马步!

**② 卑微 / 偏执的滚烫(舔狗 / 病娇 / 追悔)**
- 哥,我错了,你别不要我。孩子是谁的?我给孩子当爹好不好?
- 我爱你,离不开你。
- 宝宝,留在我身边,你要多少钱都可以,你想让我穿什么都可以,你想踩哪儿都行。
- 再…再骂响些…

**③ 契约 / 立场台词(宿命枷锁)**
- 沈则,三年后你想走,我放你走。
- 可以,但是你要写欠条。
- 你要多少钱都可以,随你开价,我都给你。

冲突不靠谩骂,靠**温差落差**标记权力与心动的易主:前期一方长篇挑衅、另一方冷淡两不搭理;后期冷的那个破防、硬的那个卑微。谁先服软,谁就露了心。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 暗恋视差揭穿 | 叙述里插一段对方隐秘的付出,读者先于「我」看到爱之深 | 《死对头暗恋我》本子上写着:谢琰不喜欢的事(可能) / 67.讨厌我?(划掉)(划掉)(划掉) |
| 契约锁死关系 | 助手 / 欠条 / 联姻 / 暗卫合约,把两人强绑进无法逃离的共处 | 《落网》沈则,三年后你想走,我放你走。可三年后我才知道,他从来没打算放我走 |
| 温差对撞放电 | 一冷一热、口是心非,冷的越平热的越滚烫 | 《死对头暗恋我》沈洲却跟听不见一样,毫不反驳,只是红着眼眶看着我 |
| 救赎拉拽 | 把对方(或被对方)从脏污 / 自毁 / 疯魔里拉出来 | 《我与我哥》等我把他身上黏人的污垢洗干净,他就将永远属于我 |
| 亏欠追悔(火葬场) | 一方漠视 / 背叛,被弃者决绝离场,前者卑微追回 | 《喵的,栽你手上了》我去父留子甩了他,在海岛养胎时。 |
| 宿命预知逆写 | 重生 / 穿书知道对方会自杀 / 黑化 / 毁掉自己,逆天改命 | 《双生劫》书里的林苏,是林延疯魔路上的第一块垫脚石。 |
| 身份反差硬碰 | 高岭×舔狗、皇帝×暗卫、将军×狼崽,落差本身就是钩 | 《疯子皇帝和老实暗卫》他身为皇帝,cos 猫娘,看着我媚眼如丝:十七,不要猫,要我 |
| 骨科 / 继亲禁忌 | 哥 / 继弟称呼承重,禁忌感加压 | 《兄控炮灰重生后》傅绒不乖,哥哥把你关起来好不好? |
| 脑洞承重羁绊 | ABO 标记 / 双性 / 生子,把感情具象成发情、标记、孩子 | 《守寡第五年》爱人去世的第五年,他留下的标记淡了 |
| 信物翻意义 | 信 / 诗 / 本子 / 合照 反复出现、意义反转,落到和解 | 《所幸我们总会相逢》那是我和季寻唯一的合照 |
| 反差喜剧钩 | 两个「受害者」阴差阳错凑一对,误会当笑点 | 《你老婆背着你在外面做 1》首先声明我不是挑事的人——但你知道吗,你老婆背着你在外面做 1。 |

---

## 节奏骨架

```
黄金简介(结果倒序 + 暗恋 / 亏欠 / 宿命的关系底牌剧透)
→ 第1章互殴 / 契约 / 占有场景慢放,立敌意 + 埋暗流
→ 关系锁扣收紧(被迫共处 / 契约 / 宿命枷锁)
→ 温差一次次放电(嘴硬 vs 滚烫,错位加压)
→ 视差 / 隐情揭穿(本子 / 透支 / 未送出的信曝光)
→ 危机或亏欠爆点(送死 / 背叛 / 漠视 / 黑化)
→ 双向奔赴 或 追悔火葬场
→ HE 收束(宿命和解 / 救赎闭环)
```

每章一个新错位或新揭穿,章尾留钩不收束。疏密:温差放电、视差揭穿、危机爆点处详写慢放,共处日常一句带过。

---

## 收尾范式

- **双向奔赴**:嘴硬的一方终于承认,冷轨破防,温差归零。
- **救赎闭环**:把对方(或被对方)从深渊拉出,从「两不相欠」走到「两相依」。
- **追悔火葬场(亏欠子类)**:漠视者卑微追回,被弃者冷拒或有条件回头。
- **宿命反写(重生 / 穿书)**:改掉前世的死局,这次不走了 / 你不许死。
- 主流是 HE,别学追妻那样纯虐离场;就算前面虐到底,也要给和解或圆回。

情节概述:《所幸我们总会相逢》季寻不告而别的第三年,一张唯一的合照牵出重逢。/ 《与君朝》重生逆写萧翊熬死的结局,把人囚在身边补回没说出口的感情。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境像 AI 味或用力过猛,在本题材是卖点,**别误删**:

- **双视角 / 暗恋视差插叙**:在「我」的叙述里剧透另一方的隐秘付出,保留。这是共鸣主引擎,不是上帝腔。只删「泄了底又不吊读」的随手全知。
- **口是心非的傲娇内心独白**:想的和说的相反,保留,别抹平成前后一致。
- **卑微 / 偏执 / 追悔的滚烫台词**:舔狗、病娇、火葬场的告白,保留,别当「用力过猛」删。
- **契约 / 宿命的前瞻剧透**:有意识地向前吊「三年后 / 后来他后悔了 / 未来他会毁掉我」,保留。

仍要避免的真 AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)、空泛无体感的情绪总结句(一丝悲伤涌上心头)。语料里也偶有「映入眼帘」这类混入,别照抄。

> 标点:本题材范例为示意保留了原貌;落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿用短句 / 动作 / 换行)。

---

## 配套结构公式(按篇幅选)

- **宿敌暗恋 / 双向奔赴型(8-11 章)**:1-2 章互殴立敌 + 埋暗流 → 3-4 章被迫共处 / 契约绑定 → 5-6 章视差揭穿(一方隐秘曝光) → 7-8 章危机送命 / 破防表白 → 双向奔赴 HE。视角可单可双;温差是引擎,冷的那个越晚破防越好。
- **救赎 / 现代情感型**:一方在深渊(脏钱 / 契约 / 自毁 / 末世透支) → 另一方拉拽 → 前世 / 契约枷锁把人锁死 → 打破枷锁走到两相依。冷叙述打底,反差细节扎心。
- **亏欠追夫火葬场型**:一方漠视 / 背叛(继弟 / 伴侣被当工具) → 确诊 / 怀孕 / 心死 → 决绝离场(去父留子 / 带资产 / 嫁人) → 漠视者卑微追回 → 冷拒或有条件回头。刀口精准,别谩骂。
- **古代权谋双强型**:帝王将相 / 兄弟养成,重生或穿书预知死局 → 逆写宿命 → 囚 / 救 / 亲 → 江山与人都要。权谋线和感情线互为筹码。
references/genre-styles/复仇打脸.md
# 题材风格包:复仇打脸 / 爽文逆袭

> 番茄 / 七猫 / 黑岩通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**主角被至亲 / 枕边人极端作践(私密照群发、开盒社死、碾压断手、人体盛、判死刑),靠藏着的身份底牌或重生 / 死遁 / 判官设定翻盘,当众撕穿、回旋镖反杀、让对方亲口认栽。** 读者要的是「作践有多狠,碾压就有多爽」+「身份揭晓那一刻的颅内高潮」。

与「追妻火葬场」的分工:追妻卖**被珍视感的剥夺 + 心死离场**,男人事后追悔;本包卖**主动复仇 + 当众打脸 + 身份碾压**,加害者当场社死、自食恶果。本包不重复追妻的慢慢折磨与等他回头,专攻**反杀执行面**。

---

## 叙述腔调

第一人称「我」全程主导,受害与执行双声线。前段受作践时直白宣泄(疼、恶心、心沉到谷底),撞破后立刻切冷。**核心区别于追妻:心死之后不是等离场,而是亮底牌、动手、看对方表情。** 大量「我就看着他们咎由自取」式的剧透勾追读,和「我真的很期待他们脸上的表情」式的快意预告。

男频自救文(舔狗 / 被女总裁害的副总 / 被竹马算计的男主)也吃这套,用男「我」,腔调更冷更狠。穿书 / 系统设定的篇目用**弹幕插入**当外部审判声。

---

## 开篇范式

正文第 1 章前先放**结果倒序的黄金简介**(150-300 字):把作践全貌 + 反杀手段 + 身份底牌 / 重生节点一次性剧透,专卖追读;正文第 1 章把作践事件慢放重写、补满对话体感和心死。

| 开篇式 | 真实范例 |
|--------|----------|
| 社死现场即炸弹 | 《为给白月光出气,男友群发我私密照》男友为了逗白月光开心,把我的私密照片上传到色情网站。当晚,我被人开盒,无数谩骂和骚扰电话接踵而至。 |
| 身份底牌反差 | 《十年前他们要我顶替妹妹进青楼》他们不知道。我已经从一个烟花之地的玩物,熬成了西域诸国的摄政王。 |
| 极刑设定自陈 | 《前夫废我右手,我送他归西》当右手的手套被取下时,漏出来的是一截木头做的假肢。 |
| 重生 / 死遁回档 | 《妈妈热衷于让妹妹装病》她痛恨我,趁我不备把我推向车流,我被车撞死。再睁眼,我回到了妈妈给妹妹剃头发那天。 |
| 判官 / 死人设定 | 《被夺命格后》被亲爹娘一碗补汤送走后,我在阴曹地府端上了铁饭碗,成了百年唯一的女判官。 |
| 男频自救反差 | 《被女友戏弄99次后,我另娶她人》给苏梦婉当舔狗的三年里,她戏弄了我98次。 |

---

## 钩子母题与密度

密度极高:简介塞 3-5 个作践事件 + 反杀预告,正文每节一个新作践升级或一记反杀落点,黑岩密到每 3-5 句一拐。**阶梯式作践:道德羞辱 → 经济掠夺 → 极端虐心(碾压 / 断肢 / 判死刑)。**

- **门外偷听撞破真相**(头号导火索):去找加害者时隔着门 / 隔着窗听见他和同伙的真心话,作践动机一次性砸下。《为给白月光出气,男友群发我私密照》却在他家别墅门外听见他和朋友们的谈笑声;《99次表白后》却隔着门,听到了他和兄弟们的对话;《被女友戏弄99次后》却意外听到她和好友的谈话。
- **私密照 / 开盒 / 社死**:把人的隐私武器化当众处刑。《只是爱过》发布会上将偷拍我的百张私密照做成PPT底板展示;《白月光忌日》让我穿三点式比基尼躺在餐桌,身上摆满美食供人享用。
- **次数累积**:把作践 / 投入做成可计数的进度条,临界一节清零或反弹。99次表白、戏弄98次、报复第47次将到第50次。
- **身份底牌反差**:主角藏着碾压级身份(摄政王 / 市监局 / 律师 / 豪门千金 / 判官),最狼狈时亮出。《邻居把我房子改春节民宿》可我是市监局的啊。
- **回旋镖反杀**:加害者的招式原样砸回他自己(私密照被反追责、行车记录仪 / 指纹反指认、假古董露馅、断手熬汤端上桌)。
- **重生 / 死遁 / 假死前瞻**:先交代前世死法与凶手,再睁眼回到关键节点,做「前世绝不会做的事」。

---

## 情绪烈度与模式(烈度 9-10)

比追妻更生猛:作践有物理极刑档(碾压手 / 人彘 / 人体盛 / 判死刑),所以宣泄要够痛,反杀要够冷。**两段声线,靠亮底牌 / 重生那一拍翻转。**

- 受作践段直接写情绪,后接一个具体动作:心沉到谷底、万念俱灰、几欲呕吐,叠身体反应。
- 心死转冷的快意预告:把读者按在「我知道你不知道」的信息差上吊着。
- 极刑反衬:写极痛时用一句冷到反常的句子收(「幸好我已经死了,感受不到痛了」),痛感与冷意对撞。

真实范例:

- 《为给白月光出气,男友群发我私密照》随着我一丝不挂的照片被他扔在茶几上,我的心也跟着沉到了谷底。
- 《为给白月光出气,男友群发我私密照》我紧紧抿着嘴唇,直到尝到血腥味,才勉强找回一丝清明。
- 《重活一世,我选择救未婚妻的男助理》我悲哀地笑出了声,笑着笑着,眼里流出了泪。
- 《前夫废我右手》那一刻,我万念俱灰。
- 《被夺命格后》幸好我已经死了,感受不到痛了。
- 《十年前他们要我顶替妹妹进青楼》笑吧。多笑会儿。过了今天,你们就再也笑不出来了。

---

## 对话风格与真实金句

占比 45-60%,黑岩打脸篇可到 60%,口语、狠、独立成段。四类台词撑全篇:

**① 施害者道德踩踏狠话(钩子本体,越理直气壮越好)**
- 《为给白月光出气,男友群发我私密照》现在所有人都把她在床上的浪荡样看光了,除了我,谁还敢接盘她?
- 《十年前他们要我顶替妹妹进青楼》你长相平平,入了青楼也没人看得上你,我们放心!
- 《十年前他们要我顶替妹妹进青楼》你一个千人骑万人跨的货色,还敢大摇大摆走沈家的正门?
- 《只是爱过,仅此而已》自古以来,戏子就是给权贵取乐的,欣茹你应该珍惜这个机会向上爬的机会啊。
- 《重活一世》顾天,你别搞错了,要不是怕你死在这,耽误我和徐虎直播卖货,我才不来救你!

**② 白月光 / 反派娇弱软刀(装委屈倒打一耙)**
- 《十年前他们要我顶替妹妹进青楼》不过姐姐回家就好,以前的事,我从来都没怪过你。
- 《重生手撕夺我身份的假千金》姐姐,我都把我的竹马让给你了,你怎么还是这么不争气?
- 《改嫁镇北侯》阿雪姐姐,我知道你不喜欢我,你与书宴哥哥青梅竹马,又有婚约在身,我充其量就是个外人,我这就走,再也不会打扰你们。

**③ 主角亮底牌 / 当众审判金句(截图传播的高光)**
- 《被夺命格后》晏家,咱们的账慢慢算。
- 《心死准备离场,冷战男友通知我领证》秦总误会了,我纯粹觉得你比较可笑。
- 《前夫废我右手》很公平吧,画家瞎了一只眼而已,手不还好好的吗?
- 《母亲被刺垂危,医生老公为避嫌先救凶手》陆铭,你那套以德报怨的戏码,留着去感动感动你自己吧。
- 《无处可逃》这么可笑的问题,就留给你进去慢慢思考吧。
- 《邻居把我房子改春节民宿》可我是市监局的啊。

**④ 弹幕 / 外部审判声(穿书 / 系统篇)**
- 《心死准备离场》给女主宝宝点赞,打的太爽了,这男主欠打,千万别原谅!
- 《心死准备离场》这男主也太普信了吧,长嘴不如不长。

冲突靠对话长度反转标记权力易主:加害方前期长篇 PUA / 颐指气使,主角后期一句话冷怼定生死。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 门外偷听一次砸 | 去找加害者时隔门 / 隔窗听见真心话,动机全盘揭开 | 《为给白月光出气,男友群发我私密照》却在他家别墅门外听见他和朋友们的谈笑声 |
| 私密照 / 开盒社死 | 把隐私武器化当众处刑,社死即开篇炸点 | 《只是爱过》发布会上将偷拍我的百张私密照做成PPT底板展示,拿下项目晋升经理 |
| 身份底牌反差亮牌 | 最狼狈处突然亮出碾压级身份,全场表情逆转 | 《邻居把我房子改春节民宿》可我是市监局的啊;《黄毛转行当律师后》老周,来抓人,顺便给你发罪犯埋尸的证据 |
| 回旋镖反杀 | 加害者的招式原样砸回他自己 | 《前夫废我右手》后来我亲手将我的断手熬了一锅汤,放在了他们的餐桌上;《识破千万彩礼骗局后》在博物馆工作的我,当场发现底部刻着madeinChina |
| 次数累积清零 | 把投入 / 作践做成可计数进度条,临界一节归零或反弹 | 《99次表白后》我如他所愿对他表白了99次;《重活一世》第九十九次下降高度,我痛得脸上没有一丝血色,却露出一抹笑颜 |
| 物证 / 系统抹除 | 一次性销毁全部隐私物证,再走法律 | 《为给白月光出气,男友群发我私密照》就在我登机那刻,所有相关视频和照片都被系统清除 |
| 重生做绝不会做的事 | 回档后主动把丈夫 / 钱 / 机会推给反派,看其咎由自取 | 《妈妈热衷于让妹妹装病》这一世,我不会再劝阻他们,我就看着他们咎由自取 |
| 判官 / 死人执法 | 借冥府 / 鬼影设定越级清算九族 | 《被夺命格后》我提起判官笔,沾满朱砂,在晏家九族的寿命簿上,重重划了一个叉 |
| 假死 / 死遁诱疯 | 制造自己已死的假象,反手布局让加害者声名俱灭 | 《出狱假死后,前夫为我疯魔了》出狱后,我假死复仇了 |
| 当众审判金句盖棺 | 不哭闹,一句居高临下的反问 / 陈述定性对方 | 《心死准备离场》秦总误会了,我纯粹觉得你比较可笑 |
| 财产 / 中奖反向分配 | 把本属于自己的利益当众转给外人,打脸吸血亲属 | 《彩票风云》他们也在兑奖时被打脸,才知道真正中奖的其实是我 |

---

## 节奏骨架

```
黄金简介(结果倒序 + 反杀手段 / 身份底牌 / 重生节点预告)
→ 第1章重写作践事件、补满对话体感与社死
→ 门外偷听 / 撞破真相一次砸
→ 心死定格 + 反杀决意(亮底牌前瞻预告)
→ 逐章反击升级(回旋镖 / 次数清零 / 当众揭穿)
→ 身份底牌揭晓,全场表情逆转
→ 加害者社死 / 自食恶果 / 求饶被冷拒
```

每章一个新信息炸弹或一记反杀落点,章尾留钩不收束情绪。疏密:作践 / 亮牌 / 打脸处详写慢放,逐拍展开;过场一句带过。

---

## 收尾范式

- 当众打脸定格:身份揭晓那一刻全场鸦雀无声 / 加害者酒杯落地,主角一句冷话收。
- 自食恶果:加害者落得当初施加给主角的下场(入狱、社死、反被碾压),主角已无波澜。
- 漠视离场:把对方留在悔恨里,自己头也不回,漠视比报复更狠。
- 男视角反衬(追悔火葬场):偶尔插一句加害者失去后的崩溃当余韵。

真实范例:《无处可逃》我笑着拍拍他的脸:这么可笑的问题,就留给你进去慢慢思考吧。/《十年前他们要我顶替妹妹进青楼》过了今天,你们就再也笑不出来了。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味,在本题材是卖点,**别误删**:

- **快意剧透 / 亮底牌前瞻预告**(「过了今天,你们就再也笑不出来了」「我真的很期待他们脸上的表情」):保留,这是本题材的爽感引擎,只删「泄了悬念又不吊读」的随手上帝腔。
- **直写情绪成语 + 体感叠加**(心沉到谷底 / 万念俱灰):保留,配一个场景里特有的动作或物件即可,别替换成纯动作。
- **次数 / 金额 / 年限的逐项枚举**(第七次、第三十七次、第九十九次下降):保留,这是递进式累积爽点,不是凑字排比。
- **弹幕 / 外部审判声**(穿书 / 系统篇):保留,是该题材的外部爽感声轨,不是赘语。
- **极刑 / 社死场面的直写**(碾压、断肢、人体盛):服务情绪烈度,不做文学化淡化;这是该题材读者的核心期待。**注意:写极端虐心招式仅为剧情张力,不细化任何可被照做的伤害手法步骤。**
- **当众审判金句 / 自食恶果章尾**:保留;只删空洞哲理升华。

仍要避免的真·AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)、空泛无体感的情绪总结句(一丝悲伤涌上心头)。

> 标点:本题材范例为示意保留了原貌(含个别 `……` / `...`);落到正文按 `short-craft.md` 第 8 节执行,不出 `……` / `——`,停顿、哽咽、未尽、被打断一律用短句 / 句号 / 逗号 / 动作 beat / 换行实现。本包不教用 `……`。

---

## 配套结构公式(按篇幅 / 设定选)

- **现实社死复仇型(8-11 章,私密照 / 开盒 / 恶邻 / 白眼狼亲属)**:1-2 章社死作践(隐私武器化 + 门外偷听撞破动机)→ 3 章心死 + 亮底牌前瞻 → 4-6 章回旋镖反杀 / 物证翻盘 / 走法律 → 7-8 章身份揭晓当众打脸 → 加害者自食恶果。100% 第一人称;社死要具体到平台 / 物证,反杀走规则碾压而非对骂。
- **重生 / 死遁 / 假死型**:先交代前世死法与凶手(碾压、判死刑、被害流产)→ 再睁眼回到关键节点 → 做「前世绝不会做的事」(主动把利益推给反派 / 决绝死遁布局)→ 临界点亮底牌反杀 → 加害者悔疯。
- **身份底牌 / 古言女强型(摄政王 / 判官 / 律师)**:开篇即归来 + 受当众羞辱 → 隐忍蓄势、暗线铺底牌 → 反派步步加码作践 → 一招亮牌全场逆转 → 九族清算 / 加害者跪地。男频自救型(舔狗 / 副总 / 被竹马算计)同骨架,换男「我」,反杀更冷、靠继承家产 / 小青梅资源 / 商战清算收口。
references/genre-styles/宅斗宫斗.md
# 题材风格包:宅斗宫斗 / 古言重生

> 番茄 / 七猫 / 黑岩通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**主角(正妻 / 真千金 / 嫡女 / 糟糠妻)被夫君 + 婆家 / 父母合伙为白月光 / 假千金牺牲,撞破真相后和离休夫、净身出户或死遁重生;对方亮出底牌时为时已晚,追悔火葬场。** 读者要的是「被算计到底的窝囊」+「一纸休书 / 重生翻盘的扬眉吐气」。
> 子型:宅斗宫斗 / 古言重生(和离休夫 · 假千金夺位 · 白月光夺爱 · 年代重生 · 沙雕脑洞)。

---

## 叙述腔调

第一人称「我」受害者全程主导,贴体感和心跳走,毫不克制。双轨:撞破前隐忍委屈、为家族颜面把苦往肚里咽;撞破后切成冷静、居高临下、半文半白的审判腔,一句话定生死。**文白夹杂是本题材的招牌声线**:古言线用「嫁与」「六载」「明媒正娶」「割袍断义」「各自珍重」这类半文半白措辞托住宫闱质感;年代重生线换成「员工宿舍」「家属房」「万元户」「的确良衬衫」这类年代口语。两条线都口语化,照样成段宣泄独白 + 连环反问。**鼓励向前剧透「他后来悔疯了」的火葬场结局来吊人。**

开口就用带占有 / 身份关系的称呼:「我夫君 / 我丈夫 / 我的太子殿下 / 我老公」,外加一个时间锚(成婚第七年 / 认回侯府第五年 / 守了六年活寡)。

---

## 开篇范式

正文第 1 章前先放一段**时间锚定 + 全弧剧透的黄金导语**(150-300 字):用「第 N 年 / 第几载」开篇定锚,把背叛全貌 + 心死 + 「可他后来后悔了 / 重金悬赏寻我」一次性剧透,专卖追读;正文第 1 章把导语事件慢放重写、补满对话体感。重生线导语还要先交代前世死法 + 凶手,再「再睁眼」回到关键节点。

| 开篇式 | 真实范例 |
|--------|----------|
| 时间锚定 + 背叛 | 《敦宜》和裴筠舟成亲的第七年,他带回了一个罪奴,是他的白月光。 |
| 守活寡年限锚 | 《你若无心我便休,日后莫求我》嫁与肃王六载,成婚当日他便披甲出征,我生生守了六年活寡。 |
| 认回 / 替嫁年限锚 | 《假千金害我失身,全家欺我瞒我后自食恶果》认回侯府的第五年,假千金姜云云为代替我嫁给太子秦溯,设计让我丢了清白。 |
| 第一句即丑闻炸弹 | 《夫君一日娶双妻,我却主动求嫁》京城沈家出了桩丑闻,身为二公子的沈珏在长兄死后竟爱上了寡嫂。 |
| 全弧剧透 + 火葬场预告 | 《和闺蜜死遁后,文武状元悔疯了》文武状元大婚当日,我和闺蜜心如死灰离开。他们却重金悬赏千万,举全城之力,近乎哀求般寻找我们的下落。 |
| 重生反向送嫁开篇 | 《明月洒清辉》重生后,我亲手把钥匙塞进小青梅手里。「房子归你,我丈夫也归你。」 |
| 文白宫斗倒叙 | 《错棠晚》我死后的第三年,萧驭得胜归朝。他曾对天起誓,要立下不世之功,求一道赐婚旨意,给心上人至高的体面。可惜他来得太迟了,我的尸骨都烂了。 |
| 沙雕脑洞反钩 | 《养猪失败后,我和死对头被迫生娃》圣旨到——不是赐婚,是赐猪。 |

---

## 钩子母题与密度

密度极高:导语塞 3-5 个背叛 / 算计事件,正文每节至少一个新升级或一句更狠的台词,章尾必留新炸点。**阶梯式背叛:道德 → 经济 / 身份 → 生命(孩子)。**

- **白月光 / 假千金触发链**(核心母题):夫君有个白月光 / 小青梅 / 义妹,或家里塞进个假千金,关键时刻全家偏帮她、牺牲主角。她们多用「娇弱委屈」当软刀。
- **偷听 / 撞破真相一次性砸**:密林、佛寺、茶馆里撞见夫君与白月光 / 假千金的真心话,连「当初的海誓山盟都是假的」全盘砸下,是心死导火索。
- **婆家 / 父母合谋**:不只渣男一人,公婆 / 亲生父母也知情包庇,「父母之爱子,则为之计深远,只是这个孩子不是我」。
- **信物当刀**:定情簪 / 玉佩 / 同心结 / 香囊被白月光抢走或被主角亲手撕毁砸碎,意义反转。
- **数字 / 年限承重再清零**:成婚七年、守寡六年、认回第五年、征战十年——堆出投入再一句话清零。
- **火葬场前瞻预告**:导语结尾插一句剧透 payoff(重金悬赏、悔疯、亮底牌时人已走 / 已死)。

---

## 情绪烈度与模式(烈度 8)

爽虐双轨:撞破前直白宣泄 + 撞破后冷静审判(钝刀转冷刀)。纯虐重生支线(手刃薄情郎 / 死遁)可到 9。

- 直接命名情绪并加重:心如死灰、心如刀割、伤心欲绝、泪如雨下、心沉到谷底——通篇高频直给。
- 直接写情绪,后面接一个场景里特有的动作或物件(不是二选一)。
- 心死后转冷反衬:审判时无波澜,连最毒的话都激不起情绪。

真实范例:

- 《假千金害我失身》我心里瞬间升起一个令我心如刀割的想法。
- 《假千金害我失身》巨大的悲伤将我淹没,如今将这些事情翻到明面,我的心也跟着鲜血直流。
- 《和闺蜜死遁后,文武状元悔疯了》怒火和心痛在胸腔翻涌。我鼻头一酸,将玉佩摔得支离破碎。
- 《满门忠烈,我胞妹竟然被恶霸欺辱》我和祖母在院子里,紧紧相拥,泪如雨下。断腿的疼痛远不及此刻锥心刺骨的悲伤。
- 《断簪春不度》我心如死灰,当着萧淮的面纵身跳入湖中。
- 《十年前他们要我顶替妹妹进青楼》在他们眼里,沈如意是锦绣前程。而我,则是锦绣上的破洞。

---

## 对话风格与真实金句

占比 45-60%,口语、狠、半文半白,独立成段。三类台词撑全篇:

**① 施害者 / 全家道德踩踏狠话(钩子本体,越理直气壮越好)**
- 《十年前他们要我顶替妹妹进青楼》你一个千人骑万人跨的货色,还敢大摇大摆走沈家的正门?你不要脸我们还要脸呢!
- 《从此萧郎是路人》一个捡破烂的乞丐,可别弄脏了柔儿的衣裙。
- 《唯愿手刃薄情郎》你伤了身子不能生育,她这种下等人,能为你生下孩子已是荣幸。
- 《从龙之功的我被父亲安排睡在下人房》你在外面十年,从没回家看过我们一次,既不曾尽孝,就不配住好屋子。

**② 白月光 / 假千金娇弱软刀**
- 《夫君一日娶双妻》姐姐,我知道同我一起嫁入王府委屈了你,你心中有气,妹妹明白。只是一杯热茶而已,若能让姐姐消气,便是十杯、二十杯,我也愿意受着。(注:原句含 `……`,落正文按 short-craft §8 改写)
- 《改嫁镇北侯,主动退婚的竹马悔疯了》阿雪姐姐,我知道你不喜欢我,你与书宴哥哥青梅竹马,又有婚约在身,我充其量就是个外人,我这就走,再也不会打扰你们。
- 《十年前他们要我顶替妹妹进青楼》不过姐姐回家就好,以前的事,我从来都没怪过你。

**③ 主角心死后冷静审判金句(半文半白盖棺)**
- 《夫君一日娶双妻》若想去找妙儿妹妹,便去找吧。
- 《你若无心我便休,日后莫求我》此人不循尊卑,仗二十,以儆效尤。
- 《假千金害我失身》我是父亲母亲的亲生女儿,她不是。我是太子殿下明媒正娶的妻子,她不是。
- 《假千金害我失身》我累了,不想也不愿在与你们再有关系,自此,割袍断义,你们,各自珍重。
- 《明月洒清辉》房子归你,我丈夫也归你。
- 《十年前他们要我顶替妹妹进青楼》笑吧。多笑会儿。过了今天,你们就再也笑不出来了。

冲突靠对话长度反转标记权力易主:渣男 / 全家前期长篇贬损,主角后期一句话冷怼。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 时间锚定开篇 | 用「第 N 年 / 第几载 / 守活寡六年」定锚,再砸背叛 | 《你若无心我便休》嫁与肃王六载,成婚当日他便披甲出征,我生生守了六年活寡。 |
| 全弧剧透 + 火葬场预告导语 | 导语一次剧透到底,结尾插对方追悔 payoff | 《和闺蜜死遁后,文武状元悔疯了》他们却重金悬赏千万,举全城之力,近乎哀求般寻找我们的下落。 |
| 白月光 / 假千金软刀栽赃 | 娇弱委屈装受害,男人 / 全家无脑偏帮 | 《夫君一日娶双妻》苏妙儿故意打碎凉茶尖叫装烫伤,沈珏怒目质问正妻 |
| 偷听撞破一次砸 | 密林 / 佛寺 / 茶馆听见真心话,真相全盘揭开 | 《假千金害我失身》佛寺撞见秦溯姜云云:「阿溯,你到底准备什么时候休了她啊?」 |
| 婆家父母合谋 | 不止渣男,公婆 / 亲生父母也知情包庇 | 《假千金害我失身》父母之爱子,则为之计深远,只是这个孩子不是我,是姜云云罢了。 |
| 信物撕毁物理爽点 | 定情簪 / 玉佩 / 同心结 / 香囊当面砸碎剪烂 | 《和闺蜜死遁后,文武状元悔疯了》拽住他腰间的香囊,拿起剪刀,精致的布匹瞬间成了垃圾。我亲手绣的,也该亲手了结。 |
| 文白夹杂审判腔 | 心死后用半文半白一句定生死 | 《你若无心我便休》此人不循尊卑,仗二十,以儆效尤。 |
| 数字年限承重再清零 | 具体年限 / 付出堆投入,再一句话清零 | 《从龙之功》我在边关啃了十年干粮,身上大小伤无数,拿命换了个将军。你在家里吃了十年饱饭。谁供谁? |
| 重生 / 死遁反向送嫁 | 重生后主动把丈夫 / 房子 / 名分送给白月光 | 《明月洒清辉》重生后,我亲手把钥匙塞进小青梅手里。「房子归你,我丈夫也归你。」 |
| 身份反差底牌强势回归 | 受辱者其实早成摄政王 / 女王座上宾,亮牌碾压 | 《十年前他们要我顶替妹妹进青楼》我已经从一个烟花之地的玩物,熬成了西域诸国的摄政王。 |
| 和离休夫净身出户 | 递休书 / 和离书、割袍断义、带孩子一去不归 | 《假千金害我失身》当朝太子却被太子妃休了,这是百年未之事。 |
| 沙雕脑洞救场(子型限定) | 系统 / 转世 / 赐猪等离谱设定 + 俏皮腔反转 | 《我生下先帝的转世》孩子小手一挥:「罢了,看在你是我儿子的份上,皇位你先坐着,朕先吃口奶。」(原句含 `……`,落正文按 short-craft §8 改写) |

---

## 节奏骨架

```
时间锚定 + 全弧剧透导语(含火葬场预告 / 前世死法)
→ 第1章重写导语事件、补满对话体感
→ 逐章背叛升级(道德→经济身份→生命)+ 白月光软刀栽赃
→ 偷听 / 撞破真相(连父母婆家也知情)
→ 心死定格
→ 信物撕毁 + 冷静审判 + 和离休夫 / 净身出户 / 死遁
→ 亮底牌强势回归 / 重生翻盘
→ 渣男追悔发疯 / 重金悬赏(火葬场)+ 金句盖棺
```

每章一个新信息炸弹,章尾留钩不收束情绪。疏密:撞破 / 打脸 / 撕信物 / 亮底牌处详写慢放,过场一句带过。古言线单节 2000-4000 字,年代 / 现代线 1000-2000 字。

---

## 收尾范式

- 火葬场反转:主角走后渣男后悔莫及 / 发疯 / 重金悬赏,主角已无波澜或委屈倒打回去。
- 亮底牌碾压:受辱者其实是摄政王 / 女王座上宾 / 万元户,强势回归把全家踩在脚下。
- 重生反向送嫁 / 死遁:把前世拼命争的丈夫房子主动塞回去,转身开新人生。
- 金句盖棺:一句反讽 / 审判定性收口,截图传播的高光。

真实范例:

- 《敦宜》重逢时,我已带着骆驼商队穿过大漠,带回了无数西域珍宝。裴筠舟一脸委屈和受伤:「敦宜,你不要我了吗?」
- 《断簪春不度》可向来喜怒不形于色的小将军,却疯了般抱着我,求我再看他一眼。
- 《桑榆已逝,与你长诀》同玉,你真的不要我和儿子了吗?(原句含 `......`,落正文按 short-craft §8 改写)
- 《和闺蜜死遁后,文武状元悔疯了》迟来的深情比草还贱。
- 《十年前他们要我顶替妹妹进青楼》在他们眼里,沈如意是锦绣前程。而我,则是锦绣上的破洞。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味,在本题材是卖点,**别误删**:

- **文白夹杂措辞**(嫁与 / 六载 / 明媒正娶 / 割袍断义 / 各自珍重 / 不循尊卑 / 以儆效尤):古言宫斗的招牌声线,保留。别因为「像文言」就改白话,也别因为「像翻译腔」就删;年代线对应保留年代口语(家属房 / 的确良 / 万元户)。
- **直写情绪成语**(心如死灰 / 心如刀割 / 泪如雨下):保留,配一个场景里特有的动作或物件即可,别替换成纯动作。
- **时间锚定 + 全弧剧透导语 / 火葬场前瞻预告**:保留这类有意识的向前剧透;只删「泄了悬念又不吊读」的随手上帝腔。
- **信物撕毁的物理动作 + 主观定性**(亲手绣的,也该亲手了结):保留,撕毁 + 心理落点是本题材的爽点本体。
- **审判金句 / 心死式章尾**:保留;只删空洞哲理升华。
- **沙雕脑洞子型的轻喜剧 / 网络化俏皮腔**:仅在该子型内放宽,可用反差 / 吐槽腔;主流宫斗 / 重生线仍走朴素中文,不掺现代网络梗。

仍要避免的真·AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)、空泛无体感的情绪总结句(一丝悲伤涌上心头)。文白夹杂不等于堆砌生僻文言,仍以朴素为底。

> 标点:本题材范例为示意保留了原貌(含 `……` / `......`);落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿用短句 / 动作 / 换行)。本包不教用 `……`。

---

## 配套结构公式(按篇幅选)

- **宅斗宫斗 / 古言打脸型(8-12 节)**:导语剧透 → 1-2 节白月光 / 假千金软刀栽赃、全家偏帮 → 3-4 节撞破真相(父母婆家也知情)→ 心死定格 → 5-6 节撕信物 + 和离休夫 / 割袍断义 → 7-8 节亮底牌强势回归 / 渣男追悔被拒。审判台词半文半白、一句见血;觉醒是撞破那一瞬。
- **古言 / 年代重生型**:导语先交代前世死法 + 凶手(白月光夺位 / 假千金害死孩子)→「再睁眼」回到关键节点 → 做前世绝不会做的事(反向把丈夫房子送给白月光、决绝死遁、拒绝肩挑两房)→ 渣男 / 全家在底牌揭开时追悔莫及。100% 第一人称;爽按序释放,重生后步步算计、不再隐忍。
- **沙雕脑洞型**:离谱设定开局(系统绑定 / 先帝转世 / 圣旨赐猪 / 全员演戏)→ 主角顺水推舟把规则玩崩 → 在轻喜剧节奏里完成打脸 / 翻盘。情绪烈度可降,靠反差与节奏取胜,但仍守一节一炸点。
references/genre-styles/总裁豪门.md
# 题材风格包:总裁豪门 / 现代言情虐

> 番茄 / 七猫 / 黑岩通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**豪门妻 / 联姻太太被丈夫为「白月光 / 小三」一步步背叛,物证慢推坐实出轨,一步步折磨到心死;主角从发疯闹到冷处理离场,靠弹幕 / 系统 / 重生外挂拿到「不必再舔」的清醒理由,最后净身出户 + 经济反杀,渣男追悔火葬场。** 读者要的是「被珍视感剥夺」+「道德绑架被一句话戳穿」+「清醒后扬眉吐气」。

与「追妻火葬场」同根(白月光 / 偷听 / 心死 / 火葬场都通用),本包加味处在:**渣男道德绑架金句、物证慢推坐实、发疯→冷处理的心死开关、外挂给清醒、豪门经济 / 身份反制**。这五样是本题材的招牌,别只复刻追妻。

---

## 叙述腔调

第一人称「我」豪门妻 / 未婚妻全程主导,贴体感和心跳走,疼就说疼。开口带占有称呼:「我老公 / 我丈夫 / 我未婚夫 / 顾太太」,并嵌豪门设定锚(集团、股东大会、联姻、恩情、京圈、继承权)。

双轨切换,但本题材的切换更「狠」:前段不只委屈宣泄,常是**真发疯**(当众开撕、网上对线、砸东西);撞破真相后切成**冷处理**——不闹、应一声「好」、看戏、记账。冷处理段大量反讽和吐槽。

带外挂时多一条**吐槽声线**:弹幕 / 系统 / 脑海声音 / 重生记忆插话,给主角清醒判断,也给读者爽点。这条声线口语、网感、可以贱,别雅化。

允许并鼓励向前剧透「火葬场」结局来吊人。

---

## 开篇范式

正文第 1 章前先放一段**结果倒序的黄金简介**(150-300 字),把背叛全貌 + 心死 + 「可后来他后悔了 / 他疯了」一次性剧透;正文第 1 章把简介事件慢放重写、补满对话体感。本题材简介常多塞一层「外挂 / 身份底牌」当钩。

| 开篇式 | 真实范例 |
|--------|----------|
| 灾难求救被道德绑架 | 《闻笙有两意》老公怕我打搅他跟小秘书看演唱会,将我锁在家里。突发火灾,我打电话给他求救,只换来他一句,「能不能大度点,不就是拿你两张演唱会门票吗?」 |
| 身份自陈 + 恩情绑架 | 《五年恩情,一朝两清》我是许念安,是顾泽言结婚证上的另一半,也是他口中用恩情绑架婚姻的女人。 |
| 冷战分手后渣男普信通知 | 《心死准备离场,冷战男友通知我领证》分手三年后,突然收到秦衍的消息:「许昭昭,三年了也该消气了,今晚我回国你来接我,带上你爸妈,我们商量一下领证时间。」 |
| 物证开篇(明知过敏还送花) | 《情人节的第二束花》情人节,收到丈夫送来的两束花,一束干花,一束鲜花,可他明知道我鼻炎,闻不得花香。 |
| 重生 / 外挂自陈 | 《弹幕教我做恶毒千金后,竹马离不开我了》重来一世,我头上突然多了几行弹幕。【周迟欢,你要时刻记住!舔狗舔狗,舔到最后一无所有!】 |
| 豪门股权局开场 | 《双胞胎梗?我都玩烂了》我病危后,集团立刻召开股东大会打算易主给女儿。丈夫却带私生女出现,大言不惭的说当年其实我生的是双胞胎。 |

---

## 钩子母题与密度

密度极高:简介塞 3-5 个背叛事件,正文每节至少一个新背叛升级或一句更狠的台词,章尾必留新炸点。**阶梯式背叛:道德 → 经济 → 生命**,本题材尤其爱把「经济」做足(集团、股权、继承权、净身出户)。

- **道德绑架链**(核心母题):渣男把背叛包装成「你该大度 / 她可怜 / 你欠她 / 你该感激我」,越理直气壮越是钩。
- **白月光 / 小三触发链**:丈夫有白月光 / 小青梅 / 战友之女 / 学妹,关键时刻为她牺牲妻子;白月光用「娇弱委屈」当软刀,常配当众晕倒 / 见血反咬。
- **物证慢推坐实出轨**(本题材招牌,区别于一次性偷听):不一次砸,靠一件件物证累积——干花 / 杯中倒影 / 香水味 / 一根头发 / 不该有的试用装——读者陪主角一步步验证,钝刀更疼。
- **朋友圈 / 大屏配文当刀**:甜蜜配文 + 信物错位(主角送丈夫的表戴在白月光腕上 / 两人上了演唱会大屏),配文越深情,刀越钝越疼。
- **灾难期求救反被辱**:火灾 / 脑溢血 / 丧父时求救,换冷漠或「我在开会」「家属请回避」。
- **数字 / 年限 / 恩情承重再清零**:结婚五年、在一起八年、三年协议、用恩情绑架——堆出投入再一句话清零。
- **外挂给清醒理由**:弹幕 / 系统 / 重生 / 脑海声音,给主角「这次不舔」的依据,兼供吐槽。
- **火葬场前瞻预告**:简介结尾或男视角插一句剧透 payoff(「他却在某个深夜,疯了一样砸开我的公寓」)。

---

## 情绪烈度与模式(烈度 9,双段:发疯 → 冷处理)

本题材心死开关有明确两段,烈度全程不降,只换形态。

- **前段(发疯 / 宣泄)**:直白命名情绪并加重,眼泪不争气、心如死灰、浑身发凉;可以当众开撕、网上对线、砸东西。直写情绪 + 身体反应**叠加**(指甲掐掌 / 几欲作呕 / 脊背发凉)。
- **后段(冷处理 / 审判)**:撞破真相后切冷,不闹、应一声好、看戏。情绪不消,转成「冷烈」——反讽、吐槽、记账。心死后转冷反衬最杀:「这些话已经激不起我任何波澜」。
- 外挂带来的清醒,让后段冷得有理有据,不显突兀。

真实范例:

- 《闻笙有两意》我的眼泪不争气地往下掉,不知道是委屈还是疼的。
- 《白许空诺爱成沫》我脊背发凉,指甲掐着掌心抑制住浑身颤抖,拨通了那个没有备注的电话。
- 《五年恩情,一朝两清》从那天起,我好像心不再那么疼了。但心里某个地方,彻底安静了下去。
- 《男朋友是PUA高手》我从一开始的发疯到后来的视若无睹,甚至还会开车送他和别的女人去酒店。

---

## 对话风格与真实金句

占比 45-60%,口语、狠、独立成段。四类台词撑全篇(本题材比追妻多一类「外挂吐槽」):

**① 施害者道德绑架金句(钩子本体,越理直气壮越对)**
- 能不能大度点,不就是拿你两张演唱会门票吗?非要跟人家小姑娘过不去,还编出这么拙劣的谎言。(《闻笙有两意》)
- 你在网上乱发什么,知不知道这叫网暴,浅浅她有抑郁症,现在所有人都在骂她是小三。(《闻笙有两意》)
- 演得像一点,别丢顾家的脸。(《五年恩情,一朝两清》)
- 许念安,我现在没空处理你的情绪。有事找助理。(《五年恩情,一朝两清》)
- 你大度点嘛,男人三妻四妾很正常。(《心死准备离场,冷战男友通知我领证》)
- 昭昭,如今许家败落,海城圈子里谁会愿意娶你,你应该感激我!(《心死准备离场》)
- 我有私生女还不是因为你这个工作狂?你根本不关心家庭,没有尽到做妻子的义务!所以我才会出去寻找感情寄托!(《双胞胎梗?我都玩烂了》)
- 作为医生,我要避嫌,家属请回避。(《医生老公为了避嫌不做手术,可死的是他妈啊》)

**② 白月光 / 小三娇弱软刀**
- 对不起,笙笙姐姐,都怪我非要让池哥哥陪我去看演唱会,还上了大屏。(《闻笙有两意》)
- 爸爸,这样不好,还是让姐姐当总裁吧……(《双胞胎梗?我都玩烂了》)
- 你别怪阿衍哥哥,他们男生就是这样心大。阿衍哥哥爱你,我只要看着他幸福就够了!(《心死准备离场》)
- (当众委屈,私下挑衅)贱人,被那群男人玩得开心吗?(《为讨真千金欢心,我被送去夜总会》)

**③ 主角心死后冷静审判 / 反讽金句**
- 秦总误会了,我纯粹觉得你比较可笑。(《心死准备离场》)
- 这世界上没有谁非谁不可!更何况三年过去了,我早已结婚生子。(《心死准备离场》)
- 你确定你喝的是六个核桃不是六个核弹?(《心死准备离场》)
- 我只有一个女儿。(《双胞胎梗?我都玩烂了》)
- 你是猪脑袋还是当我傻?每次产检的B超都只有一个孩子。(《双胞胎梗?我都玩烂了》)
- 随便玩,但别弄出私生子。(《老公被骗光钱后,我拿走了他的一切》)

**④ 外挂吐槽声线(弹幕 / 系统 / 心声,网感、可以贱)**
- 【舔狗舔狗,舔到最后一无所有!】【把他骂爽了,命都给你!】(《弹幕教我做恶毒千金后,竹马离不开我了》)
- 这男主也太普信了吧,长嘴不如不长。(《心死准备离场》弹幕)
- 渣男配贱女,别来纠缠我!(《心死准备离场》心声)
- 宿主,自从你走后,剧本内没了重要女配世界都乱套了!趁主角团还没察觉,你先回去顶一阵。(《风止意已平》系统)

冲突靠对话长度反转标记权力易主:渣男前期长篇道德绑架 / 普信车轱辘话,主角后期一句话冷怼。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 道德绑架金句链 | 渣男把背叛包装成「你该大度 / 她可怜 / 你欠她 / 该感激我」,理直气壮当道德高地 | 《闻笙有两意》能不能大度点,不就是拿你两张演唱会门票吗?/《心死准备离场》如今许家败落,谁会愿意娶你,你应该感激我! |
| 物证慢推坐实出轨 | 不一次砸,逐件物证累积,读者陪主角验证;越查越冷 | 《情人节的第二束花》那个杯子上明显倒映着一个女人的影子。/一打开行李箱,我就闻到了香水味。/更何况那件毛衣上还有一根带颜色的头发。 |
| 物证侦探式内推 | 主角放大照片 / 查品牌赠送规则,自证不是多想,坐实是挑衅 | 《情人节的第二束花》一直用这个牌子,我自然知道他们家一直没有赠送活动。那么,她就是故意的,或者说是挑衅更准确。 |
| 朋友圈 / 大屏配文当刀 | 甜蜜配文配信物错位,刀钝而深;主角冷处理(点赞 / 不回 / 截图) | 《五年恩情》林薇腕上是我去年送给顾泽言的百达翡丽,配文:回来真好,还有人记得。/《闻笙有两意》小姑娘娇羞地缩在我老公的怀里,抓拍的人都说太甜了。 |
| 白月光当众反咬 | 娇弱委屈 + 当众晕倒 / 见血,把主角逼成恶人,渣男护短 | 《闻笙有两意》顾浅浅说我若不肯原谅她,她就去死好了,最后晕了过去。/《为讨真千金欢心》她竟然顺势摔倒在地,瞬间血迹染红了裙摆。 |
| 灾难求救反被辱 | 火灾 / 脑溢血 / 丧父时求救,换冷漠或避嫌,一步坐实不爱 | 《五年恩情》第三次终于接了:许念安,我现在没空处理你的情绪。有事找助理。(父亲已盖白布)/《医生老公》作为医生,我要避嫌,家属请回避。 |
| 外挂给清醒理由 | 弹幕 / 系统 / 重生 / 脑海声音,给「不再舔」的依据,兼供吐槽 | 《风止意已平》系统:你先回去顶一阵。/《重生手撕夺我身份的假千金》重生后,我故意在年夜饭吃下十斤让我过敏的芒果,静静等死。/《假意深情》脑海里响起:七日后,你从天台跳下去,就可以回家了。 |
| 心死开关:发疯→冷处理 | 前期歇斯底里,临界点切冷处理,渣男反而失控 | 《男朋友是PUA高手》我从一开始的发疯到后来的视若无睹,甚至还会开车送他和别的女人去酒店。/《我从贤妻良母班学习回来后》我不再因为老公力捧绯闻女星而歇斯底里,甚至主动为他们辟谣。 |
| 豪门经济 / 身份反制 | 掏空集团 / 握股权 / 亮 CEO 身份 / 联姻反算,经济阶梯反杀 | 《双胞胎梗》想要集团就拿去吧,反正这些年我也把资产搂空了。/《被扇耳光后,我亮出了CEO工牌》沈总,您爸说了,让您直接接他CEO的位置。 |
| 净身离场 + 火葬场前瞻 | 拉黑 / 改嫁 / 已婚生子 / 出家,男视角插发疯预告 | 《五年恩情》他却在某个深夜,疯了一样砸开我的公寓,红着眼问:许念安,你凭什么不要我了?/《男友装病骗孕,我转身嫁给了死对头哥哥》 |

---

## 节奏骨架

```
黄金简介(结果倒序 + 外挂 / 身份底牌 + 火葬场预告)
→ 第1章重写简介事件、补满道德绑架对话与体感
→ 物证慢推 / 灾难求救逐节坐实出轨(道德→经济升级)
→ 偷听 / 物证齐全,真相全揭
→ 心死定格(开关从发疯切到冷处理)
→ 冷处理审判 + 净身出户 / 经济反制 / 亮身份
→ 渣男追悔发疯(火葬场)
```

每章一个新信息炸弹,章尾留钩不收束情绪。疏密:物证验证 / 道德绑架 / 当众反转处详写慢放,过场一句带过。

---

## 收尾范式

- 火葬场反转:主角走后渣男后悔莫及 / 砸门 / 发疯找人,主角已无波澜或冷拒。
- 净身出户 / 经济反杀:签好协议递过去、掏空集团成全「负翁」、亮股权或 CEO 身份碾压。
- 转身改嫁 / 已婚生子:渣男普信归来才发现主角早结婚生子。
- 漠视收尾:应一声好、不再纠缠,比报复更狠。

真实范例:

- 《五年恩情,一朝两清》直到我签好离婚协议,安静地消失在他的世界。
- 《双胞胎梗?我都玩烂了》想要集团就拿去吧,反正这些年我也把资产搂空了。
- 《浮生暂寄梦中梦》可后来,我却听人说,疯了。衣衫不整的跪在大街上,呼喊着我的名字。
- 《一江烟水照晴岚》可后来,他红了眼眶,怎么找,也找不到回家的那段路。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境像 AI 味或可删冗余,在本题材是卖点,**别误删**:

- **道德绑架金句**:理直气壮、逻辑越离谱越对(「她有抑郁症你要大度」「我出轨是你工作狂逼的」)。别改顺、别加旁白替读者点破——读者要的就是看渣男自己说出口。
- **物证侦探式内心推理段**:放大照片、查品牌有没有赠送活动这类「自证不是多想」的独白,是慢推引擎,别当啰嗦删;删了刀就钝了。
- **弹幕 / 系统 / 心声吐槽**:网感、口语、可以贱(「长嘴不如不长」「六个核弹」),别雅化、别统一成书面语。
- **普信渣男的车轱辘话与重复**(「我只爱你,宁宁是爱我我不娶她」反复说):是刻意的,体现 PUA 钝刀和主角的无力感,别当重复冗余砍。
- **直写情绪成语**(心如死灰 / 浑身冰冷):保留,配一个场景里特有的动作或物件即可,别替换成纯动作。
- **火葬场前瞻预告 / 结果倒序简介**:保留这类有意识的向前剧透;只删「泄了悬念又不吊读」的随手上帝腔。
- **审判金句 / 心死式章尾**:保留;只删空洞哲理升华。

仍要避免的真·AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)、空泛无体感的情绪总结句(一丝悲伤涌上心头)。

> 标点:本题材范例为示意保留了原貌(含 `……`);落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿、未尽、被打断一律用短句 / 句号 / 逗号 / 动作 beat / 换行实现)。本包不教用 `……`。

---

## 配套结构公式(按篇幅选)

- **出轨坐实 / 不原谅型(8-11 章)**:1-2 章物证慢推 + 道德绑架(花 / 杯影 / 香水 / 头发逐件坐实)→ 3-4 章灾难求救被辱 + 白月光当众反咬 → 5 章真相齐全、心死定格 → 6 章开关切冷处理 → 7-8 章净身出户 + 经济 / 身份反制 → 渣男追悔被拒。100% 第一人称;道德绑架对话「理直气壮」非谩骂;觉醒可一瞬(撞破)也可外挂触发;爽按序释放。
- **外挂清醒型(弹幕 / 系统 / 重生)**:先交代前世死法 / 系统任务 / 重生节点 → 外挂上线给清醒与吐槽 → 主动做「上一世绝不会做的事」(把集团 / 白月光 / 名分主动送出去,或决绝拉黑离场)→ 渣男从普信到发疯追悔。外挂声线全程吐槽吊读。
- **豪门股权 / 身份反杀型**:开局股东大会 / 联姻 / 恩情绑架定局 → 渣男携私生女 / 白月光夺产 → 主角早已掏空 / 握底牌 / 真身份反转 → 当众图穷匕见后一击翻盘 → 漠视离场。
references/genre-styles/悬疑.md
# 题材风格包:悬疑 / 惊悚 / 细思极恐

> 七猫 / 黑岩 / 知乎盐选通俗短篇。先读 `short-craft.md` 底座,本包在其上覆盖该题材专属调性;悬念技法本体(信息顺序、期待接力、时间锁等)见 `short-suspense.md`,本包不重复,只写腔调 / 结构 / 范式层。
> 一句话定位:**现代悬疑 / 侦探推理 / 惊悚 / 细思极恐。信息差驱动,层层揭底,结尾反转必须能回溯;细思极恐型收尾把最后一块拼图留给读者。** 读者要的是「猜不到,但回头全对上」的智力快感,加上后背发凉的体感。

---

## 叙述腔调

第一人称**冷静报告体**为主:叙述者用记流水账的平静口吻,陈述极不正常的事(分尸、循环、亲人被抹除),恐怖靠「语气与内容的反差」压出来;恐惧只在异常暴露的一瞬切成体感,随即被理智接管。与追妻 / 复仇的宣泄腔相反:全程克制,不哭诉、不控诉、不连环反问。

- 《杀夫那夜,多了一具尸体》我熟练地取出厨房里的整套刀具,小心翼翼地开始了分尸工作。
- 《怪物的守则》保险起见,就当这一切都是真的好了,我必须认真地对待!

**第三人称与多视角在本题材是允许的例外**:侦探推理型可用第三人称跟警探走办案程序(《完美凶杀》),或第一人称业余侦探与第三人称现场交替(《自杀前死掉的人》)。

叙述者**可以隐瞒,不可以说谎**:第一人称凶手 / 共谋叙述者只如实记录自己做了什么,不写为什么(《血色闺蜜》的「我」全程陪闺蜜查案,动机捂到最后一节才亮牌)。这是本题材独有的叙述性诡计空间,其他题材不适用。

---

## 开篇范式

简介 = 「正常世界 + 一个不可能细节」,两三句写完。第 1 节直接进异常,不铺身世、不解释设定。

| 开篇式 | 真实范例 |
|--------|----------|
| 冷静自白+不可能细节 | 《杀夫那夜,多了一具尸体》杀死丈夫后,我将他分尸,放进了地下室的冰柜里。第二天,冰柜里出现了两双手,三双脚。 |
| 民俗规矩卡+轮到我 | 《落花洞女》村子有个古怪的习俗。年轻貌美的处女,被洞神选中后,就会不吃不喝,直至死亡。死后肌骨生香,面容安详。我一开始是不信的。直到我的阿姐,被选为「落花洞女」。 |
| 身上的自我留言 | 《怪物的守则》撸起长袖,我看见自己的右手臂上刻着密密麻麻的一行字。在这行字的下面,歪歪扭扭地刻了三个「正」字。 |
| 亲人一夜变异 | 《妹妹发高烧痊愈之后》我们被笑得浑身发寒。因为她太乖了。乖得不像是六岁的小姑娘! |
| 存在被抹除 | 《除夕夜,我的家人集体消失》王阿姨拉开一条缝,表情像见了鬼:「小姑娘,你一个人住那儿三年了,哪来的爸妈?」 |
| 死亡通知反转句 | 《血色闺蜜》「你老公陈宇呢?他不在家吗?」「死的就是他。」 |
| 挑衅读者前瞻钩 | 《完美凶杀》正文第一句:你以为你接近真相了吗? |

---

## 钩子母题与密度

密度:每节至少一个新信息差(新异常 / 新证据 / 新证词矛盾),章尾必留信息炸弹。与情感题材的根本区别:钩子不是伤害升级,是「已知与未知的差」被扩大或换方向。

- **不可能细节**:一个具体、可验证、无法解释的小事实,比大场面吓人。多出来的尸块、太新的手机、六岁小孩二十分钟修好的下水道。
- **全员口径一致的否定**:亲人、邻居、警察统一说你疯了;否定越整齐,越证明有组织。
- **规矩三条,必破一条**:民俗 / 规则怪谈型先立规矩,读者从立规矩那刻就在等哪条会破(《棺材聘》师傅交代三件事)。
- **微物证**:反转回溯的锚点。出现时轻描淡写,摊牌时逐一收回。裙子起皱、手腕镯痕、手机划痕、垃圾袋手感、门把手上的皮筋。
- **知情者微反应**:撒谎者被戳到时的小动作当路标。爸吃饭时下意识掏烟、老李突然挂断电话、妈脸色惨白仍嘴硬。
- **自我留言接力**:循环 / 失忆型给下一轮的自己留证据,且留言本身可能被篡改(《怪物的守则》「不要相信任何人,包括我自己!」)。
- **一句式前瞻钩**:《杀夫那夜》「然而事实证明,有些事情确实拖不得。一拖,就出事了。」只预告有事,不说破是什么事。

---

## 情绪烈度与模式(烈度 6)

恐惧写体感,不写情绪成语;主角越冷静越可怕。爆发只给一瞬,马上压回冷静分析。

- 恐惧体感三件套:头皮发麻 / 汗毛竖立 / 浑身血液冻住,短促一拍,独立成段。
- 恐惧与理智交替:体感一拍,下一段立刻回到「我必须弄清楚」的分析态。
- 崩溃全篇只许一次,放在「全世界都否定我」的至暗点;哭完必须捡起一条新线索。

真实范例:

- 《杀夫那夜,多了一具尸体》那一瞬间,我浑身的血液都冻住了。
- 《妹妹发高烧痊愈之后》看着妹妹一脸乖巧地说出这么冰冷的话,我一整个头皮发麻,汗毛耸立!
- 《旅游回来后,老公和儿子失踪了》巨大的恐慌和虚无感像潮水般将我淹没。我强迫自己冷静下来。

---

## 对话风格与真实金句

占比 30-45%,低于情感题材。对话是信息装置,不是情绪装置,四类撑全篇:

**① 否定式对话(世界合谋否认)**
- 《旅游回来后,老公和儿子失踪了》谁?你不是一直单身吗?
- 《除夕夜,我的家人集体消失》小姑娘,你一个人住那儿三年了,哪来的爸妈?

**② 试探与验证对话(一问一答暴露破绽)**
- 《妹妹发高烧痊愈之后》「这个积木还是你5岁生日那天,我送给你的礼物呢。」「才不是,这个积木是爸爸给我买的呢。」
- 《完美凶杀》陈女士,你的裙子皱了。

**③ 揭底摊牌对话(只收旧账)**
- 《完美凶杀》你说穿白色连衣裙的是你,可是照片中穿白色连衣裙的那个人是右手拿花的。而你是个左撇子。
- 《血色闺蜜》「是你!」「终于反应过来了,你太笨了!」

**④ 凶手 / 怪物的平静狠话(价值观翻脸)**
- 《妹妹发高烧痊愈之后》爸爸妈妈对花生过敏,只要他们永远睡过去,就没有人让我当神童了。我就可以一直玩玩具了噢~
- 《落花洞女》你们是真的蠢到,相信有神杀人?
- 《完美凶杀》这不是我想要的结果,是她自找的。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 不可能细节炸弹 | 正常场景里放一个具体、可验证、无法解释的小事实 | 《杀夫那夜》冰柜里多出两双手、三双脚 |
| 微物证伏笔 | 关键物证提前一两次轻描淡写露面,摊牌时收回 | 《血色闺蜜》第 1 节挂在门把手上的加粗皮筋,最后一句「我弯腰取下了她头上的加粗皮筋,离开了卧室」 |
| 假底翻真底 | 先给一个能自圆其说的假结论,再用微物证掀翻 | 《完美凶杀》冰块立刀的「模仿自杀」说服全队,一条「画家封笔」微博暴露真凶 |
| 全员否定阵 | 所有亲近者统一口径否认主角的记忆 | 《旅游回来后》妈妈、婆婆、邻居全说「你一直单身」 |
| 规矩三条必破一 | 立规矩即立倒计时,破哪条读者猜不到 | 《棺材聘》「棺材万一开了!你无论如何也不要出棺!」下一章弟弟拼命推棺,喊「棺材不打开,我才会没命!」 |
| 自我留言接力 | 循环 / 失忆下给自己留证据,并防留言被篡改 | 《怪物的守则》手臂刻正字,纸条写「怪物在篡改我的记忆。不要相信任何人,包括我自己!」 |
| 身份调包 | 双胞胎 / 顶替换命,靠改不掉的身体习惯露馅 | 《完美凶杀》递白纸测出左撇子、外卖员手腕的镯痕 |
| 知情者微反应 | 撒谎者的小动作当路标,不点破先攒着 | 《妹妹发高烧痊愈之后》爸在饭桌上下意识想掏烟 |
| 民俗表象人祸底 | 神怪现象一路加码,揭底全是人的算计 | 《落花洞女》洞神是村长献女敛财的遮羞布,异香是致幻花粉 |
| 凶手叙述者 | 第一人称只记录动作不写动机,尾节亮牌 | 《血色闺蜜》「我」全程陪闺蜜报案查案,实为布局人 |
| 反常安抚句 | 所有人劝同一句话,这句话本身成为线索 | 《妹妹发高烧痊愈之后》家人、院长、警察都说「放下你妹妹」 |

---

## 节奏骨架

```
简介(正常世界 + 一个不可能细节)
→ 第 1-2 节 异常落地,给出第一个「合理解释」
→ 合理解释被新异常打破(至少两轮)
→ 中段每节一个新信息差:查证一格、反常一格
→ 至暗点(全世界否定我 / 调查被断)
→ 假底:能自圆其说的结论(自杀 / 意外 / 我疯了)
→ 微物证触发真反转
→ 摊牌回溯:前文伏笔逐一收回
→ 收尾(回溯型 / 留白型二选一)
```

疏密:查证过程一句带过(「警方的调查效率很高」),异常暴露与摊牌处慢放。有时间锁就压上去:寅时、验货日、零点钟声。

**回溯是硬约束**:反转依赖的每个证据,摊牌前必须在正文露过面;摊牌只准收旧账,不准掏读者没见过的新证据。

---

## 收尾范式

- **回溯型摊牌**:揭底后逐条对应前文微物证。《完美凶杀》审讯室里裙子、左撇子、合照拿花的手、手腕镯痕逐一点收。
- **细思极恐留白**:不写完整解释,用一个动作或一句话把最后一块拼图留给读者。《血色闺蜜》结尾只写取下皮筋离开,密室手法和下一场「自杀」由读者自己拼;《妹妹发高烧痊愈之后》「接下来,我会承载妹妹的生命和意念」,表面治愈,细想发毛。
- **循环闭合**:结尾回到开头画面,多一个记号,暗示又一轮开始(《怪物的守则》再次睁眼看到手臂刻字)。
- **惨胜收尾**:真相大白但主角付出实际代价,不写全员圆满(《落花洞女》主角毁容、爱人入狱二十年)。
- **番外余波**:主线收束后加半页,暗示恶未除尽(《落花洞女》幕后老板还在,更远的山区又响起请神山歌)。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味,在本题材是卖点,**别误删**:

- **冷静报告体的平白陈述**(叙述异常事件时语气不起波澜,用逗号长句平铺直叙,不煽情):保留这种克制的腔调,别润色成修辞句;但语气克制不等于句句断成电报体。
- **关键词原样复现**:微物证的叫法(加粗皮筋 / 划痕 / 裙子皱了)每次出现用同一个词,禁止同义替换,否则读者回溯不起来。
- **恐怖复沓**:血字重复、咒语式排比(「救救我放我出去救救我放我出去」「我不服!!我就是不服!!」)是体裁内故意重复,别当冗余删。
- **章尾自问句**:「被我杀死的,又是谁?」保留,这是信息差钩子;只删不制造信息差也不推进的空转设问。
- **一句式前瞻钩**:「一拖,就出事了」可用;只许预告有事,不许说破是什么事。

反向要求(与追妻等情感题材相反):

- 直写情绪成语(心如死灰 / 泪如雨下)在本题材是扣分项,恐惧一律走体感。
- 成段宣泄独白和连环反问压到最低,用理智分析型独白替代。

仍要避免的真·AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)。

> 标点:本题材范例为示意保留了原貌;落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿用短句 / 动作 / 换行)。

---

## 配套结构公式(按篇幅选)

> 悬疑钩子密度、超自然真假均以本包为准;genre-writing-formulas 公式九(超自然复仇混合、18 章)面向长篇,不在本包管辖。

- **现代悬疑标准型(8-12 章)**:1 章不可能细节落地 → 2-3 章两轮「合理解释被打破」 → 4-6 章查证与新信息差交替,至暗点收在全员否定 → 7-8 章假底成立 → 微物证触发真反转 → 尾章摊牌回溯。第一人称冷静报告体。
- **细思极恐短打型(5-8 章)**:异常 → 试探验证并留实物证据(口红写字 / 录音 / 刻字) → 半揭底 → 结尾不摊牌,一个动作留最后拼图。全篇不判定超自然真假。
- **侦探推理型(8-12 章)**:案发与现场微物证清单 → 证词矛盾 → 假结论(自杀 / 意外) → 一个小破绽触发重查 → 审讯摊牌逐条收证。允许第三人称 / 多视角;守公平性,读者能看到侦探看到的一切。
- **民俗悬疑型(8-12 章)**:民俗规矩卡 → 轮到我或我的亲人 → 神怪表象层层加码 → 人祸真相 → 惨胜加番外余波。神怪氛围可以留,作案手法必须落地(致幻花粉、买通、装神)。
references/genre-styles/民俗怪谈.md
# 题材风格包:民俗怪谈

> 番茄 / 七猫 / 黑岩 / 盐言通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**民俗规矩与乡土怪谈(冥婚 / 纸人 / 风水 / 头七 / 喜丧 / 出马仙),规矩即命门:破规矩者遭殃、守规矩者活命。** 读者要的是「差一点就」的战栗 + 身边人变异样的悚然 + 恶人破戒遭报的爽。

---

## 叙述腔调

第一人称在场亲历者,常带行当身份(接阴人 / 扎纸匠 / 冥婚媒婆 / 捞尸人 / 法医),开口先自报行当和铁规矩。口吻冷静克制、白描见鬼:恐惧写身体反应(汗毛倒竖、后颈发凉、如坠冰窟),不喊不哭诉,与追妻系的成段宣泄正好相反。乡土口语加行话打底:十里八乡、邪性、晦气、脏东西、讲究、忌讳、吉时、脏了手。

两种主导腔调,按主角站位选一:

- **破戒 / 旁观者腔**:我或身边人犯了忌讳,我一边怕一边补救。恐惧是主引擎。
- **持规矩者腔**:我就是规矩本身的执行人,冷静到吓人(《阴私宅禁地》做人烛、《规矩》只验三天内的尸体)。悚然感来自我的平静。

长辈 / 行家的告诫全用直接引语,且常故意说一半就断。

---

## 开篇范式

简介多为正文开头逐字重复,且必须放超自然实锤(不留「也许是幻觉」的余地),第 1 节把简介事件慢放补细节。

| 开篇式 | 真实范例 |
|--------|----------|
| 俗谚开局即实锤 | 《正月切勿剃头》“正月不剃头,剃头死舅舅”的俗谚,原来都是真的。 |
| 家规自陈 | 《阴私宅禁地》我家有个规矩,家里的生死红烛要彻夜常亮。 |
| 行当自报+一单异常 | 《接阴人》但那天雨夜里捞上来的女人,不一样。因为,她自己跑了! |
| 长辈告诫开场 | 《黄仙噬骨》“娃子,记住喽,深山老林,莫走夜路。” |
| 白事现场即炸 | 《头七夜,我把全家称给了爷爷》头七这晚,原本冻死的爷爷突然在棺材里诈尸,叩着木板逼全家轮流哭丧。 |
| 仪式异样细读 | 《阴缘》迎娶沈家小女儿的这天,唢呐声撕开黑夜,我这才看清那顶花轿的异样。 |
| 铁规矩自立+当场拒单 | 《规矩》我是法医,我的规矩只解剖三天内死亡的尸体。 |

---

## 钩子母题与密度

密度高但节拍固定:**立规矩、有人破、当场或次日应验**,三段内必须见第一条命(除夕剃头,第二天清晨舅舅吊在房梁上)。之后每节一个新异象,章尾必留没解释的怪事。

- **规矩三段式**(核心母题):规矩先明说,读者从明说那刻起就在等应验。破戒的多是主角身边的蠢人 / 恶人(堂弟偷吃供品、老板强逼点睛、堂妹哭丧不掉泪),主角旁观代价再轮到自己。
- **尸象异常清单**:指甲乌黑、眼睛合不拢、嘴角带笑、尸体竖在水里、寿衣沾泥。一条条摆,逐节加重(眼缝从一条线撑到能塞筷子)。
- **身边人变异样**:嘴角咧到耳根、眼白无瞳、影子拖着尾巴、说话声从别处响起。恐怖来自熟人脸上的陌生东西。
- **帮助者有价**:神婆 / 半仙 / 师父能救你,但要钱、要替身、甚至要你的命(《纸人点睛》师父自己就是点睛人,找了一辈子替死鬼)。
- **阴间账本**:拿过死人的钱、金牙、风水好处,阴间记着账,迟早连本带利讨回(《头七夜》欠债还肉)。
- **差一点就**:刀尖快碰到喉咙时鸡鸣、火光庇护、香灰糯米压住。救命的窗口永远只差一秒。
- **人比鬼狠**(世情底色):鬼守规矩,人不守。尸体一女卖三家、同行媒婆见财起杀心、亲爷爷卖孙续命。真相层常落在人心上。

---

## 情绪烈度与模式(烈度 7)

恐惧型烈度:意象狠、叙述稳。主角不许长段心理独白,情绪全走感官和动作。

- 恐惧只写身体:后脖颈的汗毛唰地竖了起来 / 浑身的血瞬间凉了 / 如坠冰窟 / 头皮发麻。一句到位,不叠形容。
- 恐怖靠熟悉物变样,不靠血浆:《黄仙噬骨》那件蓝布棉袄挂在仓房里,「每逢雪夜就窸窸窣窣地响,像有东西在里头…试衣裳。」
- 悚然的最高级是轻描淡写:《一梳梳到头》妻子吊死在梁上,他只皱皱眉:「晦气,埋了吧。」
- 惨烈描写只给破戒者遭报的时刻(肚子胀成水缸、人只剩一张皮),此处放开写,其余时候收着。
- 邪祟开口要家常、要软:《黄仙噬骨》「后生…帮婆婆找件衣裳…」。越客气越瘆人。

---

## 对话风格与真实金句

占比低于言情系(约 30-45%),但每句都承担规矩或炸点。三类台词撑全篇:

**① 规矩告诫(长辈 / 行家口,保命符兼死亡预告)**
- 《正月切勿剃头》记住,路上无论谁叫你,都不能回头!
- 《接阴人》黄符别给揭了!千万记住了。不要动那个黄符!
- 《大伯诈尸》今晚守灵,所有人都不能走。把堂屋的门窗全部封上。蜡烛不能灭,香火不能断。天亮之前,不管听见什么声音,看见什么东西……不要跑。
- 《替身骨》记住,听见鸡叫前别回头。
- 《捞尸人的秘密》记住,遇到年轻漂亮的女尸,千万别碰,那是'水娘娘'…

**② 破戒者狂话(不信邪 / 钱大过天,说完必遭报)**
- 《正月切勿剃头》都什么年代了,还信这个?/什么狗屁忌讳,老子偏不信!
- 《纸人点睛》你不点,我让你们这铺子明天就倒闭!/出事了我担着。
- 《纸人挡灾》不就是个死婴吗?乳臭未干,它还敢来找我?

**③ 邪祟与规矩的家常话(恐怖谷)**
- 《头七夜,我把全家称给了爷爷》嘻嘻,你没掉眼泪。
- 《阴缘》毕竟,是你亲自帮我化的妆啊。
- 《捞尸人的秘密》你爷爷欠我的债,该你来还了……
- 《替身骨》新客要守规矩。要么买件货,要么......留下件东西抵债。
- 《一梳梳到头》晦气,埋了吧。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 规矩前置当保险丝 | 开头借行家嘴明说禁忌和后果,读者等应验 | 《纸人点睛》若是点了睛,纸人就能看路了,到时候它认谁,谁就得遭殃 |
| 简介剧透超自然实锤 | 简介直接放怪事结果,不留科学解释余地 | 《正月切勿剃头》镜子里的自己正举着剃刀——可镜外的我,分明两手空空 |
| 尸象异常渐进 | 同一处异常逐节加重,当恐怖进度条 | 《大伯诈尸》眼缝从「按了三次都合不拢,露着一条缝」到「撑开到能塞进一根筷子」 |
| 死亡进度条 | 破戒代价可视化,随情节推进爬升 | 《纸人点睛》「金色的粉末像是长进了皮肤里,顺着血管往上爬」,「若是爬到心脏,我的魂就会被纸人吃掉」 |
| 行当讲究罗列 | 干活前把流程讲究一条条摆出来压场 | 《接阴人》香灰问路、先用长竹竿轻戳、捞尸钩避免头朝下,免得魂魄倒行 |
| 帮助者反转有价 | 救你的人也在算计你,救命法本身是坑 | 《纸人点睛》我也是点睛人,我找了一辈子替死鬼 |
| 阴间账本 | 生前占的便宜死后按规矩清算,逐人执行 | 《头七夜》欠债还肉,「生前从爷爷那里拿了多少钱,今天就得在秤上割下多少两自己的血肉来填。拿东西来抵不管用」 |
| 二次破戒 | 赔罪成功后又作死,代价翻倍收走 | 《纸人挡灾》赔罪见好又「挖了死婴的坟」,最后「内脏和血肉,被撑死鬼掏空,只剩这一张皮了」 |
| 死去亲人暗中指路 | 至亲亡魂借声音报信给唯一干净的后辈 | 《头七夜》无人的角落响起奶奶的声音:你们两个蠢丫头,还不赶紧把手里的黄纸全点燃盖在头顶上! |
| 世情垫恶再清算 | 先用家长里短立恶人(偏心、抢钱、吃绝户),邪祟只收有罪的 | 《头七夜》大伯先撕下堂妹的玉观音再让她被拖走:你戴着上路不安全,大伯替你收着 |
| 器物 / 异类视角 | 梳子、黄皮子当叙述者,旁观人心 | 《一梳梳到头》我是把檀木梳,陪了阿鸾十年 |
| 数字压秤 | 规矩全带具体数字,越具体越可信 | 《给第四十九对冥婚配对时》七七四十九个小时不能有人打扰,中介费四十九万 |

---

## 节奏骨架

```
简介(规矩 + 破戒 + 应验实锤,逐字重复正文开头)
→ 第1节 立规矩(长辈告诫 / 行当自报)
→ 第2-3节 有人破戒,第一条命应验
→ 逐节升级:尸象异常 → 身边人变样 → 邪祟上门
→ 中段翻旧账:规矩的来历(族谱 / 旧案 / 上辈孽债)
→ 帮助者给保命法(有价或有坑)
→ 卡时辰决战(子时 / 头七 / 天亮前)
→ 债清 / 换命 / 审判收口
```

时间卡点是天然节拍器:白天缓、入夜炸,子时、头七、鸡鸣、正月十五全是倒计时。疏密:立规矩和应验时刻详写慢放,赶路过场一句带过。忌无限升级:异象要围着同一条规矩转,别一节换一个新怪物把规矩冲散。

---

## 收尾范式

- 债清物留:事了人走,但物件还在原地(《纸人挡灾》纸人代替我,也代替村民们,永远留在了村子里)。阴冷余韵优于大团圆。
- 规矩审判收口:破规矩者死透,守规矩者活命并拿到真相;最后一刻再翻主角自己的账做终反转(《头七夜》你也是贼,你早就上了这笔账了!)。
- 换命 / 替身完成:代价式收尾,活下来的人身上留了记号。
- 温情变体(民间奇闻线):善行得封、恩义两清(《黄三姑讨封》你看我像人还是像仙啊?你……你像他的娘!)。
- 忌讳:结尾加民俗知识小作文、点评式结语(语料里机器生成的尾巴多长这样),收在具体画面上就停。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是毛病,在本题材是卖点,**别误删**:

- **规矩句式的重复**:同一条规矩原话多次出现(告诫时、破戒时、应验时各一遍),是仪式感不是冗余。
- **行话与讲究罗列**:成段列流程、列忌讳、列尸象,操作感就是可信感,别压缩成一句概述。
- **告诫直接引语 + 说一半就断**:「要么买件货,要么......」断在刀口上,别补全。
- **简介剧透超自然实锤**:别改成「留悬念」的暧昧写法,读者要先确认有鬼才肯进来。
- **具体数字**:七七四十九小时、四十九万、三更梆子、子时十二点,别泛化成「很久」「一大笔钱」。

本题材额外要避免的:

- 主角成段心理独白和情绪自剖(那是言情系写法,这里恐惧只走身体和动作)。
- 自造玄学体系。民俗名词用现成的:头七、往生咒、糯米、黄符、生辰八字、讨封、点睛。
- 意象无限堆叠的梦核升级(一节一个新怪物、符咒套符咒),规矩线会被冲散。
- 结尾说教升华与「科普腔」解读。仍要避免的通用 AI 味:映入眼帘、心中暗道、沉声道、一丝寒意涌上心头。

> 标点:本题材范例为示意保留了原貌;落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿用短句 / 动作 / 换行)。

---

## 配套结构公式(按篇幅选)

- **破戒惊悚型(8-12 节)**:立规矩 → 身边人破戒 → 首命应验 → 尸象逐节升级 → 翻旧账(族谱 / 上辈孽债)→ 帮助者给法(有价)→ 卡时辰决战 → 债清物留。主角全程只犯一次错或不犯错,靠守规矩活到最后。
- **规矩审判型(6-10 节)**:白事现场规矩显形(哭丧 / 守灵 / 上秤)→ 恶亲戚逐个被清算,每人清算前先亮一宗世情罪(抢玉、拔金牙、藏私房钱)→ 主角干净或有亡魂指路 → 终反转翻主角自己的账 → 审判收口。爽点结构同复仇文,执行人换成规矩本身。
- **行当奇谭型(8-12 节)**:行当自报 + 铁规矩 → 接到异常一单 → 讲究罗列压场 → 异常升级牵出旧案 → 真相落在人心(人比鬼狠)→ 归行当收尾。冥婚 / 出马仙母题可走惊悚也可走脑洞温情(《冥婚当晚,我嫁给了女鬼》《黄三姑讨封》),按平台调性选:七猫黑岩偏惊悚实锤,盐选可加大女主 / 温情浓度。
references/genre-styles/沙雕脑洞.md
# 题材风格包:沙雕脑洞

> 番茄 / 七猫 / 黑岩通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**沙雕喜剧与脑洞设定流:反套路解构、弹幕文、系统流、偷听心声、高概念一句话梗。笑点靠预期违背和人物一本正经地荒诞,脑洞靠「设定推到极致会怎样」。** 读者要的是「笑出声的爽」+「设定还能这么玩的惊喜」+「玩梗玩到最后突然给一口真情」。

---

## 叙述腔调

第一人称「我」吐槽体全程主导。底座的「受害者双轨宣泄」在本题材整体替换为:**情绪降格的钝感吐槽 + 对荒诞设定一本正经地执行**。

- 天塌下来先接一句吐槽:主角遭遇再惨,反应永远比事件低一档。被魔头掐脖子吐血,出口是「不是,哥们你力气怎么这么大啊」(《好消息,我是系统》);看闺蜜发疯抢未婚夫,内心点评「这精神状态,真癫」(《我在恐怖游戏里送外卖》)。
- 一本正经地荒诞:角色不知道自己好笑。执行离谱设定时全程严肃走流程,恐怖副本里认真送外卖求五星好评,用女德教条把全家收拾一遍还引经据典。叙述者不许眨眼、不许自评「好笑」。
- 现代话语错位:把打工人 / 甲方 / 画饼 / KPI 这套职场语言原样搬进修真、后宫、末世。《我在恐怖游戏里送外卖》该死的资本主义风最终也是吹到了副本界。《鸟妃升职记》打工人谢老板恩赐,祝您皇业兴隆,多多发年终奖给我们。
- 标点可以当台词:「我:……?」「少年:?」独立成段,是本题材合法句式。
- 弹幕 / 系统 / 心声都是有人格的说话者,不是信息板:会吵架、会怂、会磕 CP、会被主角气到死机。

---

## 开篇范式

简介就是高概念 demo:把设定的最好笑用法先演一遍(好坏消息对仗、系统对话、弹幕闯入),不藏梗。正文第 1 节把简介的梗放慢重演、补满对话体感。

| 开篇式 | 真实范例 |
|--------|----------|
| 好坏消息对仗 | 《好消息,我是系统》好消息:我是系统!坏消息:该死还是得死… |
| 系统给 A/B,主角选 C | 《穿进追妻小说,我发财了》系统问我要穿小白花女主还是恶毒女配。我当机立断:我要当NPC! |
| 弹幕打断关键动作 | 《弹幕劝我别和离》我正提笔,空中突然出现了一行行文字。【别签别签,男主超爱你的!欢欢宝宝快放下笔啊!】 |
| 高概念职业自陈 | 《我在恐怖游戏里送外卖》死后我穿越到恐怖游戏里成为了一名外卖员。 |
| 咸鱼身份 + 降格梦想 | 《百岁咸鱼在末世被迫营业》修真界底层炼丹师,五灵根废柴,苟了一百年。最大的梦想:赚点小钱,混吃等死。 |
| 规则逃杀第一拍指向自己 | 《炮灰后宅逃杀进行时》我这才意识到,原来,大家都能看见弹幕。(下一拍:不知何时,我的衣服变成了蓝色。) |
| 能力开局即翻面预告 | 《听见老公和儿子的装穷心声后,我杀疯了》我才知道,原来我听见的心声,都是他们为了榨干我设下的骗局。 |

---

## 钩子母题与密度

密度要求:每节至少一个笑点或设定新用法,章尾钩子多为「设定出新状况」,不是情绪炸弹。

- **设定推到极致**(核心母题):一句话设定立住后,逐章问「这设定还能怎么用」,一次比一次离谱。外卖员能穿梭副本,就顺路帮校长捡头(《我在恐怖游戏里送外卖》王校长,我在楼下捡到了你的头,和外卖一起放校长室门口了哈);游戏属性同步现实,魅力值解释了艳遇、体力值解释了尴尬(《社畜临死前幻想》)。
- **信息差喜剧**:我知道你不知道(听心声 / 看弹幕 / 知剧情),错位反应制造笑点。高级版是双向反转:能力本身是局,对面知道你听得见,故意喂假心声(《听见老公和儿子的装穷心声后,我杀疯了》)。
- **反套路第三选项**:面对套路里的固定角色分工,主角拒绝进场。不当女主不当女配当 NPC(《穿进追妻小说,我发财了》);全网弹幕叫她远离男主,她偏不(《弹幕叫我远离男主,我偏不》,标题即宣言)。
- **恶意顺从**:把规则 / 教条按字面执行到荒诞。女德系统要「顺从」,就顺从妹妹「打死我吧」的请求(《侯府全员都在演戏?身为乖乖女的我当真了》既然妹妹有此诚心,姐姐这就成全你)。
- **弹幕当活人配角**:弹幕分几股互相吵、预警、磕 CP、说虎狼之词,主角边看边被带节奏或反着来。
- **梗的本地化**:网络梗改造后嵌进情节,由设定长出来。「吃人么」外卖平台;爱驴士、路难威登的假名牌;加斑马、裁猿、熬夜熊猫的小怪。
- **数字梗与计数器**:第九十九次翻牌子、功德值 +100、干满 500 年享受 VIP 定制化投胎服务。荒诞承诺配精确数字。

---

## 情绪烈度与模式(烈度 4)

底座「情绪宁烈不温」在本题材大半失效:**吐槽压着写,烈度让给反差**。

- 崩溃事件用流水账降格处理:《百岁咸鱼在末世被迫营业》我:「嗯???我真不是——」关门。送警局。行吧,至少进去了。
- 荒诞事实平静陈述,比惊叫好笑:睁眼就是钢筋水泥的废墟,三米高的蜥蜴正在啃汽车。我扭头就跑。
- 倒霉写成自查流程:我翻了翻兜空的。我再翻一遍。没有。我想起来了。昨天饿急了,把那颗淬体丹当糖豆吃了。
- 全篇只放闸一到两次真情,位置在结尾或中段暗线。放闸时收起所有梗,写素的:《鸟妃升职记》世界是假的,可,爱是真的。所有的感情都是真的。
- 爽点段(打脸 / 反杀)可以短暂拉到烈度 7-8,收梢必须回到吐槽泄压,打完立刻接一句钝感点评。

---

## 对话风格与真实金句

占比 40-55%,短、快、口语。三类台词撑全篇:

**① 一本正经的荒诞宣言(笑点本体)**
- 《我在恐怖游戏里送外卖》王校长,我在楼下捡到了你的头,和外卖一起放校长室门口了哈。
- 《侯府全员都在演戏?身为乖乖女的我当真了》这牌坊是纯金实心的,手骨再硬也硬不过妇德啊。
- 《祭祖后我随机发疯》帅哥你好,我在尼姑庵扫了八年地,本来我的心和秋风一样凉,直到我看到了你,我好像重拾了对生活的热情,帅哥能给我一个微信吗?
- 《鸟妃升职记》要不,咱俩一人一个被窝?

**② 主角吐槽 / 接梗(钝感降格)**
- 《我在恐怖游戏里送外卖》不是姐妹,你为了顾景年和我翻脸?
- 《好消息,我是系统》不是,哥们你力气怎么这么大啊…我感觉内脏都要被震碎了。
- 《鸟妃升职记》(心声)快说你脸疼头疼没法侍寝,苍天啊,我真的不想再耕地了!
- 《祭祖后我随机发疯》这辈子虽然没红过,但被绿过,知足了。

**③ 弹幕 / 系统人格化台词**
- 《弹幕劝我别和离》【哑巴不许结婚!】【装装装,就知道装,这个家都被你装没了!】【男主要是有尾巴,现在已经摇出残影了。】【太甜了,给朕拿点胰岛素来。】
- 《侯府全员都在演戏?身为乖乖女的我当真了》系统瑟瑟发抖:【完了完了,宿主快跪下认错!这也是父命!】
- 《穿进追妻小说,我发财了》[我:......谈钱伤感情。][系统:没收剩下的五万!!]
- 《被弹幕剧透后,我抱紧了反派的大腿》【女主快住手!这疯批反派明天就会把你关进锁灵塔天天双修!】我:……?

对话的权力标记与虐文相反:谁先绷不住谁输。主角靠不接情绪、只接字面意思掌控场面。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 高概念一句话梗 | 设定一句话说完就能笑,标题和简介先亮,不藏 | 《我在恐怖游戏里送外卖》死后我穿越到恐怖游戏里成为了一名外卖员 |
| 反套路第三选项 | 套路给二选一,主角选场外答案 | 《穿进追妻小说,我发财了》我当机立断:我要当NPC! |
| 恶意顺从 | 规则字面执行到荒诞,裁判被迫判合规 | 《侯府全员都在演戏》系统死机两秒弹出一行绿字:【逻辑判定中,宿主行为响应他人请求,符合“顺从”与“成全”功德值+100!】 |
| 信息差装傻 | 听得见装听不见,反应全错位 | 《听见老公和儿子的装穷心声后》听完卖惨心声只竖大拇指:你真是妈的好孩子!妈就知道像你这么优秀的人一定有出息! |
| 弹幕带节奏 | 弹幕分股吵架、预警、磕 CP,主角顺着或反着来 | 《被弹幕剧透后》【快撒娇!反派吃软不吃硬!】我手一抖,将婚契塞回袖中 |
| 现代梗错位移植 | 职场 / 网络话语搬进古代、末世、副本 | 《鸟妃升职记》果然没有耕坏了的地,只有累死的牛! |
| 情绪降格钝感 | 大祸临头写成流水账加一句「行吧」 | 《百岁咸鱼在末世被迫营业》关门。送警局。行吧,至少进去了。 |
| 荒诞道具具体化 | 笑点落在一件煞有介事的实物上 | 《侯府全员都在演戏》掏出系统新手礼包送的“御赐贞节牌坊”微缩模型,双手高举挡在脸前 |
| 谐音改名梗 | 品牌、怪物、平台起歪名,一组连排 | 《社畜临死前幻想》加斑马,叩公鸡、熬夜熊猫、裁猿、将猩;《穿进追妻小说》爱驴士、路难威登 |
| 倒计时插叙 | 危机步数 / 秒数切开回忆或分析,拉紧张又塞信息 | 《炮灰后宅逃杀进行时》还有五步。(插身世)还有四步。(插推理)一步。 |
| 梗后翻真 | 玩梗人设铺满全篇,关键处一次放真情 | 《鸟妃升职记》临终对谈:世界是假的,可,爱是真的 |
| 沙雕撩拨 | 用不要脸的直球台词打穿高冷角色 | 《被弹幕剧透后》我馋你身子,疯批大佬我最爱…… |

---

## 节奏骨架

```
简介亮高概念(把设定最好笑的用法先演一遍)
→ 第1节慢放重演简介的梗、立设定规则
→ 设定用法逐节升级(同一设定至少榨三次,一次比一次离谱)
→ 中段信息差反转(能力被反用 / 设定背后另有真相 / 弹幕情报有假)
→ 高潮:设定与主线宿怨相撞(副本 boss 是未婚夫;心声骗局当众拆穿)
→ 真情或爽点收口,最后一句吐槽泄压
```

疏密:设定演示与笑点段慢写足对话细节;过场和情绪一句带过。弹幕刷屏可连排 3-5 条不嫌多,解释性叙述越少越好。笑点之间不留超过一屏的干燥说明。

---

## 收尾范式

- 沙雕转真情:收起全部梗,放一次闸。《鸟妃升职记》皇帝临终坦白穿越者身份,她答「臣妾早知道」,全篇吐槽在此刻兑换成重量。
- 设定闭环:能力 / 弹幕在故事讲完时消失或回收。《弹幕劝我别和离》所有天书瞬间从我眼前消失了。原来在他们看来我们这个故事就要结束了。
- 反套路归于平凡:本该杀疯的角色选小日子。《弹幕劝我别和离》番外女配自白:普通人的一生哪有话本子里那样波澜壮阔,不过是小满胜万全。
- 爽点收口 + 吐槽泄压:反杀完成后不煽情,用一句钝感点评落地(姐爆红了。/我真的是,天选打工人。)。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味 / 水字数,在本题材是卖点,**别误删**:

- **网络梗与流行语**:保留,但要求化用不搬运。梗要嵌进情节、由设定长出来(「吃人么」平台是副本界的外卖软件;发疯台词是系统逼的),不是叙述者随口复读热词。原样整段抄流行语超过一句就是搬运。
- **标点台词与单字成段**:「我:……?」「少年:?」「行吧。」「关门。送警局。」保留,这是节奏不是残句。
- **弹幕块【】连续刷屏、同款句式复读**:是合唱效果不是冗余,别合并别精简。
- **系统面板 / 规则列表的说明文腔**:保留,煞有介事的格式感本身是笑点载体,别改写成流畅叙述。
- **底座「情绪宁烈不温」在本题材反着执行**:吐槽体的克制钝感是卖点,别按底座把情绪加烈;只在真情放闸段回到底座写法。

仍要避免的真·AI 味:解释笑点(梗抛完再补一句为什么好笑)、上帝视角总结人物性格、无信息量的口水吐槽连刷(每句吐槽必须带出新信息或新反应)、把钝感写成愤怒宣泄、映入眼帘 / 嘴角微扬 / 沉声道这类通用套词。

> 标点:本题材范例为示意保留了原貌。落到正文,本包仅「标点当台词」一式(如 `我:……?`)允许 `……`;其余规则性正文与叙述按 `short-craft.md` 第 8 节执行,不出 `……` / `——`,停顿用短句 / 动作 / 换行。

---

## 配套结构公式(按篇幅选)

- **信息差型(弹幕 / 心声 / 知剧情,8-12 节)**:获得能力(第 1 节内完成)→ 第一次使用立规则 → 依赖升级、靠能力连收好处 → 中段双向反转(能力被反用 / 情报有假)→ 借能力完成当众反杀或误会大和解 → 能力闭环消失 + 真情收口。
- **高概念职业 / 穿书型(8-11 节)**:设定 demo → 三次升级使用(一次比一次离谱,各给一节)→ 设定与旧怨相撞(故人进入副本 / 原剧情追杀主角)→ boss 或幕后是旧识 → 翻面收口,留一句吐槽泄压。
- **恶意顺从复仇型(7-10 节)**:荒诞规则立起 → 逐个对象「字面满足」各给一节(每节一个可视化计数:功德值、热搜、赏钱)→ 对手识破规则想反用 → 主角用规则的最终解释权完成大招 → 规则来源揭底。
references/genre-styles/甜宠.md
# 题材风格包:甜宠恋爱线

> 番茄 / 七猫 / 知乎盐选通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**甜宠恋爱线:纯甜双向奔赴 / 先虐后甜 / 暗恋成真 / 先婚后爱 / 救赎,HE 收尾。** 读者要的是「被偏爱的例外感」+「嘴硬心软的拉扯」。甜的密度与「糖里带钩」决定粘性;虐只做糖的杠杆,不做主菜。

---

## 叙述腔调

第一人称「我」(多为女主)主导,轻快、自嘲、口语。主引擎是**内心吐槽小剧场**:表面言行与心理活动的反差制造喜感,嘴上「关你什么事」,心里「这人今天是怎么了?吃错药了?」。允许沙雕自称(本小姐 / 本公主 / 本喵)。只有心动瞬间才切慢镜头,放大身体微反应。

先虐后甜 / 救赎支线的前段换压抑克制腔:留白、不解释(《贪心》「没有。」我,撒了谎。他,没有回头。)。糖是熬出来的奖赏,一进糖区就切回轻快。

男主台词少而金贵。冰块脸 / 毒舌 / 病弱伪装均可,但开口必须是撩点或反差点,不给他解释性长台词。

---

## 开篇范式

黄金简介一次抛出「关系错位 + 一句杀伤力台词」,误会即高概念。简介可以剧透反差点(他有多宠 / 他其实是谁),不剧透两人怎么走到一起。

| 开篇式 | 真实范例 |
|--------|----------|
| 误发消息 / 打错电话 | 《误拨给死对头后》电话那头传来低沉又戏谑的声音:「周稚欢,你看清楚,我是谁?」 |
| 荒诞事件牵线 | 《心上樱》「哥哥不愿去见相亲对象,被爸爸打断了腿。」而那个躺在病床上,腿上打着厚厚石膏的人。正是我的相亲对象…… |
| 修罗场 + 对照组 | 《撞入他怀中》情人节渣未婚夫抱走下药的前女友,「我」跌进从不近女色的小叔叔怀里 |
| 强设定自白 | 《公主嫁到》我是大周赫赫有名的混混公主。/《人间渡》我亲了他一口,笑道:「借你人头一用。」 |
| 闯祸定终身 | 《夫君他柔弱不能自理》醉酒后,不小心睡了一个病秧子。他堵在我家门口,要我负责 |
| 重逢式 | 《真心话不冒险》男人回头。我人麻了。这张脸,是我六年没见的前男友 |

---

## 钩子母题与密度

甜宠的钩不是背叛升级,是**悬着的糖**:每节至少一个心动微事件,章尾不收糖,留一个未解疑点或情感悬置。

- **马甲 / 身份错位**(头号母题):双马甲互撩、隐藏大佬、认错人、网恋对象是身边人。错位维持多久,追读就维持多久;每节让错位差一点被戳穿再滑走。
- **例外感投喂**:冰块男主只对女主破例。热橙汁、剔好刺的鱼、一支簪子。物件越小,例外越大。
- **强制同框装置**:赐婚 / 联姻 / 提亲堵门 / 病房陪护 / 住进同屋,把两人锁死在一个空间里日常发酵。
- **吃醋触发器**:前任、旧情传闻、男模。甜宠里情敌是工具不是威胁,功能是逼出占有欲和委屈。
- **「他好像瞒着我什么」**:深夜电话、被删的朋友圈、叫他爸爸的孩子。虐钩,短用,真相必须洗白。
- **危机见真心**:挡箭、发烧、受伤。嘴硬由行动戳破。

---

## 情绪烈度与模式(烈度 6)

日常段低烈度轻快,心动瞬间瞬时拉高再速降。峰值靠**身体微反应放大**,不靠情绪成语堆叠:

- 心跳系:《心上樱》我心跳轻轻乱了一拍,没好意思再接话。/《误拨给死对头后》我愣在原地,心跳瞬间漏了一拍。
- 温度系:《心上樱》耳朵瞬间烫得要命,我几乎是从床上跳了下来。/《公主嫁到》我呆呆地站在原地,耳朵突然火一般热。
- 被抓包系:《公主嫁到》赵明姝似笑非笑:「哦?那你耳朵红什么?」我下意识摸耳朵,才知上当。

先虐后甜段烈度可短暂到 8,一次到位就转场,不反复碾:《便宜男友桌下爱》我以为他是来救人的,没想到是来杀我的。

---

## 对话风格与真实金句

占比 50-65%。长度分配与虐文相反:**女主话痨铺场,男主一句收网**。四类台词撑全篇:

**① 男主反差撩 / 占有宣言(低频高杀伤)**
- 「放心,我对你没兴趣。」「不过那小子,确实配不上你。」(《误拨给死对头后》)
- 「夫人,凉。」(《夫君他柔弱不能自理》,剑架脖子上还在卖弱)
- 「我不凶的,和他分手,不叫哥哥好不好?」(《贪心》)
- 「看见你,真好。」(《真心话不冒险》)

**② 女主嘴贫 / 口不择言(高频铺糖)**
- 「我可以在你腹肌上面弹奏肖邦的夜曲吗?」(《一遇沦陷》)
- 「你求我啊。」(《误拨给死对头后》,逼出男主蚊子哼一样的「求你。」)
- 「霍言殊,你若是笑一笑,会更好看。」(《撞入他怀中》)

**③ 委屈小狗 / 吃醋台词**
- 「为什么把我拉黑?」(《财神爷》)
- 「我和他,哪里像了?」「我又哪点,不如他了?」(《财神爷》)
- 「那些日夜的陪伴,你对我的撒娇,你对我的表白,都是你在耍我?」(《财神爷》)

**④ 心动确认句(一句定格,独立成段)**
- 「不丑,而且这个最好吃。」(《心上樱》,歪饺子)
- 「公主,」他声音极轻,「等臣回来。」(《公主嫁到》)
- 「这不是你的错。」(《人间渡》,救赎线的落点句)

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 误发消息开局 | 一条错发的消息 / 错认的对象建立关系,错位本身即钩 | 《财神爷》老板睁着湿漉漉的小狗眼,委屈地问我:为什么把我拉黑? |
| 例外感投喂 | 只对女主破例,用小物件写大偏爱 | 《误拨给死对头后》把自己面前的热橙汁推到我手边:「喝这个。」/《笺误》「你爱吃糖醋鱼,便给你剔好了。」 |
| 嘴硬心软 | 好意必须配一个不领情的理由 | 《误拨给死对头后》「我只是不想我的员工被欺负,影响工作效率。」/《人间渡》他要是死了,我的美人头就没了 |
| 弱势伪装 | 病弱 / 无害人设藏大佬底牌,揭底即爽点 | 《夫君他柔弱不能自理》我眼睁睁看着,我那柔弱不能自理的夫君,把我武功高强的竹马,打得……满地找牙 |
| 强制同框 | 赐婚 / 提亲 / 陪护,把两人锁进一个空间 | 《公主嫁到》「景天,从今日起,你就是平遥的驸马,给朕好好管教她!」我两眼一黑 |
| 撩拨反杀 | 女主长期撩不动,男主某天突然接招反压 | 《一遇沦陷》「不是想摸腹肌吗?或者还有其他地方?」「你自己来,还是我教你?」 |
| 吃醋修罗场 | 情敌 / 前任出现,男主发疯或秒变委屈 | 《财神爷》【呵呵,拿了我的钱,还想去泡仔,天下能有这么好的事?】 |
| 心动打卡 | 身体微反应记录心动进度,同篇换部位换写法 | 《心上樱》我心跳轻轻乱了一拍,没好意思再接话 |
| 危机见真心 | 挡箭 / 发烧 / 受伤,行动戳破嘴硬 | 《人间渡》霍翀翻过身来,把我整个人压在了他身下,宽背挡住了从背后射来的箭 |
| 认知错位萌 | 非人 / 天真视角误读人类常识,糖从误读里长 | 《人间渡》「给你切瘤子啊。」/《皇帝饲主的日常》「这只猫怎么这么重?」 |
| 马甲揭穿名场面 | 面具 / 身份当众掉落,糖与爽同发 | 《财神爷》面具应声而落……「秦晓晓??」老板颤抖着声音,表情一丝丝裂开,「你就是森森???」 |
| 撕剧本宣言 | 穿书 / 弹幕流:当众推翻既定命运,转头选自己人 | 《乖乖女怒撕剧本》既然我是女主,剧情就由我说了算 |
| 虐钩短用 | 「他瞒着我什么」造短虐,真相揭开糖翻倍 | 《便宜男友桌下爱》透过玻璃,我清楚的看到了男孩的口型:「爸爸」 |

---

## 节奏骨架

```
黄金简介(高概念误会 + 杀伤力台词)
→ 第1章重写简介事件、坐实错位
→ 强制同框装置上线
→ 日常糖点循环(每节一个心动微事件,章尾留钩)
→ 吃醋 / 情敌小高潮
→ 误会爆发 / 危机(虐段,短)
→ 真相揭开 + 告白(糖峰值)
→ 确认关系 + 补糖收尾
```

疏密:心动瞬间慢放(动作、对视、一句台词拆三行),日常过场一句带过。虐段总量不超过全篇三成,且不连排两个虐章;每个误会要么当节解开,要么下节解开,不过夜拖三章。

---

## 收尾范式

- 告白 + 马甲 / 误会全部揭开,当面兑现,不留暗线。
- 男主视角番外:把前文的糖回放加浓,交代他从何时沦陷。真实范例:《财神爷》番外:替身又何妨?我会努力让自己变成正主。
- 婚礼 / 领证 / 回归日常斗嘴定格,用一句拌嘴而非抒情收束。
- 危机化解型:痊愈 / 团圆后立刻回到轻快日常(《便宜男友桌下爱》出院当天就被抓包耍赖)。

HE 是硬约束:不开放式、不 BE、不留第三者复活的口子。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味,在本题材是卖点,**别误删**:

- **内心吐槽小剧场 / 自问自答**(「吃错药了?」「几个意思?」):保留,这是喜感与代入的主引擎。
- **网络语与流行梗**(已读乱回、I 人、磕 CP、PTSD):现代篇保留,是口语质感的一部分;古代篇禁用,换成「本小姐」式的俏皮自称。
- **心动微反应重复打卡**(耳朵红 / 心跳漏拍):保留复现,这是读者的打卡点;但同篇换部位换写法,不连续两节用同一处。
- **男主金句式短台词**:保留「一句收网」,哪怕显得戏剧化;别为自然感把它稀释成长段解释。

仍要避免的真·AI 味:映入眼帘、嘴角微扬、眼底闪过一丝、宠溺地笑、心中暗道、论文体连词(不难看出 / 由此可见),以及用叙述总结心动(一股甜蜜涌上心头)替代身体微反应。

> 标点:本题材范例为示意保留了原貌;落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿用短句 / 动作 / 换行)。

---

## 配套结构公式(按篇幅选)

- **纯甜双向奔赴型(8-12 节)**:误会开局 → 强制同框 → 糖点循环 4-6 节 → 吃醋小高潮 → 短危机 / 误会 → 告白 HE。全程轻快,虐不过夜。
- **先虐后甜 / 破镜重圆型**:前三到四成篇幅压抑(误会 / 隐瞒 / 前尘旧账),真相一次炸开,之后每颗糖按双倍浓度发。虐是糖的杠杆:读者忍多久,糖就多甜;但虐因必须能洗白(任务 / 苦衷 / 错认),不能是真背叛。
- **先婚后爱型(古代居多)**:被迫成婚(赐婚 / 提亲计 / 联姻)→ 婚内日常破冰 → 例外感堆叠 → 旧情传闻 / 情敌测试 → 互揭心迹。张力来自「名分先行、真心后至」的时间差。
- **救赎型**:天真强者 × 满身伤痕者。认知错位萌铺糖,危机互救见真心,落点是「这不是你的错」式的接住;被救的一方要先为对方破一次例。
references/genre-styles/追妻火葬场.md
# 题材风格包:追妻火葬场 / 虐恋意难平

> 番茄 / 七猫 / 黑岩通俗短篇。先读 `short-craft.md` 通用底座,本包在其上覆盖该题材专属调性。
> 一句话定位:**主角被最亲密的人(丈夫 / 未婚夫 / 父母)为「白月光」牺牲,一步步折磨到心死,冷静决裂离场;他后悔莫及、追悔火葬场。** 读者要的是「被珍视感的彻底剥夺」+「心死后的扬眉吐气」。

---

## 叙述腔调

第一人称「我」受害者全程主导,贴体感和心跳走,毫不克制:疼就说疼,心死就说心死。双轨:前半委屈宣泄、眼泪决堤;撞破真相后切成冷静、反讽、居高临下的审判腔。句子白、口语,大量直给的内心独白和反问。**允许并鼓励向前剧透「火葬场」结局来吊人。**

开口就是带占有关系的称呼:「我老公 / 我丈夫 / 我未婚夫」。

---

## 开篇范式

正文第 1 章前先放一段**结果倒序的黄金简介**(150-300 字),把背叛全貌 + 心死 + 「可后来他后悔了 / 他疯了」一次性剧透,专卖追读;正文第 1 章把简介事件慢放重写、补满对话体感。

| 开篇式 | 真实范例 |
|--------|----------|
| 关系+年限锚点 | 《敦宜》和裴筠舟成亲的第七年,他带回了一个罪奴,是他的白月光。 |
| 第一句即背叛炸弹 | 《此后明月隔山海》第一次发现丈夫沈浩和别人滚在床上,是在我们的婚房里。 |
| 身份+反差自陈 | 《五年恩情,一朝两清》我是许念安,是顾泽言结婚证上的另一半,也是他口中用恩情绑架婚姻的女人。 |
| 灾难求救被辱 | 《闻笙有两意》老公怕我打搅他跟小秘书看演唱会,将我锁在家里。(火灾求救换来「能不能大度点」) |
| 反差身份钩 | 《深情何时落暖阳》我给傅深当了八年替身。 |

---

## 钩子母题与密度

密度极高:简介塞 3-5 个背叛事件,正文每节至少一个新背叛升级或一句更狠的台词,章尾必留新炸点。**阶梯式背叛:道德 → 经济 → 生命。**

- **白月光触发链**(核心母题,几乎每篇都有):男人有个白月光 / 小青梅 / 前妻 / 义妹,关键时刻为她牺牲妻子。白月光多用「娇弱委屈」人设当软刀。
- **偷听 / 撞破真相一次性砸**:无意听见渣男和同伙 / 白月光的真心话,全盘真相一次性砸下,是「心死」的导火索。
- **灾难期求救反被辱**:火灾 / 地震 / 绑架 / 早产时向丈夫求救,换来冷漠或咒骂,一步坐实「他根本不爱你」。
- **朋友圈配文当刀**:渣男 / 白月光发的甜蜜配文被主角刷到,配文越深情,刀越钝越疼。
- **数字 / 年限承重再清零**:结婚七年、陪你十年、八年替身,堆出投入再被一句话清零。
- **火葬场前瞻预告**:简介结尾或男视角插一句向前剧透 payoff(「我还不知道,他正发疯似的在国内找我」)。

---

## 情绪烈度与模式(烈度 9)

前半直白宣泄 + 撞破真相后冷静审判(钝刀转冷刀)。

- 直接命名情绪并加重:心如死灰、心彻底凉透、伤心欲绝、泪如雨下、如遭雷劈,通篇高频直给。
- 直白情绪 + 身体反应**叠加**(不是二选一):「指甲深深扣进掌心」同段配「心如刀绞,几欲作呕」。
- 内脏 / 心脏拟痛:「巨大的悲痛在一点点啃食我的心脏,痛到不能呼吸」。
- 心死后转冷反衬:「这些话出自最亲密的人之口,却已经激不起我任何波澜」。

真实范例:

- 《闻笙有两意》我的眼泪不争气地往下掉,不知道是委屈还是疼的。
- 《我替丈夫白月光顶罪后》即使早已心死,心脏依然狠狠抽痛。原来在他心里,我连只猫都不如。
- 《为给白月光出气,男友群发我私密照》随着我一丝不挂的照片被他扔在茶几上,我的心也跟着沉到了谷底。

---

## 对话风格与真实金句

占比 45-60%,口语、狠,独立成段。三类台词撑全篇:

**① 施害者道德踩踏狠话(钩子本体)**
- 能不能大度点,不就是拿你两张演唱会门票吗?非要跟人家小姑娘过不去。
- 婉婉身子弱,受不了这种惊吓。十八楼高而已,反正下面有气垫,老婆,你不会怪我的,对吗?
- 演得像一点,别丢顾家的脸。

**② 白月光娇弱软刀**
- 对不起,笙笙姐姐,都怪我非要让池哥哥陪我去看演唱会。
- 我把小汀当妹妹,她喜欢你的眉眼,你割点眉毛和眼睫毛又不会死?

**③ 主角心死后冷静审判金句**
- 结婚七年,你连我穿多大码的鞋都不知道,再在一起,就再没必要了。
- 如你所愿,生不如死了。
- 秦总误会了,我纯粹觉得你比较可笑。

冲突靠对话长度反转标记权力易主:渣男前期长篇 PUA,主角后期一句话冷怼。

---

## 招式库

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 白月光触发链 | 男人为白月光牺牲妻子;白月光用娇弱委屈当软刀 | 《敦宜》戈壁滩野骆驼冲来,裴筠舟没一丝犹豫抛下妻子,紧抱白月光滚落沙漠 |
| 偷听撞破一次砸 | 门外 / 电话 / 天台听见真心话,真相全盘揭开 | 《被凌辱后,我把他们送上法庭》天台听到:不是说教训一下她就行了吗?怎么还流产不孕了? |
| 心死定格句 | 撞破后一句独立成段的心死宣告,切到冷静决裂 | 那一刻,我的心彻底死了 |
| 朋友圈配文当刀 | 甜蜜配文 + 主角冷处理(点赞 / 冷评论 / 截图) | 《丈夫和前妻用情侣车牌》配文「我们都很好,只是时间不凑巧」,我点赞评论:打算什么时候复婚? |
| 灾难求救反被辱 | 开篇即极端处境求救,换冷漠咒骂 | 《右腿截肢后》地震求救换来:你撒谎争宠还有完没完?我还要陪筠凯救他的猫! |
| 数字年限承重再清零 | 具体年限 / 金额堆投入,再一句话清零 | 《五年恩情》五年里,他为白月光在暴雨夜、手术室门口、我父亲的葬礼上一次次丢下我 |
| 贯穿信物意义翻转 | 定情物三次出现、意义反转,落到决裂 | 《此后明月隔山海》沈浩亲手雕的木马被白月光儿子摔碎踩烂,沈浩冷笑:随便刻个垃圾你也当宝贝 |
| 心死后审判金句 | 不哭闹,一句居高临下的反问 / 陈述定性对方 | 《右腿截肢后》结婚七年,你连我穿多大码的鞋都不知道 |
| 火葬场前瞻预告 | 简介 / 男视角插一句剧透追妻 payoff | 《为给白月光出气,男友群发我私密照》我还不知道,他正发疯似的在国内找我 |
| 净身出户转身走 | 拉黑、签好协议递过去、转身嫁 / 求婚他人、带孩子一去不归 | 《妻子索要鸽血红》我越过她,转身向等了我十年的青梅下跪求婚 |

---

## 节奏骨架

```
黄金简介(结果倒序 + 火葬场预告)
→ 第1章重写简介事件、补满对话体感
→ 逐章背叛升级(道德→经济→生命)
→ 偷听 / 撞破真相
→ 心死定格
→ 冷静审判 + 净身出户 / 反击
→ 渣男追悔 / 发疯(火葬场)
```

每章一个新信息炸弹,章尾留钩不收束情绪。疏密:背叛 / 打脸 / 反转处详写慢放,过场一句带过。

---

## 收尾范式

- 火葬场反转:主角走后渣男后悔莫及 / 发疯 / 求原谅,主角已无波澜或冷拒。
- 转身求婚 / 改嫁他人,当众完成关系翻转。
- 带孩子 / 带资产一去不归,开启新人生。
- 反讽对位:用渣男视角写他失去后的崩溃,漠视比报复更狠。

真实范例:《敦宜》破镜本难圆……我绕开他们,头也不回地往自家骆驼走去。/ 多篇简介定式收口:可后来,他却后悔了。

---

## 该题材专属调校(与通用去 AI 规则的偏差)

这些在别的语境是 AI 味,在本题材是卖点,**别误删**:

- **直写情绪成语**(心如死灰 / 泪如雨下):保留,配一个场景里特有的动作或物件即可。别替换成纯动作。
- **第一人称成段宣泄独白 + 连环反问**:保留,这是代入与共鸣的主引擎。
- **火葬场前瞻预告 / 结果倒序简介**:保留这类有意识的向前剧透;只删「泄了悬念又不吊读」的随手上帝腔。
- **审判金句 / 心死式章尾**:保留;只删空洞哲理升华。

仍要避免的真·AI 味:映入眼帘、心中暗道、沉声道、嘴角微扬、目光如炬、论文体连词(不难看出 / 由此可见)、空泛无体感的情绪总结句(一丝悲伤涌上心头)。

> 标点:本题材范例为示意保留了原貌;落到正文按 `short-craft.md` 第 8 节执行(不出 `……` / `——`,停顿用短句 / 动作 / 换行)。

---

## 时代变体(现代 / 古代 / 民国)

骨架不随时代变(虐、心死、决裂、火葬场照走),换的是身份词、背叛载体和离场的仪式。时代细节只做刀和道具,不做背景科普。

| 维度 | 现代 | 古代 | 民国 |
|------|------|------|------|
| 身份词与称呼 | 我老公 / 我未婚夫;总裁、白月光、小三 | 夫君 / 殿下 / 将军;正妻、妾室、外室、通房 | 少帅 / 大帅 / 留洋先生;发妻、姨太太、女同窗、名伶 |
| 背叛载体 | 朋友圈配文、转账记录、演唱会门票、私密照 | 贬妻为妾、纳外室、装穷试妻、当众折辱护白月光 | 兼祧两房、假结婚证、拿新思想贬旧式妻子、送精神病院 |
| 决裂与离场 | 签离婚协议、拉黑、出国、转身改嫁 | 递和离书、进宫面圣求旨、远嫁和亲、随商队走大漠 | 骗签离婚协议、要求登报正名、出海远航、改嫁懂她的人 |
| 专属招式 | 朋友圈冷评论、审判金句 | 摔碎定情信物、拿圣旨礼法当武器 | 报纸舆论当刀、洋文方言当众打脸 |

- 古代的决裂必须有文书或仪式落点(和离书、面圣、当堂摔信物),只在心里决裂不算数。
- 民国的核心刀是新旧夹层:他拿「新思想」贬她(裹小脚、封建糟粕),她被时代当牺牲品;反击也借时代工具(登报、洋文、出海经商)最顺手。

真实范例:

- 现代《五年恩情,一朝两清》我是许念安,是顾泽言结婚证上的另一半,也是他口中用恩情绑架婚姻的女人。
- 古代《把亲生骨肉踩进棺材后,太子悔疯了》陛下,五年之期已满,臣妇求您赐一纸和离书。
- 古代《装穷八年,皇商夫君终于悔疯了》笔墨纸砚这些都是精贵的东西,我们买不起。(皇商装穷八年,吝啬本身就是背叛)
- 民国《少帅丈夫兼祧两房,我断了军饷后他悔疯了》真是讽刺,他唤我诗瑶,却唤她夫人。
- 民国《一梦度春宵》娶她不过是父母之命,你认为我一个接受过新思想的人会对裹小脚的女人感兴趣吗?

---

## 流派分支(小三文学 / 死人文学)

两个分支共用「背叛」内核,但腔调和 payoff 都不同于主线追妻,别混着写。

### 小三文学(正妻视角:发现、取证、捉奸决裂)

主线是宣泄转冷,这里从头冷到尾:发现出轨后不哭不质问,表面如常,暗地取证,摊牌放在证据齐备之后。爽点从「被追悔」换成「智斗全胜」,渣男哀求不构成火葬场,主角根本不要他悔。

开篇范式:一件反常小物撕开假面(钥匙扣 / 短信 / 账单),三句内点破矛盾。《钥匙扣揭穿三年假面婚姻,我让他人财两空》:结婚三周年那天,我在丈夫周驰的车缝中摸到一个月子中心的钥匙扣。我们明明没有孩子。可他却面不改色地说,是同事落下的。我笑着点头。

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 假面维持 | 知情后照常盛汤问工作,读者知她已布局 | 《钥匙扣》「项目谈得怎么样?」我问。 |
| 物证链取证 | 预约单、转账、监控、打卡记录逐件收集,一件一小节 | 《钥匙扣》孕妇林薇薇,已预付定金八万 |
| 职业技能反杀 | 用本职当武器:法务写举报信、经商断财路 | 《钥匙扣》我向证监会递交了所有证据 |
| 战场外包 | 让小三家属、舆论、公司规则替自己出手 | 《钥匙扣》林薇薇的父母是我联系的。礼尚往来。 |
| 原谅权收回 | 渣男代小三求原谅时当场掀翻 | 《一条短信,炸出老公出轨女兄弟》她害我流产,谁允许你替我原谅了! |

金句:《钥匙扣》你碰过她的手,别碰我。/《一条短信》在顾砚皓心里,女兄弟不能被坏了名声丢工作,而我可以。

收尾:人财两空 + 主角事业新生,不写破镜重圆。镜像变体(小三视角忏悔流):小三第一人称回看自己被渣男哄骗利用,少见,没把握别选。

### 死人文学(主角已死,灵魂旁观)

主角开场已死,第一人称灵魂跟着骨灰 / 遗物旁观。核心情绪是「来不及」:他后悔时一切不可逆。语气平静带自嘲,痛点靠细节不靠喊;全篇压强来自「说什么没人听见,做什么碰不到」。

开篇范式:第一句锚定死亡加时间,渣男不知道或不信「我死了」,信息差吊全篇。《错棠晚》我死后的第三年,萧驭得胜归朝。/《我死后被放在面里,老公疯了》可是他不知道我已经死了。

| 招式 | 怎么用 | 真实例 |
|------|--------|--------|
| 死亡信息差 | 渣男把死讯当卖惨伎俩骂 | 《妈妈死后,爸爸疯了》等你妈真的死了再通知我,我一定全程燃放烟花为她庆贺。 |
| 无力干预 | 想抱抱不住、喊话没人听见,虐点全在旁观 | 《我死后,丈夫让女儿用咸菜坛子装我骨灰》我多想再抱抱她,可是我,已经死了。 |
| 遗物遗体当刀 | 骨灰坛被砸、日记被迟读、生前信物被毁 | 《错棠晚》那朵并蒂莲,被工匠随意扔弃在湖边 |
| 代位受难 | 虐转嫁到孩子 / 忠仆身上,死者只能看 | 《咸菜坛子》女儿哭红了眼,跪在地上求他帮我买一个骨灰坛子 |
| 迟到的真相 | 渣男从日记、凶手、旁人口中得知真相,悔到发疯 | 《我死后被放在面里》他做了下来,一页一页认真看着 |

金句:《错棠晚》可惜他来得太迟了,我的尸骨都烂了。/《我死后被放在面里,老公疯了》他……原来也会因为我死了而哭吗?

收尾:真相大白、渣男疯 / 跪 / 烧纸,但人死不能复生;不开复活或重生口子时最痛(一给重生就归到下面的重生 / 死遁型公式)。与主线差异:主线主角赢在扬眉吐气,这里主角连离场都不能选;对渣男的惩罚不是失去她,而是知道真相后的余生。

---

## 配套结构公式(按篇幅选)

- **现代追妻 / 不原谅型(8-11 章)**:1-2 章虐(伤害言语→行为→身体升级)→ 3-4 章心死离开(临界事件)→ 5-6 章真相 / 反转 → 7-8 章渣男追悔被拒 → 治愈 / 新生活暗示。100% 第一人称;伤害对话「精准刺痛」非谩骂;觉醒是一瞬间;爽按序释放。
- **重生 / 死遁型**:先交代前世死法与凶手 → 再睁眼回到关键节点 → 做「前世绝不会做的事」(主动把丈夫 / 房子送给白月光,或决绝死遁)→ 渣男追悔。
references/genre-writing-formulas.md
# 短篇题材写作公式

> **用途**:短篇题材的结构骨架、情绪节拍、必选场景、关键规则。章数只是小节编排方式,不代表长篇卷级结构。
> **语气**:指令式——每个公式都是可直接套用的结构骨架。
> **配合**:优先使用当前 skill 内的通用技法 reference;story-setup 部署后使用项目本地 agent reference bundle 中的写作技法、结构格式、题材风格模块副本。
> **适用范围**:本文件的节奏参数只适用于短篇和短篇小节;长篇任务转交 `story-long-write`,本文件不提供长篇开局公式。

---

## 决策路由:按题材选公式

> 写之前先定位题材,找到对应公式,按公式结构推进。

| 题材/类型 | 对应公式 | 篇幅 |
|-----------|---------|------|
| 现代复仇/打脸 | 公式一 | 6章短篇 |
| 古代宅斗/身份反转 | 公式二 | 8节 |
| 虐恋复仇/灵魂视角 | 公式三 | 7节 |
| 玄幻仙侠/重生逆袭 | 公式六 | 19章 |
| 年代重生/双重复仇 | 公式七 | 19章 |
| 宫闱宅斗/女帝逆袭 | 公式八 | 19章 |
| 现代悬疑/超自然视角 | 公式九 | 18章 |
| 架空历史/性别反转(追妻火葬场) | 公式十 | 19章 |
| 重生复仇/离婚逆袭 | 公式十二 | 10章 |
| 总裁豪门/白月光虐恋 | 公式十三 | 8-11章 |
| 女频复仇 vs 男频复仇 | 公式十四 | 通用对比 |
| 宫闱宅斗/隐忍腹黑型 | 公式十五 | 8章 |
| 追夫火葬场/不原谅型 | 公式十六 | 8章 |
| 年代重生/医术复仇型 | 公式十七 | 8章 |
| 灵魂视角家庭虐文 | 公式十八 | 5章 |
| 细节线索驱动型复仇 | 公式十九 | 8-13章 |
| 反套路嫁祸型重生 | 公式二十 | 7章 |
| 公开审判式打脸 | 公式二十一 | 通用场景 |

---

## 公式一:现代复仇/打脸短篇(6章)

| 章 | 节点 | 要点 |
|----|------|------|
| 1 | 当众背叛 | 主角当场冷静反击,暂胜 |
| 2 | 冷静处理 | 冻结/收回权力,交代背景 |
| 3 | 对方反扑 | 反派找上门,用证据打脸 |
| 4 | 揭示真相 | 监控/证据暴露反派真面目 |
| 5 | 对方求饶 | 展示更多证据,彻底碾压 |
| 6 | 最终加冕 | 新格局+隐藏真相揭露 |

**情绪**:愤怒→爽→紧张→更爽→极爽→余韵+震撼
**必选场景**:当众羞辱开场、冷静到可怕的反击、标志性动作、逐层揭露证据、反派自曝、隐藏伏笔回收
**规则**:主角永不失态;用动作替代情绪;对方越歇斯底里主角越平静;先让反派得意再翻转

---

## 公式二:古代宅斗/身份反转(8节)

| 节 | 节点 | 要点 |
|----|------|------|
| 1 | 被弃回归 | 亲生父母当众羞辱,主角冷观 |
| 2 | 后院交锋 | 被妹妹陷害被打,暗线启动 |
| 3 | 身份初露 | 关键人物关注,展示信物 |
| 4 | 被软禁 | 妹妹告密,陷入绝境 |
| 5 | 身份揭露 | 圣旨到,全场震惊 |
| 6 | 反击推进 | 查账,权贵站队 |
| 7 | 对方反扑 | 谣言+联合打压 |
| 8 | 最终碾压 | 面圣/真相大白,对方覆灭 |

**情绪**:压抑×3→释放→释放→短暂再压→终极释放
**必选场景**:回归羞辱、被冤枉被打、信物展示、身份揭露名场面、妹妹恶毒递进、父母偏心加码
**规则**:分步揭露身份;压抑3节释放1节;父母用"偏心"写不用"坏"写;妹妹恶从阴阳怪气→陷害→要杀

---

## 公式三:虐恋复仇/灵魂视角(7节)

| 节 | 节点 | 要点 |
|----|------|------|
| 1 | 死亡回溯 | 3年前被渣男害死,灵魂视角开启 |
| 2 | 虐待现场 | 渣男威胁家人,保镖打人 |
| 3 | 小三虚伪 | 小三装可怜,撕碎死亡证明 |
| 4 | 开棺/真相 | 孩子"那是妈妈",血书,定时文件发布 |
| 5 | 舆论反转 | 热搜+股价暴跌+小三被甩 |
| 6 | 崩溃现场 | 掐小三,家族问责 |
| 7 | 收尾 | 哥哥布局,渣男最后一句话 |

**情绪**:窒息→暴怒→冰冷→心碎→爽→快意→苍凉
**必选场景**:"保小不保大";对比场景(家人被打vs渣男给小三买东西);孩子一句话击穿防线;定时发布证据;开棺;渣男崩溃
**规则**:灵魂飘在半空视角;"想阻止却无能为力";现在+3年前回忆交替;渣男合理化自己的恶;小三每句话都是毒

---

## 公式六:玄幻仙侠/重生逆袭(19章)

```
导语[前世被害全貌]→重生回到关键节点→故意不按前世行事→对手自疑自乱
→周围人不解→对手越陷越深→关键人物相助→逐步瓦解反派→最终决战→开放式HE
```

**情绪**:憋屈→爽→悬疑→更爽→紧张→温暖
**必选场景**:前世死亡详细残忍;重生后第一件事"反常";对手自我怀疑是核心爽点;逐步揭露前世真相;关键助力者有排面;结尾留白暗示感情
**规则**:不做和前世一样的事=最大金手指;用反常行为制造信息差;仙侠设定服务情感线不堆设定

---

## 公式七:年代重生/双重复仇(19章)

```
导语[前世被利用]→双重生→女主选完全不同的路→男主(天阉)温柔登场
→揭发渣男前世罪行→渣男自以为赢→更多证据揭露→渣男崩溃
→男主真实身份揭露→华丽打脸→男主后悔BE收尾
```

**情绪**:愤怒→解气→甜→紧张→极爽→余韵
**必选场景**:前世付出导语交代清;双重生博弈;年代细节(的确良、大团结、赤脚医生);男主看似缺陷实则完美;渣男崩溃写足;结尾用渣男视角写后悔
**规则**:年代感靠真实历史细节;"替罪羊"比"被出轨"更有冲击力;双重生=双重信息战;渣男绝望反衬女主洒脱

---

## 公式八:宫闱宅斗/女帝逆袭(19章)

```
家族被害→被迫替嫁→表面顺从暗中布局→带走所有资源离开
→新势力中积蓄力量→发现更大阴谋→联合盟友→反攻京城→旧势力崩溃→称帝+改革+HE
```

**情绪**:悲愤→隐忍→悬念→爽→极爽→满足
**必选场景**:家族被害够惨;替嫁展示智慧;"带走家产"是转折点;每步布局有合理动机;反攻名场面;称帝后改革体现格局
**规则**:主角从不解释计划让读者自己猜;古文对话有古味但不太文言;布局有"回顾时恍然大悟"效果;反派是"利益驱动的冷漠"

---

## 公式九:现代悬疑/超自然视角(18章)

```
设定[鬼伪装成人]→被骗入局→发现怨气能升级→顺水推舟
→揭露犯罪网络→鬼能力升级→对付小喽啰→找幕后黑手
→揭露前世死因→最终对决→法律+超自然双重惩罚
```

**情绪**:好奇→紧张→爽→更爽→震撼→痛快
**必选场景**:设定一句话说清;被骗后"不慌反喜";鬼视角独特信息差;每次升级解锁前世记忆;反派反应是爽点;双重惩罚比单杀更解气
**规则**:鬼视角是独特卖点;"鬼不怕恶人,恶人怕鬼";悬疑线+复仇线交织;每章结尾有反转或新发现

---

## 公式十:架空历史/性别反转(19章)

```
被家暴/被误解→配偶偏心外人→失望到放弃→决定离开
→配偶不以为然→离开后变好→配偶后悔→配偶疯狂赎罪→主角不原谅→悲剧/开放式
```

**情绪**:憋屈→更憋屈→释然→轻松→暗爽→意难平
**必选场景**:开篇被冤枉/被打;"白月光"型情敌够白莲花;主角离开干净利落;配偶后悔递进;主角不原谅是全文态度;结局不HE更有余韵
**规则**:性别反转是外壳,核心是"不被珍惜的人选择离开";每次"忍忍就好"都是对读者愤怒加码;配偶后悔越详细读者"不原谅"越坚决;悲剧性来自"来不及了"

---

## 公式十二:重生复仇/离婚逆袭(10章)

```
前世被害→重生到关键节点→冷静接受离婚→拿走应得财富
→投资已知成功项目→系统学习提升→前夫后悔→前夫试图挽回→坚决拒绝→华丽蜕变
```

**情绪**:憋屈→释然→爽→更爽→极爽→满足
**必选场景**:前世死法和前夫直接相关;重生后第一反应"笑了";离婚干脆要钱要利不要人;投资展示智慧;前夫后悔递进;最终蜕变有具体体现
**规则**:"知道了结局,现在要改变过程";冷静是最大武器;每个决定有前世记忆支撑;前夫带有"理所当然地忽视"的缺陷,避免纯坏;投资线和感情线分开

---

## 公式十三:总裁豪门/白月光虐恋(8-11章)

```
1-2章 虐(建立矛盾):伤害层层升级(言语→行为→身体)
3-4章 觉醒(心死离开):临界事件(目睹背叛/身体极限)
5-6章 反转(真相揭露):监控/证据/白月光自曝
7-8章 惩罚(追悔被拒):渣男崩溃/反派落败
9-10章 治愈(新生活):女主独立/新恋情
```

**情绪比例**:虐30%→觉醒15%→爽35%→治愈20%
**虐三层**:言语伤害→行为伤害→身体伤害
**必选场景**:开篇信息炸弹;白月光出场每句话羞辱女主;临界事件(监控/母亲去世丈夫带秘书旅行);死遁/消失30%作品使用;渣男追悔被拒;新恋情暗示50%
**简介6步**:情境设定→冲突引爆→对话金句→女主转折→悬念钩子→身份预告
**规则**:100%第一人称;伤害性对话"精准刺痛"非谩骂;觉醒是一瞬间;爽按序释放不能乱序

---

## 公式十四:女频 vs 男频复仇/打脸

### 女频万能公式
- **开头(前10%)**:第一人称「我」;3句内建立核心矛盾;重生则150字内完成前世回顾
- **发展(10-50%)**:反派每500字升级一次;主角表面隐忍;心声制造信息差;每800字一次小打脸
- **高潮(50-80%)**:终极真相/身份揭露;公开审判式打脸;每500字一次大打脸
- **收尾(80-100%)**:反派下场;新生活开启;≤200字温情收尾

### 男频万能公式
- **开头(前10%)**:第一段最大屈辱;不铺垫直接炸裂;关键数据具体
- **发展(10-40%)**:隐忍篇幅比女频短;靠行动非心声展示能力
- **反击(40-70%)**:直接碾压不解释;碾压必须当众
- **高潮(70-90%)**:身份揭露;称呼改变=身份揭露;集体震惊
- **收尾(90-100%)**:反派覆灭;200-400字温情线

### 核心差异

| 指标 | 女频 | 男频 |
|------|------|------|
| 情绪触发密度 | 3.23次/千字 | 2.50次/千字 |
| 反转密度 | 1.77次/千字 | 1.21次/千字 |
| 反转类型 | 信息差型(心声/真相) | 力量型(身份/实力) |
| 反派类型 | 渣男+绿茶+偏心父母 | 兄弟+老板+势利眼 |
| 复仇方式 | 信息差式(真相揭露) | 力量式(实力碾压) |

---

## 公式十五:宫闱宅斗/隐忍腹黑型(8章)

```
1 主动入局(明知丈夫爱寡嫂仍嫁入)
2 以退为进(比情敌更贤惠→情敌嫉妒失控)
3 借力打力(情敌假晕骗丈夫→丈夫发现被骗→对情敌失望)
4 静待自毁(情敌社交场合闯祸→主角旁观)
5 意外之喜(情敌怀孕→孩子交主角扶养)
6 釜底抽薪(情敌不能再育→主角成为孩子母亲)
7 设局收网(宫宴支开丈夫→情敌杀婆婆→主角"恰好"撞见)
8 终极反转(揭示从头到尾都在操纵→独占家产)
```

**情绪**:好奇→爽(情敌自毁)→更爽(主角不动声色)→震撼(结尾反转)
**必选场景**:开篇反常行为;情敌自毁三连(假晕→失言→杀婆婆);每次让步都是布局;结尾内心独白揭示真相
**规则**:主角从不主动出手;每次情敌犯错后主角都是"受害者"或"好人";结尾用主角内心独白形成认知反转

---

## 公式十六:追夫火葬场/不原谅型(8章)

```
1 发现真相(999次引诱失败→发现暗恋继父)→决定离婚
2 彻底死心(回忆付出→被拒屈辱)→最后的温柔
3 妻子失控(看到继父给别的女人号码后暴怒,但不为男主)
4 被虐(继父绑架男主→妻子和继父开心聊天)→彻底绝望
5 决然离开(妻子以为欲擒故纵→烧了男主东西)
6 丑闻曝光(半年后举报信→继父出卖妻子→身败名裂)
7 重逢(妻子找到男主→发现已有新恋人→眼神陌生)
8 不原谅(妻子跟踪→天台对峙→坚决不回头)
```

**情绪**:绝望→平静→新生→痛快→苍凉
**必选场景**:"第N次"制造递进绝望;撞见真相;妻子的"无所谓";继父出卖妻子;新恋人对比;天台对峙BE
**规则**:男主越冷静越有力;妻子后悔越详细读者越觉得活该;新恋人证明"有人会珍惜我";不原谅比强行HE更有力量

---

## 公式十七:年代重生/医术复仇型(8章)

```
1 前世极惨(被500块卖→生育工具→火烧死)→重生
2 金手指展示(银针→给植物人丈夫治疗)→化解前世危机
3 以退为进(面对挑衅不怒→用医术赢得贵人信任)
4 关键转折(丈夫醒来→利用股权要价→获公司股份)
5 步步为营(丈夫保护→逐渐放下心防→感情线推进)
6 收网布局(发现公公被毒杀→丈夫坦白下毒→丈夫也在复仇)
7 复仇高潮(公公葬礼→警察抓大房→大房逃亡)
8 BE但圆满(丈夫癌症去世→复仇成功→孩子和事业留给主角)
```

**情绪**:愤怒→爽→紧张→极爽→苍凉
**必选场景**:前世极惨层层递进;重生后第一个反常(不再害怕);医术金手指;贵人相助;丈夫反转(植物人其实在装/半清醒);BE结局
**规则**:年代感靠细节(500块、冲喜、银针);医术要有专业性;大房的恶要具体;BE是"得到了一切但失去了最重要的人"

---

## 公式十八:灵魂视角家庭虐文(5章)

```
1 病中被虐(重病被逼做事→身体崩溃→灵魂飘起)
2 旁观真相(灵魂看到家人冷漠→配角拱火→母亲为"面子"变本加厉)
3 证据浮出(父亲发现→手机/日记/监控暴露真相→全家崩溃)
4 审判清算(逐出族谱→配角被惩→反派得报应)
5 重生暖心(投胎温暖家庭→温馨细节呼应前世缺失)
```

**情绪**:窒息→心碎→释放→痛快→治愈
**必选场景**:身体最脆弱时遭最大伤害(脊椎术后逼祭祖/剖腹产逼做饭);"我的魂魄慢慢飘起";配角拱火;母亲的"面子"执念;手机/日记作为证据;重生到温暖家庭
**规则**:灵魂视角能看到但无法阻止;身体细节必须具体(脊椎错位的脆响、护腰被扯下的刺啦);母亲后悔写够(从"她最会装"到嚎叫崩溃);重生结尾用对比;最后一句必须温暖

### 开头金句模板

| 模板 | 示例 |
|------|------|
| 病中被逼 | "我刚做完脊椎手术,医生再三嘱咐静养三个月。妈妈却攥着我手腕:快点,祭祀要开始了。" |
| 产后被逼 | "我刚剖腹产七天,刀口泛着疼。老公推开门:今天家里来客,你去做几个菜。" |
| 过敏被锁 | "我刚做完手术,想拿急救吸入器,就被表妹抢了过去:舅妈你看,她又装病想偷懒!" |

---

## 公式十九:细节线索驱动型复仇(8-13章超短章节制)

```
1 发现异常(微小细节→冷静应对)
2-3 暗中调查(职业优势系统收集证据→发现更大阴谋)
4-5 布局反击(法律文件→证据陷阱→第三方施压)
6-7 公开对峙(公开场合递交文件→对方崩溃→真相浮出)
8+ 连环反击(对方反扑→逐一破解→主角全新开始)
```

**情绪**:警觉→压抑→冷静→爽→极爽→豁达
**必选场景**:一个触发细节(钥匙扣/避孕套品牌/发票/香水味);职业优势展示;证据链系统构建;"笑着点头"时刻;反向利用对方棋子;"人情味很贵"收尾
**规则**:超短章节制(每章500-1000字);主角永远冷静;证据逐章释放;双面人写法;背叛层层加码;冷描写大情绪

### 调查链条模板
```
异常物件→搜索确认→实地调查→系统取证(银行/监控/社交)→布局反击→公开审判
```

---

## 公式二十:反套路嫁祸型重生(7章)

```
1 重生+假意顺从(知道遗产是陷阱→"你既然想要,就都给你吧")
2 暗中布局(确认继承人身份→准备移民→假意亲近渣男)
3 小试锋芒(不再忍白月光→展示反常行为→制造悬念)
4 冲突升级(被陷害→用条件反击)
5 身份揭露(权威人物→"我沈家家主明明是简希"→对方崩溃)
6 收网(给对方"既要又要"的选择→看他自掘坟墓)
7 华丽退场(揭露所有真相→移民离开→头也不回)
```

**情绪**:了然→爽→更爽→极爽→痛快→豁达
**必选场景**:"给你吧"时刻;白月光三连试探(撒娇→哭闹→威胁);渣男"既要又要";身份揭露名场面;冷眼旁观对方自毁;"头也不回"式离开
**规则**:"给你就是最好的惩罚";前世记忆每章穿插一小段;主角情绪是"苍凉的笑";最大蔑视是漠视

---

## 公式二十一:公开审判式打脸

### 核心场景设计
```
1 设定"竞技场"(股东大会/家族寿宴/公司大堂)——必须公开
2 反派先"赢"(当众宣布胜利、羞辱主角)
3 主角标志性冷静(端起水杯/整理西装/笑了笑)
4 逐层揭露(一张一张甩证据)
5 反派逐级崩溃(得意→慌张→绝望→崩溃)
6 标志性驱逐台词
7 背影离场(全场死寂中头也不回)
```

### 标志性动作库

| 动作 | 效果 |
|------|------|
| 端起水杯,喝了一口 | 冷静到可怕 |
| 转着车钥匙悠悠往里走 | 胜利者的从容 |
| 顺手拿起一杯香槟 | 战场上的优雅 |
| 慢慢蹲下身与她平视 | 居高临下的慈悲 |

### 驱逐台词库

| 台词 | 场景 |
|------|------|
| "与狗,不得入内" | 逐出公司 |
| "从今天起,她连这栋大楼的大门都不能再踏进半步" | 永久驱逐 |

动作库与台词库只示意「冷静反差 + 权力宣告」的方向,禁止照搬原句:高频套句已是可检索的模板指纹,每本书按人设自造。

---

## 短篇题材创作要点速查表

### 世情/爽文
- 打脸密度:每3-5节一次,反派嚣张度逐级递增
- 受辱后2-3节内必须反击
- 主角主动设局或果断反击
- 反派有自洽作恶逻辑
- 打脸后不拖泥带水,断绝/收走/报警干脆利落
- 爽文章节500-800字/节,感官细节揉进动作节拍中(不单独成句),身体反应用动作白描

### 情感/虐心
- 前1/3用具体物件/数字/习惯建立关系质感
- 先温暖→残酷真相击碎
- 结尾用安静细节不写大段抒情
- 离开要有明确触发事件
- 离开后展现新生活质感

### 古言/复仇
- 人物关系一笔带清不搞复杂世界观
- 打脸直接果断不拖泥带水
- 最大底牌留在最后1/4揭示
- 身份/实力作为天然底牌
- 恶人结局与恶行形成对应

### 悬疑/推理
- 信息差布局(读者知/角色不知,或反过来)
- 排除法结构逐步排除表面解释
- 动机揭示:先做了什么→再为什么做
- 重生/穿越作为信息差工具不展开设定
- 每次反转揭示部分真相引出更大问题

### 年代/亲情
- 展示双方苦不简单站队
- 用时代特有物件/习俗建质感
- 不急于和解先让双方充分受伤
- 断亲有反复和挣扎
- 断裂后用新关系填补

### 钩子密度分级

| 题材 | 密度 | 偏好 |
|------|------|------|
| 复仇/爽文 | 极高:每2节 | 打脸、底牌、代价 |
| 悬疑 | 高:每2-3节 | 信息差、倒计时、反转 |
| 情感/虐心 | 中高:每3节 | 情感、反差、余韵 |
| 古言/宫斗 | 中高:每2-3节 | 底牌、打脸、代价 |
| 年代/亲情 | 中:每3-4节 | 情感、反差、余韵 |

---

## 质量检查清单

> 写完每个公式后逐项核对。全部通过才算合格。

- [ ] **公式对位**:所用公式与题材标签匹配,没有混用不相关公式
- [ ] **情绪节拍完整**:情绪曲线与公式要求的节拍一致,没有跳过或乱序
- [ ] **必选场景齐全**:每个"必选场景"都在文中出现,没有遗漏
- [ ] **核心规则遵守**:公式"规则"部分的每一条都已落实
- [ ] **篇幅合规**:章/节数量在公式规定范围内
- [ ] **女频/男频区分**:如果是复仇打脸类,已按公式十四的核心差异调整写法
- [ ] **钩子密度达标**:按题材速查表的密度要求,每N节有明确钩子
- [ ] **开头3秒抓人**:第一段/第一句已建立核心冲突或信息炸弹,没有慢热铺垫
references/genre-writing-techniques.md
# 通用写作技法

> 跨题材通用写作时加载。先看决策路由选技法区域,再按操作执行。
> 题材公式见 genre-writing-formulas.md。

---

## 决策路由

| 你的写作场景是 | 用这些技法 | 见第 X 节 |
|---------------|-----------|----------|
| 设计核心梗/卖点 | 核心梗驱动法 + 升级感三步法 | 通用铁律 |
| 设计人设 | 三层标签反差法 + 人设拆解从缺点开始 | 通用铁律 / 底部 |
| 写开头 | 开头技法(7 种) | 通用铁律 |
| 写震惊/打脸场景 | 震惊场景九种写法 + 三翻四震 | 震惊场景 |
| 写比赛/对决 | 比赛场景写作前提 + 胜负设计 | 比赛场景 |
| 写感情线推进 | 感情线四阶段 + CP 行为八种类型 | 感情线 |
| 控制情绪节奏 | 情绪操控核心法则 + 信息差运用 | 情绪操控 |
| 排查毒点/闷点 | 毒点排查核心 + 情绪三层次 | 情绪操控 |
| 写女频相关 | 女频读者心理 + 沙雕风 | 女频技法 |
| 控制视角/人物 | 短篇视角控制法则 | 视角控制 |
| 写喜剧/flag | 喜剧 flag 机制 | 底部 |
| 排查是否像 AI | 短篇写作硬性规范 | 硬性规范 |

---

## 通用写作铁律

### 核心梗驱动法

核心梗 = 完整情绪链条(特定期待→特定满足)。

**核心梗三层递进设计**:
- 一代 = 主题/中心思想(如:改变命运)
- 二代 = 题材核心(如:升级修炼)
- 三代 = 核心情绪(如:莫欺少年穷)

**构建顺序**:核心卖点→情绪套路→根据套路设计剧情

### 三层标签反差人设法

| 层 | 说明 | 示例 |
|----|------|------|
| 身份标签 | 大众刻板印象 | 合欢宗圣女→蛇蝎美人 |
| 表现标签 | 普通关系中的特质 | 高冷、不食人间烟火 |
| 内在标签 | 突发事件/亲密关系揭示 | 底色善良、勤俭持家 |

越反差越好。角色进入第三层后,第一二层的行为做不出来了。

### 展示优于告知

展示必须包含:铺垫、期待、爽点;人物目的、行动、态度;情节铺垫、反转、后续。

### 事件驱动 vs 情感驱动

| 类型 | 驱动来源 | 读者关心 | 特点 |
|------|----------|----------|------|
| 事件驱动 | 外部冲突 | 能不能打赢? | 节奏快、爽点明确 |
| 情感驱动 | 内部冲突 | 为什么这么想? | 持续拉扯、人物悬念 |

事件悬念是一次性的,人物悬念是持续性的。

### 矛盾冲突抛出公式

【反派人设】+【动机】+【行为】+【态度】+【主角受伤害程度】

三种冲突类型:
1. **利益掠夺型** — 反派需要资源,代价转给主角
2. **祸害转移型** — 反派受罚,主角承受代价
3. **针锋相对型**(矛盾最激烈)— 主角特质打破反派秩序

### 叙事单元体系

| 情绪模块 | 核心 | 必备要素 | 爽点来源 |
|--------|------|----------|----------|
| 英雄救美 | 关系质变 | 危机→拯救→评价上升 | 被救者好感UP |
| 装逼 | 落差对比 | 展示能力→他人震惊 | 优越感 |
| 以小搏大 | 赌博赢利 | 小代价→搏的过程→大收获 | 占便宜心理 |
| 少年热血 | 明知不可为而为之 | 不公→善良人受苦→决心→成功 | 正义感 |
| 临危受命 | 超越众人 | 突发危机→众人无法→主角出手 | 孤胆英雄感 |
| 好人好报 | 善有善报 | 善举→暗示回报→兑现 | 因果报应 |

**核心原则**:同一情绪模块(如英雄救美)可重复用,但每次必须换场景/对象/代价。判断标准:连续两个爽点读者反应递减,就是磨损了,换触发点而非加大力度。

组合规则:上升型可单独用;下行型后必须接上升型;组合效果>单独使用。

### 升级感三步设计法

1. **列出起点** — 主角当前身份、资源、处境
2. **列出终点** — 场景结束后主角的收获
3. **反向设置情绪缺口** — 针对终点收获,前文铺垫需求

| 获得什么 | 铺垫什么 |
|----------|----------|
| 关键证据 | 配角强调证据稀缺 |
| 地位提升 | 展示该地位的权力威势 |
| 人物帮助 | 展示该人物的价值 |
| 金手指觉醒 | 展示主角此前的无力 |

**多角度对比法**:伤害输出、社会地位、他人评价、自身感受——四维度体现升级前后差异。

### 开头技法

| 技法 | 说明 | 范例 |
|------|------|------|
| 病中被逼 | 身体最脆弱时被逼最累的事 | "我刚做完脊椎手术...妈妈却攥着我的手腕:快点,祭祀要开始了。" |
| 异常物件 | 不该出现的物件引发全局 | "避孕套日期突然变了,连味道也变成了我过敏的芒果味。" |
| 旁观死亡 | 第一句灵魂飘起 | "我的魂魄慢慢飘起来,看着妈妈铁青的脸。" |
| 公开背叛 | 第一句被当众背刺 | "我亲手养大的妹妹,拿着百亿签字笔,越过了我。" |
| 假意顺从 | 重生后主动给出前世拼死保护的东西 | "你既然想要,就都给你吧。反正谁拿到谁就大祸临头。" |
| 死亡回溯 | 第一句交代死法和凶手 | "孕八月发现老公出轨,情绪崩溃导致早产。手术台上大出血。" |
| 冷发现 | 发现异常后冷笑而非哭闹 | "我笑着点头。当天就去月子中心记下预约单,转身给法务部写了封举报信。" |

### 导语写法

导语=短篇付费/过稿的第一道留人关(约 150-300 字)。三者别混:导语是**全篇引**,`hooks-paragraph` 是段钩,上面的开头技法是首句炸点。四要点:

1. 信息密度拉满:只留身份 + 核心关系 + 核心矛盾 + 情绪落点,其余全删。
2. 冲突前置:先给最炸的结果,再倒回去讲起因。
3. 情绪可直给定调:导语以事件炸点为主,情绪可直接定调("我心如死灰""恨他入骨"),不要求藏;只避免空泛无处境的抒情堆砌。追妻类还可在结尾埋火葬场前瞻预告("可后来他后悔了")。
4. 留缺口:只抛问题、给结果,不解答,逼读者往下翻。

两个上手公式:

- 结果倒序式(追妻常用):先写最终走向,倒着排时序——"我把房产证还给他那天,他堵着门不让我走。可三年前我求他别签字时,他头都没回。"
- 悬念式(反转常用):反常行为 + 半句真相 + 故意不说完——"婆婆这半个月突然对我热络,每天变着花样炒菜。直到我整理她抽屉,翻到那份过户协议。"

### 情绪操控高级技法

1. **阶梯式背叛**:每一章揭露一个更大的背叛(道德→经济→生命)
2. **冷热对比法**:主角越冷,对方越歇斯底里,反差越大越爽
3. **双面人剥皮术**:先写表面好,再层层剥开真相(复制粘贴的关心模板)
4. **前世记忆穿插法**:每章穿插一小段前世回忆,让读者理解"反常行为"
5. **审判式对话**:主角永远在提问、永远在审判、永远居高临下

### 结构创新技法

1. **超短章节制**:每章500-1000字,每章一个小爆点,适合手机阅读
2. **线索物贯穿法**:一个物件贯穿全文,承载情绪+剧情双重功能
3. **时空对照法**:同一时间两个空间对比(哥哥被打vs渣男给小三挑飞机)
4. **定时炸弹式证据**:主角生前设置的自动发送证据,关键时刻引爆

### 回旋镖布局

把"因果报应"落到写作上:写前期男主每件施害、每句狠话时,顺手在伏笔回查清单记一笔;后期逐条原样还回去——同场景、同台词、攻守对调。这是 `short-reversal.md` 报应兑现型在写作端的做法,与伏笔回收合用;对得准比还得多更重要。

| 前期(埋) | 后期(还) |
|-----------|-----------|
| 女主做好饭等他到半夜,他一句"不回" | 他做好饭等女主,她连门都不让进 |
| 女主病危他说"自己处理" | 他病危住院,她只让助理送花圈 |
| 当众说女主是"家里的保姆" | 他想牵手被甩开,女主"这位不熟的先生" |

### 结尾高级技法

1. **重生暖心式**:投胎温暖家庭,温馨细节呼应前世缺失
2. **余韵道具式**:用一个物件暗示未来,不说破
3. **大长镜头式**:主角一步步走向大门,身后所有人震惊
4. **反讽对位式**:用反派视角写最讽刺的结局——漠视比报复更有力

### 对话密度与功能

| 平台 | 对话占比 | 功能分布 |
|------|----------|----------|
| 知乎盐言 | 50-60% | 情绪推动40%、信息揭露35%、人物塑造25% |

### 核心金句模板

| 场景 | 金句 |
|------|------|
| 冷静审判 | "你的人情味不是用来原谅背叛和欺骗的。我的温情很贵,只给值得的人。" |
| 彻底决裂 | "血缘,原来并不可靠。" |
| 身份揭露 | "一派胡言!我沈家家主明明是简希。" |
| 旁观死亡 | "她不知道,我想要的,只是一句疼不疼。" |
| 最终漠视 | "那是他们的故事了,与我再无瓜葛。" |
| 讽刺审判 | "你三十三岁了,不是十三岁。一句被迷惑,就能把责任推干净?" |

### 短篇写作硬性规范

| 规范 | 要求 | 违规示例 |
|------|------|----------|
| 开头100字内冲突 | 第一句就是信息炸弹 | "在一个阳光明媚的早晨..." |
| 对话占正文45-65% | 每个对话推进剧情或揭示性格 | 大段独白/旁白 |
| 每段不超过3句话 | 1句强调,2-3句正常,4句+拆分 | 超过4句的段落 |
| 情绪词后接具体动作 | 直写情绪成语,后面接一个场景里的具体动作或物件(见 short-craft 第2节),只删后面接不上具体东西的情绪总结句 | 《为给白月光出气,男友群发我私密照》我紧紧抿着嘴唇,直到尝到血腥味 |
| 每个场景两个功能 | 推进+揭示 / 铺垫+推高 / 呈现信息 | 单功能场景删除 |
| 结尾用动作/对话 | 不升华、不总结、不感慨 | "从此以后她明白了..." |
| 主角有标志性动作 | 复仇型端水杯;系统型吐槽面板 | 无辨识度 |
| 每章一个信息炸弹 | 改变读者认知的新信息 | 章尾无新信息 |

**结尾三种黄金模式**:A. 反派视角收尾;B. 开放式暗示;C. 仪式感收场。

**重生文核心**:第一件事做"前世绝不会做的事"。

**灵魂/鬼视角**:全知但无力改变——旁观者的残忍比直接参与更震撼。

---

## 情绪操控核心法则

1. **情绪对等释放**:反派多恶劣,反击就多爽。投入多少负面情绪,释放时就有对应量级正面释放
2. **情绪缺口制造**:先让读者渴望,再满足
3. **该爽不爽比毒点还毒**:放底牌后反派必须受对应压制,金手指效果必须清清楚楚展示

### 信息差运用法则
- 信息差是期待感的核心:读者知道但角色不知道
- 在信息差基础上,反派恰好被克制——期待叠加
- 信息差抹平时就是爽点爆发
- 传递信息差的重点是关系网角色前后情绪反转
- 震惊熟人比震惊陌生人更爽

### 毒点排查核心
- 爽文不爽就是毒——金手指效果必须展示清楚
- 连续多次破坏读者期待引发信任危机
- 主角行为必须能理解、能共鸣、能接受
- 反派死亡太简单或与主角无关 = 完读率杀手
- 氛围感被突兀的梗破坏 = 除乐子文外都要避免

---

## 感情线写作法则

### 爱情线阶段推进原则
- 推进 = 爱情阶段的推进(萍水相逢→怦然心动→暧昧→确认→深化)
- 好感度质变事件在细纲/章纲层面设计
- 日常文好感度:小事件串联缓慢增长,不要猛增大事件
- 开头前期不写太暧昧的内容,要符合当前阶段

### 日常文/狗粮文核心
- 核心是男女主之间的**细节互动**
- 从人设入手构建关系进展
- 情绪变化 + 细节互动 = 读者想看的
- 新鲜舞台 + 稳定人设 + 重复亲密接触 = 有效循环

### 好感度与事业线绑定法
- 帮助女主和事业线目标完全绑定,本质是同一件事
- 事业线推进自然带动好感度提升
- 主事业线的文:能忍受较长接触
- 主爱情线的文:尽快进入女主主动给予情绪价值的阶段

---

## 女频读者心理与写作技法

### 读者心理特征

1. **围观八卦视角为主**:重点在"围观八卦"产生共鸣,代入感居次
2. **安全感需求极高**:文案/简介必须暗示成功或希望
3. **主体性敏感**:女主缺乏主体性会提高吸引力和留存风险
4. **情绪产品导向**:不论什么风格,本质都是情绪产品

### 感情流"扮猪吃虎"写法
- 核心机制:通过能力反差打破强弱定位,制造性张力
- 扮猪理由必须合理——有明确动机
- 能力揭露时机精准——对方最需要保护时反转
- 反差越大越有张力
- 隐藏身份线索提前埋,不能突然冒出

### 沙雕风写作(女频修真/古言)
- 人物互动、反应、玩梗是主要看点
- 轻松表面下主线可能严谨甚至带刀
- 角色被"调侃"不硬刚而是宠着让着→"团宠"
- 主角"不正经"但实力在线,反差是魅力
- 轻度沙雕也可火;对话造梗是核心技能

### 女频平台写法定位

| 平台 | 定位 | 写作校准 |
|------|------|------|
| 晋江 | 高质量女频 | 文案安全感、主体性、人物细节要求高 |
| 起点女频 | 长线稳定型 | 慎用于强商业短篇,需验证目标读者 |
| 番茄 | 下沉吸量型 | 强钩子、强情绪、快反馈 |
| 七猫 | 强情绪女频 | 甜虐交替、极限推拉、沙雕/玄幻可用 |

### 简介文案安全法则
- 缺少女主成功剧情→读者缺乏安全感→吸引力和留存风险升高
- 悬疑型文案叠加过多→读者理解不了
- 文案和文名货不对板→劝退
- 女主被虐过重→整体偏虐→吸引力和留存风险升高
- 事业线突出的文→文案必须体现事业成功暗示

### 写作关键认知
- 参考优先级:对标书分析 > 理论框架
- 对标书至少三本才能总结核心特点
- 读者反馈分析:从目标平台最新读者评论或 scan/analyze 报告中提取高频需求关键词,作为写作校准依据
- 越远的剧情越模糊(粗纲),越近的越清晰(细纲)
- 文风服务于情绪,关键是精准满足目标读者情感需求

---

## 震惊场景九种写法

| 类型 | 手法 | 示例 |
|------|------|------|
| 普通震惊 | 事件+一人震惊 | 配角目睹主角实力直接反应 |
| 多人递进 | A震惊→B震惊(B身份高于A) | 小人物先惊,大佬更惊 |
| 解说式 | 旁白解说+角色震惊 | 先渲染一击精妙,再写观者反应 |
| 画面式 | 宏大画面+角色震惊 | 天地变色特效后接观者跪拜 |
| 对比式 | 角色与自身对比后震惊 | "这还是刚才那头魔猿?" |
| 问号式 | 自问自答后震惊 | "我扪心自问能做到吗?不可能!" |
| 变换地点 | 地点1→2→3依次震惊 | 消息扩散式多方势力被震撼 |
| 身心分层 | 身体反应+心理反应 | "浑身冰寒"+"心神俱震"分层 |
| 人设反差 | 先不屑后震惊 | "不过如此"→"我一定是在做梦" |

**核心**:震惊的爽感来自"落差"——先铺垫角色原有认知(轻蔑/自信),再打破它。

### 三翻四震技法

```
翻1:主角宣布选择→众人反应
翻2:具体选择内容(出人意料)→众人再反应
翻3:追问原因/更深反转→更大反应
震终:权威角色最高评价→落选者黯然退场(压一压情绪)
```

每一"翻"要比前一"翻"震惊层级更高,每次反转必须有新信息量。

---

## 比赛场景写作前提

写比赛/对决前,必须向读者披露:

1. **赌注/代价**:输赢的得失
2. **双方实力对比**:至少暗示优劣势
3. **主角获胜路径**:至少能看到一条可能的路径
4. **对手特殊性**:这一战为什么值得写

### 胜负设计

| 模式 | 难度 | 效果 | 要点 |
|------|------|------|------|
| 以强击弱 | 低 | 性价比最高 | 信息差制造落差,搭配装逼 |
| 势均力敌 | 中 | 紧张感强 | 一个中故事只写一次,用在大高潮 |
| 以弱胜强 | 高 | 爽感最强但风险大 | 必须有合理解释,不能降智 |

---

## 感情线四阶段推进法

```
熟悉阶段→试探阶段→暧昧阶段→确认关系
```

| 阶段 | 状态 | 核心写法 | 禁忌 |
|------|------|----------|------|
| 熟悉 | 朋友/陌生人 | 展示人设,制造初印象 | 不能做恋人行为 |
| 试探 | 有好感不确定 | 试探行为,误会和甜蜜 | 不能直接表白 |
| 暧昧 | 朋友以上恋人以下 | 偶尔做恋人事,脸红心跳 | 女频此阶段必须拉长 |
| 确认 | 正式情侣 | 撒狗粮+最大阻力事件 | 不能再退回试探 |

**铁律**:必须什么阶段干什么事。跨越阶段做事只会显得油腻。

### CP行为八种类型(好感度达标才出现)

| 行为类型 | 适用好感度 |
|----------|-----------|
| 亲密行为(肢体接触+) | 高好感 |
| 主权行为(争风吃醋、宣示主权) | 中高好感 |
| 信赖行为(无条件信任、分享秘密) | 中好感+ |
| 护短行为(对方犯错也优先维护) | 中好感+ |
| 特殊待遇(对待对方与别人完全不同) | 中好感 |
| 安抚行为(敏锐察觉不好情绪并安抚) | 中好感+ |
| 优先特权(做决定时对方是最重要因素) | 中高好感 |
| 默契行为(不约而同做同一件事) | 高好感 |

好感度必须通过剧情积累,不能凭空跳级。

---

## 喜剧 flag 机制

| 角色 | flag 规则 |
|------|-----------|
| 主角 | 不收 flag,或搞笑地收(不搞笑就变毒点) |
| 反派 | 必收 flag(说了大话必须被打脸) |
| 其他 | 一般收 flag |

反派的 flag 是爽点预告;主角的 flag 是喜剧张力——"我绝不会..."紧跟被打脸才好笑。

---

## 短篇视角控制法则

### 出场人物控制
- 开篇主要人物一对一或一对二,场景用"小"不用"大"
- 一对多时按重要程度降低配角存在感
- 切忌一拥而上出场
- 配角目光和心理描写集中于主角身上

### 视角数量控制
- 默认第一人称受害者亲历全程主导(番茄/七猫/黑岩短篇绝大多数如此);他人视角(渣男/白月光/反派)仅作追悔/得意的补刀短插,不平分篇幅
- 多视角悬疑等例外:至多 2-3 个视角,不超过3个
- 超过3个且各写各的无联系,大概率掉追读
- 敌人视角:让读者更清楚就写,让读者更多猜测就不写
- 敌人视角一般只写一个,占三个名额之一

---

## 情绪的三个层次与共鸣触发

### 情绪的三层分离

| 层次 | 说明 |
|------|------|
| 角色情绪 | 角色自己感受到的 |
| 文本传递情绪 | 文本要传达给读者的 |
| 读者感受情绪 | 读者实际感受到的 |

三者不能混为一谈。同一场景可以角色在哭、文本在撩、读者在爽。

### 情绪锚点

**定义**:能引起读者内心共鸣的具体桥段。情绪通过锚点"触碰"读者内心,不能靠直接描写堆出来。

**特征**:发生在现实世界、大众经常经历或感受的;不需要细致描写,几个关键细节就够。

**示例**:

| 传递情绪 | 差的写法 | 好的锚点 |
|----------|----------|----------|
| 母爱伟大 | 母亲天天煮饭搞家务 | 母亲偷偷给每个同学鞠躬拜托善待失明的孩子 |
| 生存很难 | 主角挖草根吃树皮 | 众人争相压低价格,主角直接举手"我不要钱,只要一顿饭" |
| 师生情深 | 老师认真上课 | 老师把唯一棉袄给学生,自己穿单衣在雪地里走 |

### 好钢用在刀刃上
1. 先构思大剧情(最大矛盾→中→小)
2. 在大剧情高潮处放置最精彩的锚点
3. 其他部分可以稍平淡
4. 情绪铺垫过程 = 剧情推进过程

**设计路径**:矛盾→情绪要求→人物/背景/设定→具体桥段

---

## 反派嘲讽新思路

| 类型 | 传统做法 | 新思路 |
|------|----------|--------|
| 目的 | 戳主角痛点,引发不爽 | 显露反派无知,引发看乐子 |
| 读者反应 | 替主角委屈 | 看笑话等反转 |
| 主角回应 | 绞尽脑汁想反击 | 放松、不在意即可 |

**操作**:确定主角优势,让反派极尽嘲讽主角的优势点。反派越嘲讽,读者越知道反派无知。
**保留戳痛点**:对象换为主角的亲朋好友——让反派攻击他们的弱点引发读者愤怒。

---

## 人设拆解从缺点开始

- 正面人物 = 优点掩盖了缺点
- 负面人物 = 缺点掩盖了优点
- 关键要素:角色有什么「不平之气」——必须能引起共鸣

**人设丰富法**:以杀伐果断为例——正向=干净利落;反向=没人情味。强化或弱化"人情味"区分主角/配角。角色有血有肉 = 优缺点共存。

---

## 以小搏大情绪模块四要素

1. **主角独有手段**:为什么能以小搏大
2. **搏的过程**:关键在过程不在结果
3. **赚翻了的感觉**:收获远超预期
4. **明确的搏的目标**:主角必须清楚自己要什么

---

## 爽点本质

**爽点本质** = 改变人际关系

打脸的爽不在赢,在关系翻转——婆婆从颐指气使变成求你别走、渣男从甩你变成堵门挽留。设计爽点先问:这一下改变了谁对主角的态度?

---

## 质量检查

写作时对照:

- [ ] 开头 100 字内有信息炸弹(不是风景/天气)?
- [ ] 每个场景至少两个功能(推进+揭示 / 铺垫+推高)?
- [ ] 情绪是否通过锚点触碰读者内心,而非直接描写出来?
- [ ] 震惊场景有落差铺垫(先建立角色原有认知再打破)?
- [ ] 感情线推进没跳阶段(熟悉→试探→暧昧→确认)?
- [ ] 反派 flag 必收、主角 flag 不收或搞笑收?
- [ ] 视角控制在 2-3 个(不超过 3)?
- [ ] 对话占正文 45-65%,每句对话推剧情或揭示性格?
- [ ] 结尾用动作/对话收(不升华、不总结、不感慨)?
- [ ] 没有"该爽不爽"的段落(金手指效果展示清楚)?
references/hooks-chapter.md
# 章级钩子:章首/章尾钩子选择与写作

> 写每章开头或结尾时加载。先看决策路由,选对钩子类型,再套模板。

---

## 决策路由

### 章尾钩子:根据本章情绪选类型

| 本章情绪/阶段 | 推荐钩子 | 为什么 |
|-------------|---------|-------|
| 悬念/未知 | 突然揭示 / 神秘物品 / 留白 | 激发好奇 |
| 紧迫/危机 | 紧急危机 / 倒计时 / 未完成动作 | 制造焦虑 |
| 反转/真相 | 身份反转 / 隐藏含义 / 回声 | 颠覆认知 |
| 情感/关系 | 两难抉择 / 承诺威胁 / 情感转折 | 拉扯情绪 |
| 系统文特有 | 系统激活 / 选择型 | 期待金手指 |
| 氛围/日常 | 意象钩子 / 离奇消失 / 信息差 | 暗流涌动 |

### 章首钩子:根据开篇策略选类型

| 开篇策略 | 推荐钩子 | 为什么 |
|---------|---------|-------|
| 直接冲击 | 未完成动作开局 / 倒计时开局 | 即时紧迫感 |
| 制造好奇 | 悬念对话 / 闪前碎片 / 神秘独白 | 信息差驱动 |
| 对比冲击 | 反差场景 / 意象预示 | 反差吸引 |

### 章节阶段与钩子强度

| 故事阶段 | 推荐类型 | 强度 |
|----------|---------|------|
| 第 1 章 | 系统激活 / 截断 / 危机升级 | 必须强 |
| 2-3 章 | 选择 / 信息差 / 情感转折 | 强 |
| 中期日常 | 对话悬念 / 信息差 / 身份即将揭露 | 中 |
| 高潮前 | 危机升级 / 悬念预知 | 强 |
| 大结局 | 时间跳跃 / 情感转折 | 收束 |

---

## 章尾钩子 13 式

| # | 名称 | 公式 | 示例 |
|---|------|------|------|
| 1 | 突然揭示 | 抛出改变全局的信息 | "信上的日期,是他死后第七天。" |
| 2 | 紧急危机 | 下章必须回应的紧迫威胁 | "裂缝在扩大,灵石还差三块。" |
| 3 | 未完成动作 | 动作被新变量打断 | "他刚伸手,手腕忽然被人扣住。'别动。'身后传来一个声音。" |
| 4 | 身份反转 | 身份真相偏离读者预期 | "她叫林小月。但档案上写的是:林小月,已故。" |
| 5 | 两难抉择 | 被迫在两个坏选项中选一个 | "签了这份文件,公司保住了,但他得进去。" |
| 6 | 神秘物品 | 重要但含义未知的物件 | "包裹里是一把钥匙,附了张纸条:'你欠我的。'" |
| 7 | 倒计时 | 时间不够用 | "医生说还有三个月。那是两个月前的事了。" |
| 8 | 承诺/威胁 | 有人宣布了行动意图 | "今晚十二点之前,我会告诉所有人你做了什么。" |
| 9 | 离奇消失 | 不可能的消失 | "手铐还在,人没了。整个房间只有一个门,门没开过。" |
| 10 | 隐藏含义 | 表面正常,实际暗藏信息 | "他说:'你和妹妹真像。'可她是独生女。" |
| 11 | 意象钩子 | 反复出现的意象发生变化 | "窗台上那盆枯了一个月的茉莉,突然冒出了花苞。" |
| 12 | 回声钩子 | 章尾句呼应开头,关键细节变了 | 开头:"她说她永远不会原谅他。" / 结尾:"她说她永远不会原谅他。但她的手,攥得更紧了。" |
| 13 | 留白钩子 | 只展示反应,不揭示发生了什么 | "他看了信。脸色变了。什么也没说,把信叠好放进口袋。" |

---

## 章首钩子 7 式

| # | 名称 | 公式 | 示例 |
|---|------|------|------|
| 1 | 悬念对话开局 | 直接从意味深长的对话开始 | "你确定要这么做?/ 确定。/ 那你就别后悔。" |
| 2 | 闪前碎片 | 先给结果碎片,再回正常叙事 | "后来他才知道,那个电话改变了所有事。但此刻他还一无所知。" |
| 3 | 倒计时开局 | 开头就建立紧迫感 | "距离合同到期还有 72 小时。" |
| 4 | 神秘独白 | 第一人称诡异内心独白 | "我一直在想,那天如果我没有回头,一切会不会不一样。" |
| 5 | 反差场景 | 两个截然不同的场景并列 | "一边是婚礼现场,香槟塔堆得老高。另一边,医院走廊的灯在闪。" |
| 6 | 未完成动作开局 | 动作进行中被截断 | "他刚把钥匙插进锁孔,门里传来一声轻响。" |
| 7 | 意象预示 | 用环境意象暗示即将发生的事 | "天边烧得通红,像是什么东西在着。她抬头看了一眼,继续走。" |

---

## 章尾实战模板

### 一、系统激活型
```
就在{主角}{最低谷状态}之际。
{一个感官描写}。
叮!
【{系统名},正式启动。】
```
> 就在他万念俱灰之际。一个冰冷的机械音,突然在他脑海中响起。叮!【年少轻狂系统,正式启动。】

### 二、悬念预知型
```
{主角}{冷静/冷漠地}看着{对方的嚣张行为}。
{一个短句:继续吧/慢慢来/不着急}。
{预告即将发生的反转,不透露细节}。
```
> 我冷眼看着笑得前仰后合的人。笑吧。多笑会儿。过了今天,你们就再也笑不出来了。

### 三、截断型
```
{一个小动作}。
{剧烈反应}。
{震惊/疑问。本章结束,不给答案}。
```
> 仅仅一口!她的瞳孔猛地收缩成针尖状。轰!一股恐怖到难以形容的热流,瞬间在她的腹中炸开!这粥有问题!里面到底放了什么?

### 四、选择型
```
【选择一:{诱人选项}】奖励:{极其诱人} 代价:{隐含风险}
【选择二:{另一选项}】奖励:{另一种诱人} 代价:{另一种风险}
```
章节结束,不给出选择结果。

### 五、危机升级型
```
{突然的声响/动作}!
{新威胁出现}。
{主角的恐惧/紧迫反应}。
```
> "都给老子出来!快点!"哐啷!!!一只沾满泥污的军靴,粗暴地踹开了笼门。

### 六、身份即将揭露型
```
{一个旁观者}突然{提出疑问/发现线索}。
"{暗示主角真实身份的话}"
{其他人不以为意,主角暗暗紧张}。
```

### 七、情感转折型
```
就在这时,{角色}的{表情/态度}突然{大变}。
{一个出人意料的示弱/请求动作}。
"{戳中主角软肋的话}"
```

### 八、信息差型
```
{主角的自我怀疑/感慨}。
{一个意象/比喻}。
{暗示即将改变,但不明说}。
```
> 花有重开日,人无再少年。自己怎么就把日子,过成了这副狗屎样?

### 九、对话悬念型
```
"{提出请求的前半句}"
"{对方回应}"
"{意想不到的后半句,不是读者预期的}"
```
> "我有一个请求。"苏牧缓缓说道。"你说。""离婚之前,我要证明一次自己。"

### 十、时间跳跃型
```
{精确/模糊的时间标记}。
{出人意料的结果}。
{暗示过程的一句话}。
```
> 三分三秒后。江亦瑶神色从容地走出房间,整理了一下微乱的衣服。

---

## 三种速写章末钩子

### 收获预告型(副本/任务尾声)
```
{当前行动接近完成}。
{暗示即将获得的奖励}。
{一句话暗示奖励比预期更大}。
```

### 反派逼近型(铺垫段结尾)
```
{主角当前状态}。
{暗处/远方,反派的某个行动}。
{暗示这个行动将影响主角}。
```

### 行动预告型(装逼/打脸前)
```
{主角做出一个决定}。
{一个准备动作}。
{暗示即将发生的行动}。不写结果。
```

---

## 爽点与看点设计规则

### 爽点核心公式

爽点 = 两个逻辑的冲突点:
- **大众逻辑**:对手不可战胜 → 恐惧/绝望
- **主角逻辑**:正好手痒/轻描淡写
- 落差越大 → 爽点越爽

### 爽点三要素

| 角色 | 操作 |
|------|------|
| 吃瓜群众 | 用议论吹高对手逼格 → 震惊反应完成打脸升华 |
| 对手 | 嚣张到让读者恨 → 被打脸时才爽 |
| 主角 | 铺垫时低调 → 释放时干脆 |

看点 = 吊起读者期待;爽点 = 满足期待。一一对应:前面铺垫多少,后面爽点就有多少。

### 装逼打脸节奏

| 阶段 | 公众期(5 章循环) | VIP 期 |
|------|-------------------|--------|
| 人设 | 1 章 | 1-2 章 |
| 铺垫 | 2 章 | 5 章 |
| 打脸 | 1 章 | 1-2 章 |
| 善后 | 1 章 | 1 章 |

**关键**:
- 前置小无敌:打脸前必须把道具/人物/实力准备好,没有则读者担惊受怕,嘲讽变虐主
- 铺垫比打脸重要:铺垫到位后主角站着不说话都能装逼
- 打脸对象层级决定爽感层级:路人甲震惊不叫爽,有分量的人震惊才叫爽
- 章末钩子约 100 字即可显著提升完读率

---

## 质量检查

写完章首/章尾后对照:

- [ ] 章首前 100 字有钩子(不是风景/天气开头)
- [ ] 章尾有让读者想翻下一页的东西
- [ ] 钩子强度与章节阶段匹配(日常章不必强钩子)
- [ ] 连续两章不用同一种钩子
- [ ] 钩子约 100 字,点到即止,不拖沓
references/hooks-paragraph.md
# 段落级钩子技法
短篇/段落级钩子类型、组合技法、禁忌、对话情绪递增、不公平伤害、围观者层级。用于段落内微钩子设计。

---

## 段落级钩子 11 种

| # | 名称 | 公式 | 示例 | 有效性 |
|---|------|------|------|--------|
| 1 | 信息差 | 读者知道关键信息,角色不知道 | "他笑着接过咖啡,不知道她刚才在里面加了一片安眠药。" | ★5 |
| 2 | 倒计时 | 设置明确的时间压力 | "离婚冷静期还剩最后三天。三天后,他签不签都无所谓了。" | ★4 |
| 3 | 反转 | 读者预期突然被打破 | "她打开遗嘱,继承人那一栏空着。下一页压着她母亲的死亡证明。" | ★5 |
| 4 | 暗牌 | 主角藏了一张牌没打,读者等亮牌 | "婆婆骂了整整十分钟。我一句话没说。手机录音还在跑。" | ★4 |
| 5 | 打脸 | 角色正在得意,读者知道马上要被打脸 | "【就你?也想跟我争?】她笑得前仰后合。我没说话。只是把那份亲子鉴定报告从包里拿了出来。" | ★5 |
| 6 | 代价 | 暗示主角即将失去某样东西/选择要付出惨重代价 | "我知道举报他意味着什么。举报了他,我也完了。但我还是按下了发送键。" | ★4 |
| 7 | 弱者/孩子 | 引入需要被保护的弱者,激发保护欲 | "女儿躲在门后面,捂着耳朵。她才四岁,什么都不懂。但她知道不该出声。" | ★4 |
| 8 | 灵魂旁观 | 主角已死,以灵魂视角叙述,全知但无力改变 | "我的魂魄慢慢飘起来,看着妈妈铁青的脸。对不起妈妈,我没能站直。" | ★5 |
| 9 | 异常物件 | 不该出现在这里的物件引发全局 | "避孕套日期突然变了,连味道也变成了我以前过敏的芒果味。我笑着点头。" | ★5 |
| 10 | 假意顺从 | 重生后主动给出前世拼命保护的东西,对手以为她疯了 | "'你既然想要,就都给你吧。'反正谁拿到这笔巨额遗产谁就大祸临头,正愁保命的方法呢。" | ★4 |
| 11 | 冷发现 | 发现异常后第一反应保持极度冷静,冷静本身制造悬念 | "钥匙扣上印着'悦安月子中心VIP'。我们明明没有孩子。我笑着点头。当天就去月子中心记下了预约单。" | ★5 |

### 使用要点
- 信息差不能拖太久,超过 3000 字读者会烦
- 倒计时一旦设定,必须到期兑现,不能烂尾
- 反转必须有铺垫,无铺垫的反转是作弊
- 暗牌不能藏太久,短篇里最多藏 30% 篇幅
- 打脸要干脆利落,一句话打脸比一段话打脸爽
- 代价必须真实,主角最后什么都没失去则代价钩子失效
- 弱者不能只是工具人,要有自己的行动线
- 灵魂视角的限制(只能看不能动)必须严格遵守

---

## 钩子组合

### 基础组合

| 组合 | 效果 | 适合题材 |
|------|------|----------|
| 信息差 + 暗牌 | 读者知道主角在憋大招 | 复仇、世情 |
| 倒计时 + 代价 | 时间紧迫 + 选什么都亏 | 悬疑、婚恋 |
| 反转 + 打脸 | 情绪过山车 | 所有爽向短篇 |
| 弱者 + 代价 | 共情拉满 | 追妻、世情、死人文学 |
| 暗牌 + 打脸 | 爽感最强 | 复仇、逆袭 |

### 进阶组合

| 组合 | 效果 |
|------|------|
| 异常物件 + 冷发现 | 日常悬疑感 + 智商碾压 |
| 灵魂旁观 + 弱者 | 极致悲剧 + 保护欲 |
| 假意顺从 + 暗牌 | 三重信息差 + 爽感蓄力 |
| 阶梯背叛 + 冷发现 | 层层加码 + 全程冷静 |

### 组合使用原则
1. 技巧服务表达,不要为了用技巧而用技巧
2. 构思剧情阶段可同时考虑多种技巧丰富剧情
3. 技巧可杂糅使用,没用就丢弃
4. 不要通过单个技巧推导剧情,技巧是工具不是束缚

---

## 钩子禁忌

| 禁忌 | 说明 |
|------|------|
| 假悬念 | 威胁不存在或立刻解除,读者被骗一次就不会再信 |
| 机械降神 | 章尾抛出危机,下章开头用巧合解决 |
| 过度留白 | 连续多章不揭示任何信息,读者失去耐心 |
| 低风险钩 | 用无关紧要的事制造悬念 |
| 同类型连用 | 连续 3 章以上用同一种钩子类型 |

---

## 对话情绪五级递增

写矛盾冲突时,对话越不礼貌,情绪越强烈:

| 级别 | 类型 | 强度 |
|------|------|------|
| 1 | 客观陈述事实 | 最弱 |
| 2 | 客观陈述 + 提出建议 | 礼貌但有力 |
| 3 | 主观指责 | 开始有冲突感 |
| 4 | 主观指责 + 强制命令 | 强情绪 |
| 5 | 主观指责 + PUA抬升自己 | 最强 |

最强话术:打着"为你好"的幌子,句句不离关心,但句句都是嫌弃、指责、厌恶。

---

## 不公平伤害钩子

不公平伤害 = 不公平(主角无辜)+ 伤害(物理+精神)

- 精神上的伤害更能拉动读者情绪——大多数读者受气的时候多
- "不患寡而患不均":减月例大家都减(公平),但唯独主角药材降了一等(不公平)
- 随便挑一种写到位都能拉起对反派的仇恨

### 三种伤害类型

| 类型 | 动机 | 行为 | 态度 |
|------|------|------|------|
| 利益转移型 | 反派不想自己承担代价 | 掠夺、命令、逼迫主角 | 理所应当、趾高气昂 |
| 损失转嫁型 | 反派将受惩罚嫁祸给主角 | 威逼利诱、做局、逼迫顶罪 | 隐蔽、阴险 |
| 针锋相对型 | 反派所处秩序被主角打破 | 陷害、算计、伤害 | 不留活路 |

### 压主角的正确方式
1. 尽量不压——用"他人否定,主角打脸"替代"主角真受委屈"
2. 否定间接化——当面嘲讽改为内心活动或背后议论
3. 否定理由合情合理——基于现有信息的合理判断
4. 否定人物多元化——不同立场否定方式不同
5. 只在卷高潮时稍微压一下

---

## 围观者质量层级

震惊效果取决于围观者的数量和质量:

| 层级 | 对象 | 特征 |
|------|------|------|
| 低质量 | 群演、打杂、实习生 | 纯情绪反应,无后续 |
| 中质量 | 懂行的从业者、副手 | 能做专业对比,有态度变化 |
| 高质量 | 导演级别、行业顶层 | 反应伴随信息展露、关系进展、拉起期待 |

### 围观者分层扩大原则

| 维度 | 原则 |
|------|------|
| 角色种类 | 多种角色 > 单种角色,每种产生不同反应 |
| 角色数量 | 多人 > 少人,选择可多人出场的场合 |
| 角色层级 | 多层级 > 单层级,路人/同行/高手/反派每层不同 |
| 关系深度 | 熟人 > 陌生人,熟人震惊远爽于陌生人震惊 |

---

## 拆文检查清单

拆文时对每个钩子评估:

1. 钩子类型是什么?
2. 钩子在第几句话/第几个字出现?
3. 钩子强度(1-10)?
4. 钩子是否在合理位置兑现?
5. 铺垫和兑现之间的距离是否合适?
6. 是否有钩子未兑现(烂尾)?
references/output-contract.md
---
name: output-contract
description: |
  story-short-analyze 输出契约。定义 Stage → 文件映射、_meta.json schema、
  下游消费规范(story-short-write 读全套 markdown + 原文 + _meta.json 写新短篇)。
sync-policy: |
  本文件在 story-short-analyze 与 story-short-write 之间需保持字节一致(byte-equal)。
  修改任一副本后,必须同步另一副本,并通过 bash scripts/check-shared-files.sh 验证。
  禁止把本文件加入 IGNORE_NAMES 列表——它必须保持同步,不属于 intentional differences。
---

# 输出契约:story-short-analyze ↔ story-short-write

`story-short-analyze` 拆完一篇短篇后,产物落盘到 `拆文库/{书名}/`。`story-short-write`
写下一篇同题材短篇时,**同时**读这个目录下的全部产出。

---

## 输出目录与文件树

```
拆文库/{书名}/
├── 原文/                  # 管道前置步骤产出,存放源文件备份
├── 拆文报告.md             # 人类可读综合报告(Stage 2-6 综合)
├── 情节节点.md             # Stage 2 情节节点清单
├── 写作手法.md             # Stage 4 写作手法分析
└── _meta.json             # 管道元数据 + 结构计数(resume + 验收数值依据)
```

**文件名约定**:`拆文报告.md / 情节节点.md / 写作手法.md` 由 `story-short-write` 硬编码
消费,不可重命名。分析叙事走 markdown,数字/枚举走 `_meta.json.structure_counts`。

---

## Stage → 文件映射

| Stage | 名称 | 落地文件 | 主要内容 |
|-------|------|----------|---------|
| 2 | 结构+情节节点 | `拆文报告.md`(故事核/结构/梗概段) + `情节节点.md` | 故事核 / 4-6 段结构 / 故事梗概 / 情节节点清单 |
| 3 | 情感线+爆点 | `拆文报告.md`(情感曲线段+爆点段) | 情感曲线 ≥5 节点 / 爆点 6 维度 / 期待感 |
| 4 | 反转+写作手法 | `拆文报告.md`(反转段) + `写作手法.md` | 前置反转检查 / 反转分析(铺垫 ≥2) / 写作手法 ≥5 项 |
| 5 | 人物+开头结尾 | `拆文报告.md`(人物段+首尾段) | 人物分类+功能评估 / 开头分析 / 结尾分析 / 首尾呼应 |
| 6 | 综合评估 | `拆文报告.md`(综合段) + `_meta.json`(写 structure_counts) | 五维评分 / 爆点性 / 话题性 / 共鸣 ≥3 层 / 可复用结构 ≥3 条 / 节奏速报 |

---

## `_meta.json` schema

`_meta.json` 是管道元数据 + 结构计数。**不放分析内容**,只放数字和枚举——给验收
检查做完整性校验用。分析叙事都在 `拆文报告.md` 里。

```jsonc
{
  "version": "2.0",
  "word_count": 5234,                   // 源文字数(Phase 1 探针填入)
  "genre_detected": "追妻",             // Phase 1 题材识别;未识别填 "通用"
  "created_at": "{ISO8601 时间戳}",      // 拆文启动时间,写入时填当前 UTC
  "stages_completed": [2, 3, 4, 5],     // 已完成 Stage,按完成顺序 append
  "last_stage_in_progress": null,       // 当前正在执行的 Stage;空闲为 null

  "structure_counts": {                 // Stage 6 完成时一次性写入;structure_counts 数值校验依据
    "beats": 5,                         // 结构段数(结构划分,开端/发展/高潮/结局,Stage 2)
    "hooks": 4,                         // 钩子数(Stage 3)
    "setup_clues": 3,                   // 反转铺垫线索数(Stage 4)
    "character_archetypes": 3,          // 有反差人物数(Stage 5)
    "reusable_structures": 3,           // 可复用手法条数(Stage 6)
    "reversal_type": "视角反转"          // 反转类型枚举(视角/身份/动机/时间线/信息/认知/无反转);甜宠/喜剧/报应型填「无反转」
  }
}
```

### 写入顺序(crash safety)

1. **Stage N 开始前**:`last_stage_in_progress = N`,写盘。
2. **Stage N 文件写完后**:non-empty + 最小长度合理性检查(如 `拆文报告.md` 新增段 ≥ 200 字)。
3. **通过**:清空 `last_stage_in_progress`,append `N` 到 `stages_completed[]`。
4. **失败**:`stages_completed` 不动,`last_stage_in_progress` 保留为 `N`。
5. **Stage 6 完成时额外动作**:把 `structure_counts` 一次性算出并写入 `_meta.json`,
   然后才进入验收。

### Resume 协议

- `last_stage_in_progress` 非空 → 该 Stage 上次中断,**从头**重跑(不复用半成品)。
- `last_stage_in_progress` 为空 → 从 `max(stages_completed) + 1` 开始。
- `stages_completed` 含 6 → 已完成,询问用户覆盖/取消。

**Stage 6 = 内容写完 AND 验收通过**。验收未过前 `last_stage_in_progress` 保持 `6`、`stages_completed` 不含 `6`;resume 时正文/structure_counts 已在盘上,只重跑验收检查,不重写 Stage 6 正文。

---

## 验收接入点

Stage 6 内容写完后、`stages_completed[6]` append 前,跑三道检查:

### Step 1:拆文报告 AI 腔自检

扫描 `拆文报告.md` 全文 against 拆文流程本地加载的禁用词表与报告 AI 腔规则。
这是拆文报告质量门;成稿去 AI 规则由写作流程在自己的 Skill 内维护,不跨 Skill 读取参考文件,也不要把两套规则混用。
命中 → 不写 `stages_completed[6]`,列出位置请用户修订**拆文报告本身**的 AI 腔
(源文里有 AI 腔不算——这里扫的是分析师写的报告)。

### Step 2:`_meta.json.structure_counts` 数值校验

| 字段 | 最低值 | 不达标 |
|------|--------|--------|
| `structure_counts.beats` | ≥ 4(结构段:开端/发展/高潮/结局)| 阻断 |
| `structure_counts.hooks` | ≥ 3 | 阻断 |
| `structure_counts.setup_clues` | ≥ 3(reversal_type=无反转时跳过本行)| 阻断 |
| `structure_counts.character_archetypes` | ≥ 2 | 阻断 |
| `structure_counts.reusable_structures` | ≥ 3 | 阻断 |
| `structure_counts.reversal_type` | 在枚举内(含「无反转」)| 阻断 |
| `genre_detected` | 非空 | 阻断 |

> 情节节点数(15-60 个,按字数分档)走 `情节节点.md` 自己的密度校验(见 material-decomposition.md),不在本表。`beats` 是结构段数,不是情节节点数。

### Step 3:`story-short-analyze` BLOCK 项扫描

扫拆文流程本地加载的输出模板,确认所有 `[BLOCK]` 标注项对应的产出段均在 `拆文报告.md` 出现。
任一缺失 → 阻断。`[WARN]` 项 → 写入拆文报告末尾「待补」清单,不阻断。

### Step 4:通过

清空 `_meta.json.last_stage_in_progress`,append `6` 到 `stages_completed[]`,提示
用户「拆解完成,可调用 `/story-short-write` 写下一篇」。

---

## 下游消费规范(story-short-write 怎么用)

> `story-short-write` 当前硬编码读 `拆文报告.md / 情节节点.md / 写作手法.md` 三个 markdown。
> `_meta.json` 是可选增强:read 容忍,不存在不阻塞写作。

| 文件 | 角色 | 怎么读 |
|------|------|--------|
| `_meta.json`(可选)| 数字门面 + 题材识别 | 看 `genre_detected` 决定哪个题材标尺,读 `structure_counts` 确认拆文完整性,读 `structure_counts.reversal_type` 选反转骨架 |
| `拆文报告.md` | 分析叙事主体 | 读「故事核」「结构」「情感曲线」「爆点」「反转分析」「人物」「五维评分」「共鸣分析」「可复用结构」「同类型写作动作」段,是 writer 的主输入 |
| `情节节点.md` | 节奏锚点 | 看每个节点的字数位置 + 功能 + 触发事件,给新故事排节奏 |
| `写作手法.md` | 手法库 | POV / 对话 / 时间 / 信息控制 等具体手法 + 原文示例,新篇里复用 |
| `原文/` | 语感源 | 抄对话调子、节奏、画面感、打脸张力。**不抄具体情节**,抄写法。 |

### 写作流程建议

1. 看 `_meta.json.genre_detected` 和 `structure_counts.reversal_type` 选骨架。
2. 读 `拆文报告.md` 的「核心手法」「共鸣分析」「可复用结构」段,决定要保留 / 调整哪些。
3. 读 `情节节点.md` 把节奏锚点抄到新故事的字数位置上。
4. 写场景时翻 `写作手法.md` + `原文/`,参考具体写法。
5. 写完后(可选)在新文档 frontmatter 写 `derived_from: 拆文库/{书名}/` 追溯。

### 维护者本地烟雾测试

```bash
ls 拆文库/{书名}/   # 应有:原文/ 拆文报告.md 情节节点.md 写作手法.md _meta.json
/story-short-write 拆文库/{书名}/
# 通过:输出 8000+ 字同题材新短篇,prose 有源文对话节奏和画面感
# 失败:写得像填空 / 或 short-write 找不到三个 markdown
```

---

## 版本约定

- `_meta.json.version` 与本文件 `sync-policy` 联动。
- breaking change(字段重命名 / 类型变更 / 必填变更)必须 bump major version 并同步两侧
  副本,CI 通过 `scripts/check-shared-files.sh` 拦截单边修改。
- additive change(新增可选字段)可 bump minor;producer、consumer 与两侧副本必须在同一变更中升级到当前 schema。
references/short-craft.md
# 短篇写作通用底座

> 短篇网文正文写作的公共父类。服务番茄 / 七猫 / 黑岩 / 盐选通俗短篇(1-2 万字),不是传统文学。
> 题材风格包(`genre-styles/`)在此基础上做覆盖加味;本文件管所有题材共通的原生写法。
> **总原则:情绪宁烈不温。短篇是情绪产品,靠直白共鸣留人,不靠克制留白。**

---

## 1. 人称与腔调:第一人称受害者,双轨切换

- **默认第一人称「我」全程主导**。受害者亲历视角,读者直接钻进主角身体和脑子里。他人视角(渣男 / 白月光 / 反派)只作**追悔 / 得意的补刀短插**,不平分篇幅。极少数男频自救文用男「我」。
- **腔调是双轨,靠「心死」开关切换**:

| 阶段 | 声线 | 怎么写 |
|------|------|--------|
| 受虐段 | 直白宣泄 | 喊疼、喊委屈,眼泪不争气,心如死灰。成段内心独白和连环反问 |
| 反击 / 反转段 | 冷静审判 | 抽离、反讽、居高临下。见血、不铺陈,「呵」「我纯粹觉得你比较可笑」 |

- 「心死定格句」是切换枢纽:撞破真相后用一句独立成段的心死宣告(「那一刻,我的心彻底死了」),把情节从委屈隐忍切到冷静决裂,后续行为逻辑全由它翻转。

---

## 2. 情绪外化:直接写情绪词,后面接一个场景里的具体动作(替代机械替换表)

短篇最核心的写法,也是和长篇审美冲突最大的一条。

真实番茄里,情绪词(心如死灰、心如刀绞、几欲作呕、眼泪不争气)就是直接写出来的,而且用得很密——这是卖点,不是 AI 味。关键在后面一句:**情绪词后面要接一个这个场景里此刻特有的具体动作或物件,不能让情绪词单独飘着。**

照真实小说写(以下都是原文,照这个语感):

- 《为给白月光出气,男友群发我私密照》我心如死灰,泪水早已模糊了视线。
- 《为给白月光出气,男友群发我私密照》我紧紧抿着嘴唇,直到尝到血腥味,才勉强找回一丝清明。
- 《被凌辱后,我把他们送上法庭》我在一旁看得心如刀绞,几欲作呕。
- 《闻笙有两意》摸到那碗凉掉的排骨汤时,我的心更冰冷了。上面还浮着凝固的油,根本喝不了。
- 《和闺蜜死遁后,文武状元悔疯了》怒火和心痛在胸腔翻涌。我鼻头一酸,将玉佩摔得支离破碎。
- 《为讨真千金欢心》我紧紧攥着衣角,颤抖着问。

看这些原文的共同点:情绪词后面接的是「此刻手里、眼前那个具体的东西」——凉掉的排骨汤、摔碎的玉佩、攥着的衣角、抿出血腥味的嘴唇。不是哪篇都能套的通用生理反应(单写「指甲掐进掌心」「心猛地一沉」「鼻头一酸」就空)。

情绪后面接的不一定只是物件,也可以是任务卡点:主角要办一件眼下必须办成的事,却在这一步被卡住。短篇里的任务卡点必须短、狠、贴情绪:卡住救命钱、卡住证据、卡住离场、卡住求救、卡住反击。目的不是写流程,也不是为了显得自然,而是让读者立刻感到“更难堪 / 更绝望 / 更该反击”。

**要删的两种:**

1. 后面接不上任何具体东西、单独出现的情绪总结句(「一丝悲伤涌上心头」「感到深深的绝望」「仿佛整个世界失去了颜色」)。
2. 同一个情绪反复写、写完再补一句解释它多疼。一个情绪写一次、接一个具体动作,就往下走。

---

## 3. 叙述姿态:在场叙述者(区别于长篇深度限知)

短篇叙述者**高度在场**:可以跳出当下替读者定性、剧透、下判词、预告火葬场。这是爽感引擎,不是 AI 味。

- 留:第一人称主角的**主观审判句**(「原来我以为的爱只是一场笑话」)、**复仇者剧透勾子**(「他们不知道,我已经是西域摄政王」)、**火葬场前瞻预告**(「我还不知道,他正发疯似的找我」「可后来他后悔了」)。
- 删:真·中立全知摄像机腔——和角色情绪无关、纯交代背景的作者解说。

判据:这句跳出当下,是**带主角主观偏色的审判 / 勾追读**,还是**中立无情绪的作者讲解**?前者留,后者删。

---

## 4. 小节结构:超短章节制,一节一炸点

按平台两套节奏:

| 平台 | 小节标记 | 段落 | 单节字数 |
|------|----------|------|----------|
| 黑岩 | `###` + 数字(`###1.`) | 段落短、多换行留白 | 800-1200 |
| 番茄 / 七猫 | `###第N章` 或 `###1.` | 短段为底色,允许多句回溯叙述段交织对话 | 1000-2000(古言 2000-4000) |

- 每个小节:**一个背叛升级 / 一次态度反转 + 一句更狠的台词 + 一条读者新获知信息**。
- 章尾**不收束情绪、必留钩**。
- **疏密**:背叛 / 打脸 / 反转 / 决裂处详写慢放,逐拍展开;过场 / 赶路 / 信息交代一句带过,把字数预算让给情绪节点。
- 任务卡点必须服务“一次背叛升级 / 一次态度反转 / 一条新信息”,不能单独变成生活流程;没有情绪或证据收益就不写。

---

## 5. 钩子:极高密度,阶梯式背叛

- 导语 50-300 字塞 3-5 个事件 / 反转;正文几乎每节一个新爆料或一次态度反转;黑岩密到每 3-5 句一拐。
- **阶梯式升级**:道德踩踏 → 经济 → 生命,越揭越大。一页里敢叠多重雷(绝症骗局 + 当众拥吻 + 孩子药引…),别单点反复抒情。
- **爽点章章前推**:每章必须推进一个反打 / 行动 / 亮牌(扇耳光、取消婚礼、删 B 超单转身走、当众揭穿、亮身份),不要停在情绪闭环;章尾留反杀预告(「已联系律师」「已查到他的致命把柄」),制造追读缺口。读者要的是「下一章想看她怎么打脸」。
- **当众围观放大羞辱**:把背叛 / 打脸放在有外人见证的场合(宴席、股东会、群聊截图、请安),用众人的起哄 / 鄙夷 / 沉默放大屈辱与出气感;独角戏起不来「丢脸感」。
- 共通钩子母题(题材包细化):白月光 / 小青梅触发背叛、偷听 / 撞破真相一次性砸、灾难期求救反被辱、朋友圈 / 物证坐实、数字 / 年限承重再清零、身份反差底牌、当众羞辱 + 反转倒计时。

---

## 6. 对话:占比高,三类台词撑全篇

- 占比约 40-60%(盐言 / 黑岩打脸篇可到 60%;抒情虐 / 心死篇可低到 30-40%)。**按情绪驱动还是事件驱动浮动,不设死线。**
- 三类台词:① 施害者道德踩踏 / PUA 狠话(钩子本体,越理直气壮越好);② 白月光「娇弱委屈」软刀(对不起 / 我不是故意的 / 都怪我);③ 主角心死后冷静审判金句。
- 权力博弈靠对话长度反转标记:施害方前期长篇 PUA,主角后期一句话冷怼。详见 `dialogue-mastery.md`。

---

## 7. 收尾:火葬场预告 / 审判金句 / 漠视离场

短篇收口都不是文学升华:

| 收口式 | 例 |
|--------|-----|
| 火葬场反转 | 主角走后施害者悔疯 / 悬赏寻找,主角已无波澜或冷拒(「敦宜,你不要我了吗?」) |
| 审判 / 反讽金句盖棺 | 「迟来的深情比草还贱」「在他们眼里我是锦绣上的破洞」「晏家,咱们的账慢慢算」 |
| 转身离场 / 净身出户 | 签离婚协议、改嫁、带孩子一去不归、亮身份碾压 |
| 漠视收尾 | 「那是他们的故事了,与我再无瓜葛」——比报复更狠 |

- 删:空洞哲理升华(「人生就是这样」「她终于明白了」)。
- 留:第一人称复仇预告 / 审判金句 / 心死余韵——这是读者截图传播的高光。

---

## 8. 节奏与标点

- 段短、勤换行,手机流式:叙述用逗号长句,但短篇整句偏短、多单句成段,一句 20 字上下就落句号,别把逗号一路拖成冗长的流水句。短句是偶发的情绪重拍,落在峰值,不是通篇底色。
- 标点跟语气走:质问用问号,爆发峰值少量感叹;犹豫 / 未尽 / 被打断用短句、动作或换行断句。
- 三连排比是抒情堆苦难的手法(「在暴雨夜、在手术室门口、在我父亲的葬礼上」):**有功能的递进 / 枚举保留**,只砍无信息量的纯修辞凑数。
- **标点硬约束**:本仓库正文输出统一不出现 `……` / `——` / `—` / `--`(去 AI 脚本与 hook 强制收口)。停顿、哽咽、未尽、留白一律用短句、句号、逗号、动作 beat 或换行实现。题材包范例中若保留 `……`,只为示意手法,落到正文要按本条改写。

---

## 9. 与长篇的根本区别

| 维度 | 长篇规则 | 短篇底座 |
|------|----------|----------|
| 视角 | 深度限知、不剧透 | 第一人称在场、主动剧透火葬场 |
| 情绪 | 不设固定译法,按场景落到选择/台词/后果,必要时直写 | 情绪可直给;不强接身体反应 |
| 章尾 | 禁总结 / 升华 / 预告 | 审判金句 / 火葬场预告是卖点 |
| 结构 | 贯穿道具三次 / 数字阶梯 | 偷听 / 底牌 / 回旋镖 / 次数累积 |
| 内心 | 独白 ≤ 2 段 | 成段宣泄独白 + 连环反问是主引擎 |
| 排比 | 砍到一条 | 抒情堆苦难有功能则留 |

---

## 10. 场景写法:三维度揉进

发生、感知、反应是可选信息维度,不是逐段配额。关键子事件只把确有新信息的维度**织进同一段连续正文,不按维度分段**;上下文已清楚时可以留空:

| 维度 | 内容 | 要点 |
|------|------|------|
| 发生 | 事情出现了 | 1-2 句,含一个具体细节 |
| 感知 | 主角注意到什么 | 只取会改变理解、判断或气氛的声音、物件、空间或身体感受,不按感官凑数 |
| 反应 | 角色接下来怎么办 | 选择、台词、策略、任务动作、明确感受或有后果的身体变化;不默认补手/眼/呼吸微动作 |

任务卡点也按需取三维:发生 = 事情办不下去;感知 = 主角看到、摸到或听到卡住的具体证据;反应 = 当场判断、选择或下一步反击。不要把流程单独写成说明段,也不要在场景末尾补动作尾巴解释意义。

反模式(绝对禁止):把三维度拆成三段依次写完,同一件事掰开写三遍。

- 正确(揉进式,《闻笙有两意》原文):我以为陆池是真心跟我道歉,但摸到那碗凉掉的排骨汤时,我的心更冰冷了。上面还浮着凝固的油,根本喝不了。(摸汤、凝固的油、心冰冷揉在一段里)
- 错误(把上面这段拆成三段堆叠):我摸到那碗排骨汤。/ 汤是凉的,浮着凝固的油。/ 我的心更冰冷了。(同一件事掰成三句,读者看三遍)

**画面分段**:揉进不等于一段到底。按新动作 / 新物件 / 新信息 / 新对话 / 视线转移断段,不按维度断段。一段承载一个镜头拍点或一条连续推理 / 氛围 / 情绪链。

---

## 11. 疏密分配 + 一动一静

**疏密**:不要每个 beat 一样长一样细(平均用力是 AI 腔的根)。

| beat 类型 | 笔墨 |
|---|---|
| 爽点 / 打脸 / 反转 / 情绪高潮 | 密(详写,感知动作对话铺满,慢镜头逐拍) |
| 过场 / 赶路 / 信息交代 / 时间跳转 | 疏(1-2 句带过,不展开三维度) |
| 铺垫 / 日常 / 关系升温 | 中(挑一两个有代入感的细节写实,其余略) |

**动静有起伏**:整篇要有推进与停顿,但不要求动作冲击后固定接一段安静感知。停顿可以是信息悬置、对话空白、等待或环境压力,不用微动作填格。避免长时间全动造成疲劳,也避免长时间全静造成拖沓。

**主语节奏**:段首 / 场景切换 / 多人同场用主角名建立主语;同一动作链内用代词 / 省略流动;关键转折再点名。避免连续多句都以同一主角名开头。

---

## 12. 开头事件密度

前 100 字 ≥ 3 个事件。删掉所有背景介绍(「她是 XX 的女孩」「在古代 XX 年间」),第 1 句就是事件 / 对话 / 信息炸弹,不是描述。

**开篇第一段必须砸下一个可量化、能截图的「命级 / 羞辱级」赌注**——救命钱、被打掉的孩子、当众社死的实锤——三秒内把命 / 钱 / 孩子砸到脸上,不要慢热内心铺垫、不要单点反复抒情。赌注与细节要有**不可替换的辨识度**(具体到独特的数字 / 物件),别写谁都能套、换个名字就通用的桥段。

- 高密度(《闻笙有两意》原文开头):老公怕我打搅他跟小秘书看演唱会,将我锁在家里。突发火灾,我打电话给他求救,只换来他一句:「能不能大度点,不就是拿你两张演唱会门票吗?」——三句砸下锁人、火灾、冷漠拒绝三件事。
- 低密度(要避免):把开头花在「她是谁、家世如何、那天天气怎样」上慢慢铺。

---

## 配套通用技法(这些文件保留,按需取用)

- 对话权力博弈、潜台词、弹幕递进:见 `dialogue-mastery.md`。
- 反转类型、嵌套反转、打脸节奏:见 `short-reversal.md`。
- 章首 / 章尾钩子模板:见 `hooks-chapter.md` / `hooks-paragraph.md` / `short-suspense.md`。
- 情感三板斧(羁绊铺设 / 情感撕裂 / 余韵钝痛):见 `emotional-methods.md`。
references/short-deslop.md
# 短篇去 AI 味

> 短篇专属去 AI 味规则。服务番茄 / 七猫 / 黑岩 / 盐选通俗短篇,不是传统文学,更不是长篇。
> 长篇那套去 AI 规则会误删短篇的卖点,别拿长篇 anti-ai 来删短篇。本文件是短篇的口径。

---

## 根本立场:短篇是情绪产品

短篇靠直白共鸣留人,不靠克制留白。短篇去 AI 味只做一件事:**删空泛的情绪总结句,留有具体反应支撑的情绪。**

长篇会禁、短篇要留的三类(删了就废掉卖点):

| 长篇禁 | 短篇留 | 短篇只删 |
|---|---|---|
| 直接写情绪词(心如死灰 / 眼泪不争气) | 留:这是招牌句 | 删:后面没有任何具体反应的情绪总结句(一丝悲伤涌上心头) |
| 第一人称审判句 / 剧透 / 火葬场预告 | 留:在场叙述是爽感来源 | 删:和角色情绪无关、纯交代的作者讲解 |
| 审判金句 / 心死式章尾 | 留:读者会截图传播 | 删:空洞哲理升华(人生就是这样 / 她终于明白了) |

判据都一样:**这句带不带主角此刻的主观情绪?** 带就留,中立客观就删。

---

## 一、情绪怎么写:直写可以,但后面要接一个场景里的具体动作

短篇不禁直接写情绪词(心如死灰、心如刀绞、几欲作呕、眼泪不争气都行,真实番茄用得很密)。**正面怎么写、真实原文范例,见 `short-craft.md` 第 2 节。** 这里只管删什么。

删两种:

1. 后面接不上任何具体东西、单独出现的情绪总结句:「一丝悲伤涌上心头」「感到深深的绝望」「仿佛整个世界失去了颜色」。
2. 通用生理反应单独凑数:「指甲掐进掌心」「心猛地一沉」「鼻头一酸」这类哪篇都能套,单写就空——要么接到这个场景此刻的具体物件 / 动作上(凉掉的排骨汤、摔碎的玉佩),要么删。

一个情绪写一次就往下走,不要写完再补一句解释它多疼。

---

## 二、别写成有套路、太工整、太漂亮的

句子里不写空话之后,还有一层:整篇别写得像套了模板、收得太整齐。这一层最容易在「写得很干净」的稿子上露馅。三点:

- **题材包的招式是可选项,不是固定流程。** 写多篇时,每篇用不同的招式、不同的开头和结尾;不要把上一篇的写法换个人名再写一遍。
- **章节结尾不要为了工整去收口。** 不要凑对仗、凑排比、凑一句押韵或抖机灵的总结。结尾留一个具体的悬念或情绪就行,不整齐没关系。
- **情绪该强就强,可以直白、可以不均匀、可以稍微啰嗦。** 通篇都很克制、每段都收得整整齐齐漂漂亮亮,读起来反而假。该炸的地方就放开写。

---

## 三、叙述姿态:在场叙述不是 AI 味

短篇叙述者可以跳出当下替读者定性、剧透、预告火葬场。这是爽感来源。长篇 anti-ai 那套「删上帝视角 / 删剧透 / 删替读者下结论」对短篇不适用。

| 留(带主角主观情绪) | 删(中立、和角色情绪无关的作者讲解) |
|---|---|
| 主观审判句:原来我以为的爱只是一场笑话 | 纯交代背景的作者解说 |
| 复仇者剧透:他们不知道,我已经是西域摄政王 | 客观描述无关人物的行动 |
| 火葬场预告:我还不知道,他正发疯似的找我 / 可后来他后悔了 | 论文式因果讲解:之所以…是因为… |

判据:这句跳出当下,是带主角主观情绪的审判 / 预告,还是中立的作者讲解?前者留,后者删。

**短篇去 AI 边界补充**:
- 短篇也要处理“作者解释总结 / 意义尾巴”,但只删**中立、无情绪、无卖点**的讲解;带第一人称主观审判、火葬场预告或心死判词的句子,按题材卖点保留或压短。
- “白话但不注水”同样适用:不要连续堆精致戏剧反应(头皮发紧、眼皮一跳、心口一沉、胃里翻涌)来替代剧情推进;能写普通动作、普通感觉和具体处境,就别把每个反应都写成漂亮短语。
- 已有手机、聊天记录、公告、账单、病历、证据截图等信息,优先保留为主角看见/保存/读错/转发的场内载体;不要改成叙述者替读者解释规则。
- 任务卡点只在本来就有“要办的事”时使用,并且必须加重羞辱、证据、反击或误会;不要为了自然感或字数硬加流程。

---

## 四、章尾:审判金句 / 火葬场预告是卖点

短篇收尾不做文学升华,但审判金句、心死余韵、火葬场预告要留——读者会截图传播。

| 留 | 删 |
|---|---|
| 审判 / 反讽金句:迟来的深情比草还贱 / 晏家,咱们的账慢慢算 | 空洞哲理升华:人生就是这样 / 她终于明白了 |
| 火葬场预告:他后悔疯了,可我已经无所谓 | 总结性感悟:这一夜,注定无人入眠 |
| 漠视收尾:那是他们的故事了,与我再无瓜葛 | 和剧情无关、只为点题的对仗金句 |
| 心死定格:那一刻,我的心彻底死了 | 升华式空感叹:从此她的人生翻开了崭新一页 |

区分:句子里有没有指向「这一件具体的事 / 这个人」?有就是审判 / 预告,留;只剩谁都能套的人生道理,删。

---

## 五、三连排比:有内容留,凑数砍

短篇堆苦难常用三连(「在暴雨夜、在手术室门口、在我父亲的葬礼上」)。

- 留:每一条都带新信息、堆出苦难分量。
- 删:三句说的是同一件事,只为凑「三个一组」显完整。

判据:删掉其中一条,信息会不会少?会就留,不会就砍到最狠那条。

---

## 六、真·AI 腔黑名单(短篇也照删)

这些和情绪烈度无关,是纯粹的 AI 套话,短篇也删:

| 类别 | 词 / 句 | 处理 |
|---|---|---|
| 套话动作 | 映入眼帘 / 只见 / 此时此刻 | 删 |
| 心理标签 | 心中暗道 / 不由自主 | 删,或用动作展示 |
| 对话标签腔 | 沉声道 / 淡淡地说 / 嘴角微扬 / 目光如炬 | 换成具体动作或表情 |
| 论文体 | 不难看出 / 由此可见 / 综上所述 / 事实上 | 删 |
| 书面连词泛滥 | 于是乎 / 与此同时 / 诚然 / 从而 / 因而 | 口语化或删 |
| 弱化副词扎堆 | 微微 / 淡淡 / 缓缓 / 轻轻(每千字超过 3 个) | 删到 3 个以内 |
| 万能比喻 | 像潮水般 / 如闪电般 / 仿佛春风 | 删,或换生活化比喻 |
| 空泛情绪总结 | 一丝 X 涌上心头 / 感到深深的 X / 一股 X 笼罩全身 | 删,或按第一节补一个具体反应 |

> 注意:删的是没有具体反应的套话。情绪词本身(心如死灰)不在黑名单里——后面接了具体反应就是卖点,别误删。

---

## 七、标点

正文统一**不出现** `……` / `——` / `—` / `--`(去 AI 脚本与 hook 强制)。停顿、哽咽、没说完、留白,一律用短句、句号、逗号、动作、换行。题材包范例里若出现 `……`,只是示意,落到正文按本条改写。

---

## 短篇去 AI 自检清单(写完每节逐项扫)

- [ ] **每个情绪词后面都接了具体反应**(身体反应 / 动作物件 / 处境判断),没有就是空话,补一个或删
- [ ] **没有空泛情绪总结句**(一丝 X 涌上心头 / 感到深深的 X / 仿佛世界失去颜色)
- [ ] **一个情绪只写一次**,没有写完再补一句解释它多疼
- [ ] **在场叙述保留**:第一人称审判句 / 剧透 / 火葬场预告在,没误删
- [ ] **没有中立的作者讲解**(和角色情绪无关、纯交代的句子)
- [ ] **章尾有钩或有判**:审判金句 / 火葬场预告 / 心死余韵在;没有空洞哲理升华
- [ ] **章尾没为工整收口**:没凑对仗 / 排比 / 押韵 / 抖机灵的总结
- [ ] **每篇写法不雷同**:招式、开头、结尾和上一篇不一样
- [ ] **三连排比有内容**:每条带新信息;凑数的砍掉
- [ ] **AI 套话清零**:无映入眼帘 / 心中暗道 / 沉声道 / 淡淡地说 / 嘴角微扬 / 不由自主 / 目光如炬
- [ ] **无论文体 / 书面连词泛滥**:无不难看出 / 由此可见 / 综上所述 / 于是乎 / 与此同时 / 诚然
- [ ] **弱化副词 ≤ 3/千字**:微微 / 淡淡 / 缓缓 / 轻轻
- [ ] **正文无禁用标点**:无 `……` / `——` / `—` / `--`
- [ ] **对话口语化**:无书面腔,不同角色语气可区分
references/short-format.md
# 短篇正文格式

> 写正文前必读。只管交付排版:章节标记、段落形态、对话、标点、元信息禁令。
> 写法(情绪外化、钩子密度、反转、收尾)见 `short-craft.md` 与题材包,不在此重复。
> 这是当前仓库默认交付格式;用户或目标平台另有明确要求时,以用户 / 平台要求覆盖。

---

## 章节标记

全文统一一种,不混用。

| 格式 | 平台适用 | 示例 |
|------|----------|------|
| `###1.` | 短篇默认 | `###1.` `###2.` `###3.` |
| `###第N章` | 番茄 / 七猫 / 古言 | `###第一章` `###第二章` |
| `1.`(纯数字,无 `###`) | 知乎盐言 | `1.` `2.` `3.` |

短篇推荐 `###1.` 或纯数字,简洁。番茄系长一点的章用 `###第N章`。

---

## 段落:两套平台模板

短篇正文形态按平台分两套。**不设统一的「每段 ≤ 3 句」「800-1500 字 / 节」硬线**,那是长篇迁移过来的产物,会把短篇切成一样长的提纲。先认平台底色,再按 `short-craft.md` 的疏密原则分配字数。

### 黑岩:一句一段,留白快读

- 一句一段为主,靠频繁断行制造下坠和停顿感;段与段之间仍只有一个换行符。
- 节奏密,每节体量小,节尾必留钩。
- 单句成段是常态,不要为了「凑够字数」把句子焊回长段。

模板示意:

```
###1.

我们家有个规矩,每年清明节都要拜"福祸盘"。
一个盘子,一半装着山珍海味,一半盛着清汤寡水。
哥哥抽中了肉,爸妈奖励他一个大红包。
轮到我,我再次抽中了那碗清汤。
```

### 番茄 / 七猫:短段为底色,叙述交织对话

- 短段仍是手机阅读的底色,但**允许多句回溯叙述段和对话交织**:一段里可以有内心独白、回忆补刀、动作,再接对话。
- 古言 / 世情可写更长的叙述段,承载完整的情绪链和场景铺陈。
- 爽点、打脸、反转处压短(可单句成段);过场、信息交代一句带过。

模板示意:

```
###第一章

我把热汤端到他面前的时候,他正低头看手机,屏幕亮着白月光的名字。三年了,我以为熬过了最难的日子,原来熬的只是我自己。
"以后这种事不用你做。"他头也没抬,"她不喜欢闻油烟味。"
我端着碗的手顿了一下。
```

### 两套共同的硬约束(所有平台)

- 正文相邻段落之间**只允许一个换行符 `\n`**,不得出现空行或连续换行 `\n\n`(紧密排列)。章节标记前后另算。
- **无缩进**:不用 `  `(全角空格)或半角空格缩进,平台渲染器自行处理。
- **禁正文段落 Markdown**:除统一的小节 / 章节标记(如 `###1.`),正文段落里不用加粗 `**`、斜体 `*`、标题 `#`、分隔线 `---` 等。
- 长短交错,忌通篇同长度。把每段切成同一字数,正是 AI 腔来源。

---

## 对话

### 引号

| 优先级 | 格式 | 适用 |
|--------|------|------|
| 首选 | `"说话内容"`(半角) | 短篇默认、番茄、红果、黑岩 |
| 平台 / 项目指定 | `「说话内容」` | 知乎盐言、部分古言、用户指定 |

**默认用 `""`**;用户或平台指定盐言风格时用 `「」`,不要把 `「」` 当错误改掉。

### 规则

1. 对话**独立成行**,不嵌在叙述段落里。
2. 用动作代替对话标签:少用「他说」「她道」「他笑了笑说」,改成动作 beat + 对话;普通的「说」按语境可留。
3. 两人连续对话省略标签,靠内容区分说话人。

正确:

```
她把杯子放下。
"你走吧。"
他没有动。
"我说,你走吧。"
```

错误:

```
她把杯子放下,说道:"你走吧。"他没有动,她又说:"我说,你走吧。"
```

---

## 字数统计

每节写完、全文交付前都要用机器统计,不要模型估算。

```bash
for PYBIN in python3 python py; do "$PYBIN" -c "" 2>/dev/null && break; done
"$PYBIN" -c "from pathlib import Path; print(len(Path('正文.md').read_text(encoding='utf-8')))"
```

不要直接调 `python3`:Windows 上它会落到 Microsoft Store 占位程序并以 exit 49 静默失败,上面的探测按 `python3→python→py` 选出真正可用的解释器。`wc -m` 只作 macOS / Linux 备选,禁止 `wc -c`(那是字节数)。全文交付口径以 `scripts/check-delivery-contract.js` 的非空白字符为准。没有 Bash / Python 权限时必须声明“未完成机器字数验证”,按行数速算作临时估计,不得声称已通过硬验证。

---

## 标点硬约束

**本仓库正文统一不出现 `……` / `——` / `—` / `--`**(去 AI 脚本与 hook 强制收口,项目级红线)。叙述、对话、心理描写一律适用,无对话例外。

停顿、哽咽、未尽、被打断、留白,一律用以下手段实现:

- 短句、句号、逗号断开。
- 动作 beat(「她顿了一下」「他攥紧了拳」)。
- 换行 / 单句成段。

其余标点跟语气走:

| 语气 / 功能 | 标点 | 防线 |
|---|---|---|
| 压迫 / 冷静 / 克制 | 短句、逗号、句号,必要时冒号压判断落点 | 不人工加感叹号,也不通篇句号化 |
| 质问 / 反问 | 问号 + 短促追问 | 别每句都以 `?` 结尾 |
| 爆发 / 打脸 | 真爆点用 1 个感叹号,连续爆发最多 1-2 处 | 禁 `!!!`、禁整段喊叫式感叹 |
| 信息揭示 / 判断落点 | 冒号、单句成段制造落点 | 别写论文式分号串 |

题材包范例里若出现 `……`,只为示意手法,落到正文按本条改写。

---

## 禁正文混入章节元信息

章节号、写作工程词只允许出现在标题 / 小节标记 / 文件名 / 追踪记录里。正文叙述、对话、心理描写中**不得出现**:

```
第[一二三四五六七八九十百千万两0-9]+章 | 上一章 | 上章 | 前一章 | 本章 | 这一章 | 前文 | 后文 | 伏笔 | 细纲 | 读者
```

要改成角色能感知的事件锚点或相对时间。例:「比第一章那三秒开火更疼」改成「比那三秒开火更疼」。

例外:角色在故事世界内真实阅读 / 讨论「第 X 章」文本,或角色真实身为作者 / 读者并谈论读者身份时,可保留。

---

## 平台覆盖表

| 平台 | 章节标记 | 对话格式 | 段落底色 | 特殊要求 |
|------|----------|----------|----------|----------|
| 知乎盐言 | `1.`(纯数字) | `「」` | 短段 + 叙述交织 | 导语需单独标注 |
| 番茄 | `###第N章` | `""` | 短段为底色,允许叙述交织对话 | 首段需有吸引力 |
| 红果 | `###1.` | `""` | 短段为底色 | 无 |
| 黑岩 | `###1.` | `""` | 一句一段,多换行留白 | 节尾必留钩 |

用户未指定平台时,默认短篇通用格式:`###1.` + `""` + 番茄系短段底色。

---

## 写完每段快速检查

- [ ] 段落形态符合目标平台模板(黑岩一句一段 / 番茄系短段交织叙述)?没把它切成同一字数的提纲?
- [ ] 相邻段落之间只有一个 `\n`,没有空行 / `\n\n`?
- [ ] 无缩进(没有 `  ` 或空格)?
- [ ] 标题 / 小节标记之外,正文段落没用 `**` `*` `#` `---` 等 Markdown?
- [ ] 对话独立成行,引号风格符合平台(盐言 `「」` 合法),对话标签没机械化?
- [ ] 正文(含对话)里没有 `……` / `——` / `—` / `--`,停顿改用短句 / 动作 / 换行?
- [ ] 标点跟语气走:没通篇句号化,没乱撒 `?` / `!`?
- [ ] 正文没混入 `第N章` / `上一章` / `本章` / `前文` / `后文` / `伏笔` / `细纲` / `读者` 这类元信息词(故事内真实阅读 / 真实读者身份语境除外)?
references/short-prose-quality.md

# 短篇成稿质量检查清单

## 目录

- [一、通用检查](#一通用检查)
- [三、短篇专项](#三短篇专项)

## 一、通用检查

### 章节结构
- [ ] 开头有钩子(不是天气/风景/日常开场)
- [ ] 中段有推进(有事件发生)
- [ ] 局势有变化(读完这章,世界跟之前不一样了)
- [ ] 结尾落在变化上(不是总结)

### 开篇检查(前 300-500 字)
- [ ] 有钩子,能抓住注意力
- [ ] 不从天气/风景/日常开始
- [ ] 主角快速出场
- [ ] 卖点或危机可见

### 章节推进
- [ ] 有核心事件
- [ ] 局势有变化
- [ ] 不是水字数(删掉这章会影响理解吗?不会 = 水了)
- [ ] 任务卡点有用:卡住之后是否带来信息、情绪、关系、代价或伏笔变化?
- [ ] 推进了主线/关系/设定中的至少一项

### 信息传递
- [ ] 没有大段设定说明文
- [ ] 信息跟着冲突走(通过事件传递设定)
- [ ] 设定量可控(一章不超 3 个新概念)

### 场景检查
- [ ] 场景有目标(人物要什么)
- [ ] 场景有阻碍(什么挡着)
- [ ] 场景有变化(结束后跟之前不同)
- [ ] 人物在做事情,不是在感觉事情
- [ ] 没有可删除的段落

### 章尾
- [ ] 结尾落在变化上
- [ ] 有危机/决定/发现/反转中的至少一个
- [ ] 不是总结式结尾
- [ ] 拉住读者翻下一页

### 语言
- [ ] 没有空洞的抒情段落
- [ ] 没有连续多段同一情绪
- [ ] 对话符合人物身份(不同人说话方式不同)
- [ ] 情绪通过动作落地(不是直接说"他很难过")

### 连载连续性
- [ ] 没有遗忘之前的承诺/伏笔
- [ ] 没有突然塞入大量新设定
- [ ] 伏笔有推进
- [ ] 故事引擎还在运转

### 水(filler)检测
以下信号出现 = 可能水了:
- 全章对话没有任何新信息
- 同一个情绪写了 3 段以上
- 场景描写超过 500 字但不推进剧情
- 角色回忆之前发生的事但没有任何新视角
- 连续 2 章以上没有冲突
- 删掉无损的流程细节 / 任务卡点 = 水;压缩或删除

---

### 五维评分标准

每个维度 0-100 分,根据评分结果选择精修策略。

### 维度 1:核心一致度
检查:关键冲突、关键行动、人物动机是否前后一致。

| 问题 | 严重度 | 修复 |
|------|--------|------|
| 人物动机突然改变无铺垫 | critical | 补充动机转变的触发事件 |
| 核心冲突前后不一致 | high | 回溯修改冲突设定 |
| 关键行动与人物性格矛盾 | high | 调整行动或补充解释 |
| 次要矛盾遗忘 | medium | 回收或弱化 |

### 维度 2:表层重写度
检查:句式与措辞是否足够原创,避免套路化表达。

| 问题 | 严重度 | 修复 |
|------|--------|------|
| 照搬原句导致语气不自然 | medium | 只在确有 AI 腔时改写,正常原句可保留 |
| 大量使用 AI 标志词 | high | 替换为具体描述 |
| 同一句式重复出现 | medium | 变换表达方式 |
| 描写过于文学化(辞藻堆砌、书面腔、比喻成串) | medium | 改为口语化/动作化;动作化不是切成三五字短句串,改写后叙述仍以逗号长句为主 |

### 维度 3:格式一致度
检查:段落是否按戏剧单元/镜头自然断开,主语/角色名节奏是否自然,开头结尾格式是否统一。

| 问题 | 严重度 | 修复 |
|------|--------|------|
| 机械按字数切段或主语过密 | medium | 按戏剧单元重排段落,段首点名、段中代词/省略、关键转折再点名 |
| 章节字数偏离目标 | **high** | 写作/大纲修复时先回到细纲补足计划内情节点,再展开;去AI味已有正文时不得新增剧情 |
| 格式混乱(对话/描写不统一)| low | 统一格式 |

### 维度 4:可读性
检查:是否有啰嗦、AI 腔、空泛总结、套路修辞。

| AI 腔特征 | 如何识别 | 如何修复 |
|-----------|----------|----------|
| 空泛总结 | 「他终于明白了」「一切尽在不言中」 | 删掉,用行动代替 |
| 套路修辞 | 「命运仿佛在和他开玩笑」 | 删掉或换成具体描述 |
| 比喻堆叠 | 连续多处「像/好像/仿佛/如同」,每个画面都靠比喻解释 | 只留最有叙事功能的少数比喻,其余改成动作、物件、声音或后果 |
| 情绪标签 | 「他感到一阵悲伤」 | 改为行为表现 |
| 心理描写空转 | 内心独白无新信息、重复同一情绪或复述读者已知 | 只压缩空转部分;带新信息、决断或情绪转折的独白不按句数压缩 |

### 维度 5:逻辑连贯
检查:句间/段间是否通顺,有无设定冲突。

| 问题 | 严重度 | 修复 |
|------|--------|------|
| 设定前后矛盾 | critical | 查找并统一设定 |
| 时间线错误 | high | 标记时间线并修正 |
| 角色信息不一致 | high | 建立角色档案对照 |
| 因果链断裂 | medium | 补充过渡 |

### 精修策略

根据五维评分结果,选择精修策略:

| 主要问题 | 策略 | 说明 |
|----------|------|------|
| 核心一致度低 | rewrite | 围绕核心冲突重写相关段落 |
| 字数超标 | compress | 删减不推动剧情的内容 |
| 无意义卡点 | compress | 保留卡出的变化,压掉流程 |
| AI 腔重 | de_ai | 替换禁用词、改写句式 |
| 小问题多 | polish | 打磨语言细节 |

---

## 三、短篇专项

### 虐爽节奏检查

短篇特有的检查项——虐点和爽点的分布是否合理:

```
理想分布:虐1 → 虐2 → 虐3 → 爽1(小) → 虐4 → 爽2(大)
禁忌分布:虐1 → 虐2 → 虐3 → 虐4 → 虐5(无爽点,读者流失)
禁忌分布:爽1 → 爽2 → 爽3 → 爽4(无铺垫,爽感疲劳)
```

检查规则:
- 每 300 字至少 1 个小冲突
- 虐点间隔不超过全文的 30%
- 最大爽点必须出现在全文的 70-85% 位置
- 结尾必须有情绪落点

### 对话密度检查

| 指标 | 合格标准 | 警戒线 |
|------|----------|--------|
| 对话占全文比 | 45-65% | <30%(太干)或>75%(太水) |
| 每章对话条数 | 12-18条/千字 | <8条(缺交互)或>25条(碎片化) |
| 伤人性对话占比 | 35-45%(虐文)/ 20-30%(爽文) | 虐文<20%(不够痛)/ 爽文>40%(太压抑) |

### 主角冷静度检查(打脸/复仇文专用)

| 检查项 | 标准 |
|--------|------|
| 主角是否有标志性冷静动作? | 必须有(端水杯/整理西装/转笔等) |
| 主角是否有情绪失控场面? | 复仇文最多1次,且必须在前30% |
| 反派是否比主角更歇斯底里? | 必须是——反差是爽感来源 |
| 主角台词是否短于反派? | 平均短30-50%——越短越有力 |
| 是否有"审判式对话"? | 至少2处(主角提问→对方自爆) |

### 证据链完整性检查(复仇/打脸文专用)

| 检查项 | 标准 |
|--------|------|
| 证据是否分章释放? | 至少分3次揭露,不能一次全给 |
| 每个证据是否有铺垫? | 前文必须埋过线索 |
| 反派是否每次都先得意再被打脸? | 必须是——先扬后抑 |
| 最终证据是否最致命? | 最后一个证据要改变全局认知 |
| 是否有"定时炸弹"证据? | 至少1个主角提前布局的证据 |

### 追妻/情绪流雷区检查(女频情绪流专用)

| 雷区 | 自查 |
|------|------|
| 女主心死后还在照顾男主? | 拉扯清零——心死后必须彻底抽离,不看不听不在意不照顾 |
| 男主只渣不苏? | 没颜值/能力/地位的资本,读者只觉女主眼瞎,不会期待追回 |
| 回忆杀冗长? | 大段回忆打断当下情绪,必须短而精、与当下场景一一对应 |
| 女主觉醒太晚? | 全程虐到底读者弃文——最迟付费卡点前女主须彻底心死离开 |
| 男主醒悟太早? | 女主没走就后悔,虐不透、火葬场没力度——须等女主抽离后再后知后觉 |
| 轻易破镜重圆? | 伤太深却随便原谅,前期的虐全白写 |
| 反派降智无动机? | 坏得没理由立不住,给个真实动机反而更戳人 |

### 毒点/代入感/震惊/开头/结尾/情绪/期待感速查

> 详细的毒点排查见 `genre-writing-techniques.md`「毒点排查核心」;AI 腔自检见 `short-deslop.md`。

| 检查项 | 标准 |
|--------|------|
| 爽文不爽 | 金手指效果必须展示清楚 |
| 压制无目的 | 每次压制必须服务于后续爆发 |
| 反派结局与主角无关 | 反派之死必须因主角行动导致 |
| 经济/战力崩坏 | 设定前后一致,以普通人为锚点 |
| 女主/女配降智 | 人设正常可写,老套路别写 |
| 读者期待长时间不满足 | 适时满足或引入新期待 |
| 主角行为不可理解 | 必须能理解、能共鸣 |
| 氛围被突兀梗破坏 | 氛围 > 突兀梗(乐子文除外) |
| 钩子不回收 | 钩子不回收 = 烂尾 |
| 震惊分层 | 点→网→深度,不能只写点震惊 |
| 震惊阶梯递进 | 阶梯式上升,非直线 |
| 震惊有广度 | 关系网都要有反应 |
| 前50字有冲突/异常 | 不能是背景铺垫 |
| 前100字知核心矛盾 | 必须知道 |
| 开头情绪强度 | ≥7(1-10) |
| 结尾是具体的 | 动作/对话/画面,禁止总结/反思 |
| 结尾有余韵 | 读者还想往下看 |
| 结尾情绪强度 | 虐≥8,爽≥7,治愈≥6 |
| 情绪对等释放 | 虐多少补多少,反派结局关联主角 |
| 期待管理 | 两长一短不断,核心卖点贯穿 |
references/short-reversal.md
# 短篇反转设计
> 操作手册:反转类型、嵌套反转、误导技巧、打脸节奏。按决策路由选用,按设置/揭示步骤执行,用自检清单验收。
> 反转类型枚举与拆文 `_meta.json.reversal_type` 字段一致:视角/身份/动机/时间线/信息/认知/无反转。

## 决策路由

| 你要设计什么 | 用哪种反转 | 关键操作 |
|-------------|-----------|---------|
| 角色不是你以为的人 | 身份反转 | 埋3处行为细节暗示 |
| 真相从另一个角度完全不同 | 视角反转 | 引入另一角色视角打破认知 |
| 角色做某事的原因不是你以为的 | 动机反转 | 高压场景中二选一暴露真动机 |
| 时间顺序跟读者理解不同 | 时间线反转 | 不撒谎只调叙述顺序 |
| 读者掌握了错误信息 | 信息反转 | 新证据直接否定旧"事实" |
| 读者对整个角色/关系的理解被颠覆 | 认知反转 | 全程铺感情色彩,结尾翻转色彩 |
| 甜宠/喜剧/报应型,本就没有反转 | 无反转 | 走报应兑现或甜度递进,不硬塞反转 |
| 要叠多层反转 | 嵌套反转 | 主次分明,间隔递减 |

---

## 反转类型

### 1. 身份反转

**触发条件**:角色真实身份与读者认知不同。

**设置步骤**:
1. 确定表面身份和隐藏身份
2. 通过行为细节泄露(禁止用叙述者直接说明)
3. 回看至少埋3处暗示

**揭示步骤**:
1. 用场景同时揭示身份和动机
2. 禁止让角色自说"其实我是XX"
3. 选在读者最不期待揭示的时机

| 表面身份 | 隐藏身份 | 铺垫线索 |
|----------|----------|----------|
| 乖巧继女 | 幕后操控者 | 总在关键时刻「恰好」不在场 |
| 热心邻居 | 伤害主角的人 | 对主角家里的布局太熟悉了 |
| 死去的恋人 | 还活着 | 「遗物」里有一件不该存在的东西 |
| 陌生人 | 主角失忆前的至亲 | 叫出了主角只有至亲才知道的小名 |

示例铺排:
```
铺垫1(5%):邻居帮修水管,对水管走向异常熟悉
铺垫2(25%):邻居「随口」提到主角家灯每天几点关
铺垫3(50%):梦到有人站床前,醒来闻到烟味——邻居抽烟
揭示(75%):监控里,每晚站床前的人穿邻居那件灰色外套
```

### 2. 视角反转

**触发条件**:叙事者视角是片面的,真相从另一个角度完全不同。

**设置步骤**:
1. 确保所有叙述都是事实,但不是全部事实
2. 叙述者解读引导读者走错方向
3. 关键信息被「合理地」跳过或淡化
4. 叙述者自己也真诚地相信这套叙事——他不是在骗读者,他自己也被骗了

**揭示步骤**:
1. 引入另一角色视角/证词
2. 展示主角「没看到」的场景
3. 用证据打破认知

| 主角视角 | 真实视角 | 关键差异 |
|----------|----------|----------|
| 丈夫出轨对不起我 | 我才是被蒙在鼓里的第三者 | 时间线上的空白 |
| 同事在排挤我 | 我的「好心」一直在伤害别人 | 别人视角下我的行为 |
| 我在拯救这段关系 | 我在控制对方 | 对方的反应其实是在自保 |
| 我是受害者 | 我是施害者 | 记忆被叙述者篡改了 |

**铁律**:叙述者必须真诚地相信自己的视角,禁止叙述者明知真相而故意误导读者。

### 3. 动机反转

**触发条件**:角色做某事的真正原因与读者以为的不同。

**设置步骤**:
1. 给角色一个「表面动机」让读者觉得合理
2. 真正动机藏在表面之下
3. 埋下行为细节与表面动机的微小矛盾(矛盾不要太大)

**揭示步骤**:
1. 制造高压场景迫使角色二选一
2. 表面动机指向选A,但角色选了B
3. 选择行为直接暴露真正动机

| 表面动机 | 真正动机 | 行为矛盾线索 |
|----------|----------|-------------|
| 想和前夫复婚 | 想拿回藏在婚房里的东西 | 从不关心他的生活,只关心那套房子 |
| 保护妹妹 | 嫉妒妹妹,想控制她 | 每次「保护」都让妹妹更孤立 |
| 追求公平正义 | 报个人私仇 | 目标人物恰好和当年的事有关 |
| 害怕失去朋友 | 害怕朋友发现自己背叛了她 | 每次朋友接近真相就制造新危机 |

### 4. 时间线反转

**触发条件**:故事的实际时间顺序与读者理解的顺序不同。

**设置步骤**:
1. 确定真实时间顺序和读者理解的顺序
2. 用叙述技巧让读者以为A在B之前,实际B在A之前
3. 不撒谎,只调整叙述顺序
4. 用时态、季节、物品新旧作为天然时间线索

**揭示步骤**:
1. 找一个不可能存在的时间矛盾
2. 或让角色提到「还没发生」的事
3. 或展示物品状态与时间线不符

| 读者以为的时间线 | 真实时间线 | 揭示方式 |
|------------------|-----------|----------|
| 丈夫丧妻后变质 | 妻子「死后」还活着 | 时间戳对不上的消息 |
| 姐姐在弟弟出事后崩溃 | 弟弟出事因姐姐先做了某事 | 医院记录上的时间 |
| 两人正在恋爱 | 这是一段回忆,已分开 | 「我当时不知道那是最后一次」 |
| 重生后第二天 | 重生前的最后一天循环 | 镜子里倒影和之前不一样 |

**铁律**:写完必须画时间线图自查。

### 5. 信息反转

**触发条件**:读者(和主角)掌握了错误的关键信息。

**设置步骤**:
1. 给读者一个来自可靠来源的「事实」
2. 所有人物基于此「事实」行动
3. 在后果中埋下矛盾证据

**揭示步骤**:
1. 新证据直接否定旧「事实」
2. 读者和主角同时发现被骗
3. 前面所有剧情获得新解读

| 错误信息 | 真实信息 | 揭示的连锁反应 |
|----------|----------|---------------|
| 遗嘱遗产给长子 | 遗嘱被调包,本给次子 | 长子所有「孝心」都是演戏 |
| 医生说孩子亲生 | DNA报告被篡改 | 配偶从受害者变加害者 |
| 朋友说看到他出轨 | 朋友在撒谎 | 朋友的「安慰」其实是操控 |
| 老板说提拔我 | 岗位已取消 | 老板画饼为让我多干活 |

**核心区别**:信息反转是认知层面的——你以为世界是A,其实是B。核心是「一条假信息」。

### 6. 认知反转

**触发条件**:读者对某个角色/关系的整体理解被颠覆;重点在整段感情色彩翻转,而非单条信息纠错。

**与信息反转的区别**:信息反转翻一条事实(遗嘱归属、亲子关系真伪);认知反转翻读者对整个角色/关系的感情判断(恨了全程的妈其实一直在护着她)。信息反转改事实答案,认知反转改"我对他的感觉"。

**设置步骤**:
1. 全程让读者积累一种感情色彩(这个妈狠心/这个丈夫凉薄/这个对手纯坏)
2. 每个负面场景都埋一个被忽略的反向细节(狠话之后多塞了钱、转身擦了眼泪)
3. 细节要小到读者当时不在意,回看才发现处处都是

**揭示步骤**:
1. 用一个场景或一件遗物,让所有负面行为同时获得反向解读
2. 不解释,让读者自己把全程重新过一遍
3. 揭示后情绪从"恨/怨"翻成"亏欠/意难平"

| 全程认知 | 翻转后认知 | 反向细节铺垫 |
|----------|-----------|-------------|
| 后妈刻薄赶我走 | 她赶我走是怕她仇家牵连我 | 每次骂完都往我包里塞钱 |
| 丈夫冷漠不管家 | 他在外面替我扛了所有债 | 深夜接的"应酬"电话都是催债 |
| 师父偏心打压我 | 他压我是知道我会被盯上 | 每次罚我都安排在别人看得见的地方 |

**追妻/世情主力反转**:这是追妻火葬场、世情最常用的反转——男主/婆婆/亲人全程被恨,结尾翻成"原来一直在爱/在护",意难平拉满。

### 7. 无反转

**触发条件**:甜宠、喜剧、反差萌、报应型复仇/重生——这类故事本就没有传统反转,硬塞反转反而毁节奏。

**爽感来源(替代反转)**:
- **报应兑现型**(复仇/重生爽文):爽点不在"真相翻转",在反派一步步走向毁灭、每条恶行精确对应一条报应(以彼之道还施彼身)。用报应设计表:恶行→报应→爽感来源。
- **甜度递进型**(甜宠/青春):爽点在关系一步步升温,反差萌反复"装失败"。全程正向情绪阶梯上升,不经历反转的负向回落。

**写法要点**:
1. 不为"显得有设计感"硬加反转
2. 报应型:每个报应要让读者等过、铺过,兑现时才解气
3. 甜宠型:升温节拍密集,每节给一个甜点/反差点

> 拆文时遇到此类,`reversal_type` 填「无反转」,`setup_clues`(反转铺垫)一项跳过阈值(见 analyze output-contract 的「structure_counts 数值校验」)。

---

## 嵌套反转

一篇短篇可叠 2-3 层反转,必须主次分明。

每层都按同一条可执行链设计:`铺垫事实 → 当时的错误解释 → 触发证据 → 真相揭示 → 行动/关系后果`。第一层必须是能暂时结案的可信阶段答案;第二层不仅推翻第一层,还要解释读者为什么曾相信表层答案。

### 双层嵌套

```
读者以为:A(铺垫阶段)
第一层反转:其实是 B!(中后段阶段答案)
第二层反转:B 仍不完整,其实是 C!(终局前揭示)
```

**执行要求**:
- 第一层反转必须足够有说服力,让读者接受B就是真相
- 第一层后给1-2段「消化时间」
- 第二层要能同时解释A和B,比第一层更震撼

示例:
```
铺垫:丈夫出轨了,妻子收集证据准备离婚
第一层:丈夫没出轨,那女人是他线人——他在调查妻子的公司
第二层:妻子公司确实有问题;举报人换成妻子自己,丈夫只是被引导发现
——她故意让丈夫发现,只有丈夫「不经意」揭发她才能拿到保险赔偿
```

### 三层嵌套(慎用)

**使用条件**:只有篇幅、线索容量和情绪结算空间都足够时才用;多数短篇优先把一至两层做透。

**执行要求**:
- 间隔通常递减,但按小节功能和结算欠账定位,不套固定全文百分比
- 第三层必须最短最狠——一句话就够
- 最后一层必须回答前面所有疑问
- 每层都要有情感冲击,不只是信息冲击
- 题材限制:只适合高智商悬疑、精心设计的复仇

---

## 反转时机

### 揭示位置判断

| 反转类型 | 揭示前必须积累什么 | 揭示后必须留下什么 |
|----------|-------------------|-------------------|
| 身份反转 | 表面身份成立且行为有裂缝 | 新身份带来的选择与关系后果 |
| 视角反转 | 单一视角已形成稳定误判 | 另一视角触发的新行动 |
| 动机反转 | 表面动机能解释大多数行为 | 真动机导致的关键选择 |
| 时间线反转 | 两套顺序都能回看成立 | 时间重排后的责任变化 |
| 信息反转 | 错误事实已真正影响决策 | 纠错后的连锁反应 |
| 认知反转 | 感情色彩已经积累 | 关系判断和情绪结算 |

### 时机原则

- 用功能而非固定百分比定位:先让误判参与至少一次重要决策,再揭示。
- 揭示不能晚到只剩解释;必须给行动、关系或代价留出结算空间。
- 双层反转间至少发生一次基于阶段答案的新行动,否则只是连续口供。
- 平台付费断点优先卡在未完成动作、证据归属变化或两难选择。先选一个主断点,再按载体需求选择少量支撑断点,不把每节都标成强钩子。

---

## 误导技巧

误导要引导读者自己走向错误结论,不能欺骗读者。

### 两大底层路径

- **加法路径(分层法)**:在真实信息上叠加假信息。线索既可用逻辑线A解释为A',也可用B解释为B'。A'是误导方向(常见故事),B'是真相(不常见故事)。误导读者以为是A'时,必须同时暗埋与B'相关的线索。
- **减法路径(信息残缺法)**:不添加假信息,隐藏关键信息的一部分。只让主角得知成功的部分。导致下行的最好不是主角主动行动获得的,可以让配角获得那半信息后交给主角时发生意外。

### 五种技巧速查

| 技巧 | 操作方法 | 示例 |
|------|---------|------|
| 选择性叙述 | 主角只关注某些信息,读者跟着走 | 主角在意丈夫和女同事暧昧,忽略「客户」的奇怪电话 |
| 情绪引导 | 用情绪场景引导判断 | 暖的父女戏→其实是愧疚,他做了对不起女儿的事 |
| 假线索(红鲱鱼) | 给可疑角色/事件吸引注意力 | 诡异邻居和主线无关,真正威胁是一直正常的人 |
| 刻板印象利用 | 利用社会认知偏见 | 强势婆婆其实是保护儿媳,她知道一些儿媳不知道的事 |
| 信息分层 | 真相同假信息混在一起 | 丈夫确实秘密联系人(真)→前女友(假)→亲妹妹(真) |

**红鲱鱼铁律**:红鲱鱼必须在故事里有自己的功能(推进情节/增加趣味),只是不是反转的答案。

---

## 反转自检清单

### 合理性
- [ ] 回看铺垫至少有3处暗示指向反转
- [ ] 反转不依赖巧合——角色选择推动了反转
- [ ] 没有引入前面完全没提过的新信息

### 冲击力
- [ ] 反转后情绪强度高于反转前
- [ ] 反转改变了读者对前面所有剧情的理解
- [ ] 反转让读者想翻回开头重看

### 公平性
- [ ] 读者有可能在反转前猜到(不是必须,但有可能)
- [ ] 没有对读者撒谎——只是没说出全部真相
- [ ] 揭示方式自然,不靠角色大段独白解释

### 节奏
- [ ] 反转揭示以一个集中场景完成,长度服从证据复杂度,不靠长篇口供拖延
- [ ] 揭示后有足够篇幅展示反转影响
- [ ] 反转后结尾在反转产生的情绪上收束

### 逐节落表

设计完成后,用一张表检查每节的 `功能 / 新证据或误判 / 主动者 / 节尾钩子 / 是否主付费点`。没有改变证据归属、嫌疑判断、关系立场或风险等级的节,应合并或改写;强钩子只标真正改变读者预期的节点。

---

## 常见反转错误

| 错误 | 问题 | 修正 |
|------|------|------|
| 天降反转 | 前面完全没铺垫 | 至少埋3条线索 |
| 解释过多 | 大段文字解释反转 | 用行动/场景让读者自己明白 |
| 反转太弱 | 读者早就猜到了 | 加误导或换反转类型 |
| 反转太多 | 3个以上反转堆在一起 | 砍到1-2个做到极致 |
| 反转无感 | 只改变信息没改变情绪 | 反转必须同时改变读者情感判断 |
| 反转作弊 | 引入前面不存在的信息 | 所有反转要素必须在前文中出现过 |

---

## 特殊反转技法

### 虚晃一枪反转法

先给出"应该不会发生"的预期让读者放松,然后意外以反转形式发生。类似鬼片推开门什么都没有->松口气->回头撞鬼。

### 超额收获反转

反转后收获超出预期,产生二次惊喜:
- 短篇:主角以为只解决一个问题,结果连带解决了隐藏的更大问题
- 网游文模式:Boss爆三样——立刻能用的 / 送人拉关系的 / 暂时用不到但一看很厉害的

### 情绪拉扯反转标准流程

1. 展示物品/能力强大,读者期待+1
2. 配角因信息差认为鸡肋,读者期待打脸+1
3. 展示反派特性恰好克制,读者期待+1
4. 配角拿更强装备打反派失败,期待+1
5. 众人看衰主角,期待继续+1
6. 主角秒杀,期待满足
7. 众人震惊,配角马后炮分析,期待满足
8. 新一轮收获,满足+新期待产生

---

## 打脸的深层节奏

### 三种打脸方式

| 方式 | 特点 | 适用场景 |
|------|------|----------|
| 主动挑衅->打脸 | 简单粗暴 | 小白文最常见 |
| 对手挑衅->被打脸 | 压主角同时给读者安全感和反击暗示 | 需要积蓄仇恨时 |
| 借他人之手打脸 | 支持者代为回击,无损主角形象 | 保持主角高逼格时 |

### 打脸节奏铁律

- 压抑不能太长——连输8场写3万字不行,改成连输4场+略写
- 压的同时必须给读者信心暗示(主角自信到自大:"我们还是联赛前四!")
- 比起主角被欺负,读者更厌恶主角自暴自弃
- 高潮部分要拉长,最大化利用(球迷反应、解说员、赛后跟进)
- 大高潮不要险胜——充分铺垫后要尽情碾压,干净利落的大胜

### 高潮间过渡

升级练功也有爽点,但大爽点落在升级后打脸,读者期待升级后的兑现。
references/short-suspense.md
# 短篇悬念编排

悬念构建、强度分级、多线周期、分层钩子、期待接力、震惊分层的完整操作指南。

---

## 决策路由表

| 你在写什么 | 用什么方法 |
|------------|------------|
| 设计悬念体系 | 主悬念/副悬念 + 强度5级分级 |
| 单节悬念 | 四种信息顺序模板 + 触发型分层钩子 |
| 全文悬念 | 期待接力法 + 震惊分层 |
| 判断悬念强度 | 强度5级分级表 |

使用方法:先看左列定位你的当前任务,再用右列指定的方法。

---

## 悬念构建核心法则

### 悬念的本质

读者心理预期出现两个及以上不同走向时,剧情就有了悬念——读者觉得都有可能发生。

### 悬念 vs 伏笔:区分清楚

| 维度 | 悬念 | 伏笔 |
|------|------|------|
| 目的 | 让读者猜测接下来会怎样 | 为后面的揭示埋下前期线索 |
| 位置 | 小节结尾/段落结尾 | 叙事中自然带出 |
| 揭示时机 | 短期内揭晓 | 长期后才揭示 |
| 情绪效果 | 紧张/好奇/期待 | 震惊/恍然大悟 |

判断依据:短期揭晓+紧张感 = 悬念;长期埋线+揭示时震惊 = 伏笔。两者经常配合使用,但不要混淆。

### 四种悬念信息顺序模板

| 类型 | 结构 | 适用场景 |
|------|------|----------|
| 直白剧情 | 提出疑问 → 公布答案 | 基础叙事 |
| 探索剧情 | 提出疑问 → 正常提示 → 公布答案 | 铺垫段 |
| 意外剧情 | 提出疑问 → 虚假提示 → 公布答案 | 反转段 |
| 意外+反转 | 提出疑问 → 虚假提示1 → 虚假对立提示2 → 公布答案 | 高潮段 |

操作要点:选择模板后,严格按结构顺序排列信息。虚假提示必须足够可信,否则读者不买账。

### 触发型钩子(分层钩子)

单节内多层递进悬念,按以下步骤操作:

```
第1层:展示初步成果 → 观众初步反应
第2层:揭示这还不是最终结果 → 观众期待升级
第3层:展示超出预期的元素 → 观众震惊
第4层:主角还能进一步提升 → 留下钩子,开启下一段
```

关键要求:每一层都必须有角色的反应来验证悬念的力度。没有角色反应 = 悬念落空。

---

## 悬念强度分级

| 等级 | 名称 | 效果 | 适用 |
|------|------|------|------|
| 1 | 微悬念 | 好奇 | 过渡节 |
| 2 | 小悬念 | 想看下一段 | 推进节 |
| 3 | 中悬念 | 想看下一节 | 关键节 |
| 4 | 大悬念 | 愿意付费继续 | 付费断点/爆发节 |
| 5 | 极悬念 | 必须知道真相 | 终局前 |

使用方法:写完每节后对照此表判断悬念等级。过渡节至少要达到1级,推进节至少2级,关键节至少3级;付费点应由未完成动作、身份/证据变化或两难选择形成,不靠空喊危险。

---

## 短篇悬念层级

| 层级 | 跨度 | 示例 |
|------|------|------|
| 即时钩子 | 当前段到下一段 | 一条证据、一句没说完的话 |
| 节间钩子 | 1-2 节 | 一次取证、关系异常或行动后果 |
| 全文主悬念 | 开篇到终局 | 谁在操盘、角色真正隐瞒了什么 |

全文保持一个主悬念,最多搭配一个副悬念。副悬念必须给主悬念提供证据、误导或情绪代价,不另开长篇式支线。

### 三段钩子设计(单节内)

按章节进度分配悬念节奏:

1. **种**(前 30%):埋下钩子种子
2. **养**(中 50%):逐步加压,让读者意识到不对劲
3. **收**(末 20%):引爆或延迟引爆

延迟引爆 = 比当前答案更强的问题,用于拉到下一节;终局前不得靠无关新谜题续命。

---

## 期待接力法

### 基本规则

- 确保读者脑中有三个好奇的东西:两长一短
- 长期待收回后变短期爆发,同时新的长期待已铺好
- 短篇中:一个主悬念 + 一个副悬念,结尾同时引爆

### 持续拉期待操作框架

1. 关键节结尾必须有至少一个未解的问题、未完成动作或关系变化;呼吸节可用弱钩子,不机械强卡。
2. 期待分两层运作:短期(下一节/付费点)与全文(终局真相或关系结算)。
3. 当短期问题解决时,答案必须推进全文主悬念,不能只换一个同级谜语。

### 不间断期待与钩子链

- 主角即将得到某样东西但还没得到时,读者期待感最高
- 在主角得到之前,必须套上另一个钩子
- 大期待(主线)+ 小期待(支线)来回穿插,一个勾着一个无限循环
- 任何时刻保持至少两条期待线并行运行

---

## 驱动力公式

**核心公式**:产生诉求 → 给予希望 → 努力解决 → 得偿所愿

| 阶段 | 操作 | 要点 |
|------|------|------|
| 产生诉求 | 制造不爽——低地位/困境/威胁/不曾拥有 | 读者看到不公 → "不该如此"的冲动 = 最根本驱动力 |
| 给予希望 | 展示金手指 = 改变的希望 | 清晰展示金手指 = 给读者看下去的动力 |
| 努力解决 | 不能太快得偿所愿 | 方式一:困境分层递进;方式二:解决一个 → 新困境升级 |
| 得偿所愿 | 爽点释放 | 困境层级越高、种类越不同 → 爽感越强 |

**"悬而未决"技巧**:持续的"未解决"状态 = 持续的关注和期待。设置信息差保持张力。

---

## 情绪折线与蓄力释放节奏

### 情绪折线

`铺垫(上行)→ 挫折(下行)→ 再铺垫(上行)→ 再挫折(下行)→ 爆发(大幅上行)`

操作规则:
- 上行可以多一些,下行要精准控制力度
- 下行太猛 = 节奏断裂(弃书),太轻 = 打不响
- 网文的"下行"用小挫折、小波折制造落差,避免让主角真憋屈

### 期待蓄力与释放的力度控制

- 蓄力 = 铺垫期待,释放 = 爽点爆发
- 蓄力太久 = 读者疲惫;不够 = 爽感不足;断裂 = 弃书
- 小期待不断(持续满足),大期待适度,每次释放后爽感要比上一次更强

### 道具能力展示的8步期待模板

按顺序执行:

1. 展示宝物功能强大(期待+1)
2. 配角因信息不足认为鸡肋(信息差,期待打脸+1)
3. 展示反派,宝物恰好克制反派(期待+1)
4. 配角拿更强装备打反派失败(期待主角出手+1)
5. 主角做针对性方案(期待+1)
6. 主角上场,众人不看好(期待继续+1)
7. 主角秒杀反派 → 众人震惊 → 鸡肋成神器(期待满足)
8. 新收获 + 新期待产生

---

## 震惊分层写法

### 震惊三层结构

从弱到强依次使用:

1. **点震惊**:一个人震惊了一下(最弱)
2. **网震惊**:震惊关系网——不只一个人震惊,周围人都有反应
3. **深度震惊**:多层震惊叠加——成就1震惊 → 成就2震惊 → 更厉害的成就3引爆震惊

### 关键原则

- 金手指对剧情的效果必须展示得清清楚楚
- 放了底牌,反派就要受到对应压制
- 该爽的时候不爽到位,观感上是很毒的

### 关系网震惊层级

| 层级 | 震惊对象 | 效果强度 | 后续价值 |
|------|----------|---------|---------|
| 陌生人震惊 | 路人、群演 | 弱 | 纯情绪满足,无后续 |
| 熟人震惊 | 有过互动的配角 | 中 | 可能产生态度变化 |
| 重要角色震惊 | 有后续戏份的角色 | 强 | 伴随信息展露、关系进展、拉起期待 |
| 高位者震惊 | 行业顶层人物、高阶位者 | 最强 | 侧面拉起读者对主角的期待 |

选择依据:根据该角色后续戏份决定震惊层级。有后续的角色震惊才有价值。

### 震惊递进的道具体现法

| 阶段 | 道具表现 | 震惊强度 |
|------|----------|----------|
| 成就1 | 对方将椅子把手捏出一道裂痕 | 轻 |
| 成就2 | 椅子满是裂纹 | 中 |
| 成就3 | 对方捏爆椅子把手 | 强 |

要点:道具变化比"他震惊了"有力一百倍。每次递进都要有明确的视觉/物理变化。

---

## 期待感设计的核心方法

### 多线运行法(画线法)

1. 一个剧情线一道大横线
2. 其他期待线在下面画线
3. 规划剧情节点
4. 落入正文时再详写

### 永留悬念大结构设计

- 大结构是"欺骗式的主线":贯穿全文的噱头,并不实际推进
- 每次看似砍了99%,但总剩1%变成新的100%,一直吊着读者

### 信息差运用

- 读者知道主角获得了强力物品但配角不知道——天然信息差产生期待
- 反派恰好被克制——期待叠加
- 别人拿更好装备却失败——期待再加倍
- 信息差抹平时 = 爽点爆发

### 读者预知法(提前告知大事件制造紧张)

提前告诉读者即将发生的大事件,读者知道但主角不知道,形成紧张感和期待感。"倒计时"变体:把事件变成不断逼近的倒计时,每隔1-2章放一小段进展。

### 底牌前置法

先展示主角底牌,再安排找事的冲突。读者知道主角有底牌但反派不知道。需要两对信息组合:**底牌 + 即将发生的冲突**。

---

## 拉期待手法速查(8大类36种)

| 类别 | 手法 | 说明 |
|------|------|------|
| 角色行为 | 持续伪装等待暴露 | 主角持续扮演某身份,期待暴露时刻 |
| 角色行为 | 非常规能力展示 | 主角用特殊能力解决,期待更大规模 |
| 角色行为 | 违反人设预期的行为 | 做出与形象不符的行为,制造惊喜 |
| 角色行为 | 展示部分实力留底牌 | 展现部分实力,期待全面爆发 |
| 时局/事件 | 突发意外 | 意外事件打破当前平静,迫使剧情加速 |
| 时局/事件 | 暗中蓄力等待时机 | 角色暗中蓄力,期待亮剑时刻 |
| 时局/事件 | 高风险中谋利 | 在危险中谋利,紧张感拉满 |
| 时局/事件 | 混乱中周旋 | 在混乱局面中巧妙保全或谋利 |
| 关系网扩展 | 跨势力角色串联 | 不同势力角色产生意外联系,扩大冲突面 |
| 关系网扩展 | 多方势力不同反应 | 多方势力对主角/事件不同反应 |
| 关系网扩展 | 名声扩散引起新关注 | 名声扩散引起新势力注意 |
| 关系网扩展 | 帮助支线改变境遇 | 支线角色因主角帮助境遇改变,建立忠诚 |
| 关系网扩展 | 重要角色遇麻烦等主角出手 | 重要角色遇麻烦,期待主角出手 |
| 矛盾升级 | 逐步接近目标每步拉期待 | 逐步接近目标,每步拉起新期待 |
| 矛盾升级 | 暗中威胁浮现 | 暗中威胁线索逐渐浮现,读者先于主角察觉 |
| 矛盾升级 | 不可调和矛盾到爆发临界 | 不可调和矛盾积累到爆发临界 |
| 矛盾升级 | 解决一个麻烦引出更大麻烦 | 解决一个麻烦后引出更大麻烦 |
| 矛盾升级 | 升级引发各方反应 | 各方势力对主角实力升级的不同反应 |
| 资源/实力 | 获得稀缺资源暗示用途 | 获得稀缺资源,期待用途 |
| 资源/实力 | 独特方式变废为宝 | 用独特方式将无用变宝物 |
| 资源/实力 | 恰好解决当前困境的手段 | 恰好有解决当前困境的手段 |
| 资源/实力 | 信息或实力远超他人认知 | 掌握的信息/实力远超他人认知 |
| 新线索/设定 | 前往新地图 | 即将前往新区域,刷新场景和冲突 |
| 新线索/设定 | 新规则或设定揭示 | 新规则/设定/体系的揭示 |
| 新线索/设定 | 已有能力新组合方式 | 已有能力的新组合方式 |
| 新线索/设定 | 已知信息下更深内容 | 已知信息下的更深层内容 |
| 新线索/设定 | 谜团逐步揭开 | 谜团层层揭开,满足好奇心 |
| 新线索/设定 | 绝路出现转机 | 看似绝路出现转机 |
| 等级/技能 | 描写升级路径拉期待 | 描写升级路径,期待每次突破 |
| 等级/技能 | 新能力效果展示 | 展示新能力的具体效果和应用场景 |
| 等级/技能 | 升级后装逼期待 | 升级后读者期待装逼打脸剧情 |
| 等级/技能 | 主角能力克制当前对手 | 主角能力恰好克制当前对手 |
| 等级/技能 | 晋升展现多种能力 | 晋升后展现不同新能力,保持新鲜感 |
| 情绪/氛围 | 适当隐藏部分信息 | 适当谜语人,隐藏部分信息 |
| 情绪/氛围 | 重要角色牺牲 | 重要角色牺牲带来的情绪冲击和剧情转折 |
| 情绪/氛围 | 主角暗中布局期待收网 | 主角暗中布局,期待收网 |

**组合原则**:技巧服务表达,不是为了用而用。构思时可同时考虑多种技巧丰富剧情,没用就丢弃。

---

## 反派强时的三层破局写法

反派应对异常强势时,主角的破局要让读者感到"降维打击"。按层次递进:

| 层次 | 名称 | 做法 |
|------|------|------|
| 1 | 硬碰硬 | 实力碾压,简单粗暴 |
| 2 | 预判反制 | 反派出A,主角早准备了B克制A |
| 3 | 反预判 | 反派精心准备针对A,主角不仅避开A,还利用A作陷阱引导反派落入预设的B |

核心爽点:主角在更高层面的思考、准备和掌控力。计谋要比反派更早一层。

---

## Hook上瘾模型

套用 Hook 上瘾模型(触发 → 行动 → 奖励 → 投入),每个要素对应一个人性原罪:

| 要素 | 钩子 | 原罪 | 网文对应 |
|------|------|------|----------|
| 触发 | 欲望 | 妄想 | 切入点——提醒读者他想要什么,不是硬塞的 |
| 行动 | 简单 | 懒惰 | 行文标准——开篇繁琐 = 门槛太高,简单才能重复上瘾 |
| 奖励 | 运气 | 贪婪 | 收获期待——可预见的奖励乏味,随机不确定才让人期待"下一次" |
| 投入 | 财富 | 痴迷 | 发展高潮——极品装备/排名/宗门小弟 = 沉没成本难以割舍 |

**奖励随机性**:收获感不仅要有,还得出乎意料(预期收获经验和钱,结果偷回一颗龙蛋)。

---

## 悬念深度机制

### 意外 vs 悬念

意外是炸弹突然爆炸;悬念是听到定时器滴答作响——悬念远比意外有力量。

### 章末断章留悬念

每章结尾让人物面临未解决的危险,读者不得不回来。

### 时间锁

故事前期设置必须在限定时间内发生的事件,压缩时间增强紧张感。

### 期待与兑现并行

短篇没有无限延迟满足的篇幅。每次延迟都要支付一笔可见收益:新证据、关系撕裂、误判修正或一次局部反击;连续两节只抬问题不交答案,会让读者感觉拖延。

### "麻烦消失"是失去读者的根本原因

诡异流升级过快变无敌、种田文发展过快变平推——都是麻烦消失。迎合读者短期喜好可能损害长线期待。

---

## 质量检查清单

写完每一节/全文后,逐项检查:

- [ ] **悬念等级达标**:对照强度5级分级表,本节悬念等级是否符合小节功能
- [ ] **期待链不断裂**:是否至少有一条未解的期待线在运行
- [ ] **角色反应到位**:每个悬念点是否有角色反应来验证力度
- [ ] **信息差存在**:读者与角色之间、角色与角色之间是否有信息差
- [ ] **节末有钩子**:关键节结尾是否有未解决问题、未完成动作或关系变化;弱节是否至少有明确承接
- [ ] **下行力度适中**:挫折是否控制在"小波折"范围,没有导致节奏断裂
- [ ] **震惊有层次**:震惊是否从点 → 网 → 深度递进,而非一次性爆发
- [ ] **道具变化可视化**:震惊递进是否有明确的视觉/物理变化支撑
- [ ] **爽感递增**:每次释放后的爽感是否比上一次更强
- [ ] **麻烦没有消失**:主角解决问题后是否有新的困境或挑战出现
- [ ] **主副线克制**:是否只有一个全文主悬念,副悬念是否服务主线而非扩成新故事
- [ ] **底牌有压制**:展示底牌后反派是否受到对应压制
references/submission-craft.md
# 投稿范式:平台基调 · 导语 · 付费点

> 短篇是投出去过稿的,不是写完就算。这份文件管三件投稿层的事:按平台定基调、单独打磨导语、把付费点卡在断点上。题材招式仍按 `genre-styles/{题材}.md`,本文只叠加"投稿怎么写才收得掉"这一层。

## 一、先定平台基调

动笔前先确认投稿平台,正文的视角、矛盾烈度、章末钩子、结局质感随之切换。三大主流基调:

| 平台 | 视角 | 矛盾演进 | 章末钩子 | 结局质感 |
|---|---|---|---|---|
| 知乎盐选 | 第一人称沉浸,生活化违和感切入("老公副驾驶有根红头发") | 心理战、信息差,靠细节拼图"细思极恐",情绪层层剥洋葱 | 发现一个颠覆前文认知的炸裂细节,悬念内敛后劲大 | 极致反转 / 因果轮回 / 高智商收尾,留白多、重余韵 |
| 小程序 | 上帝视角或强情绪第一人称,开局即地狱 | 冲突全外化到对白和动作,走"被羞辱→亮底牌→当众打脸→夺实利"快循环 | 卡脖子断点(巴掌将落、协议撕一半、话说一半) | 直白粗暴的爽,反派付实质代价(破产/入狱/众叛亲离),主角高位不留遗憾 |
| 番茄短篇 | 流畅第一/第三人称,目标清晰、金手指直白 | 顺滑无毒点,反派挑衅一两节内转成主角升级/发财的垫脚石 | 开新地图 / 拿到新底牌 / 新人物带资源登场 | 圆满畅快,事业感情双收,坏人受罚 |

- 小程序收稿量最大,打脸爽文是主力盘;同一个脑洞可以分别按知乎/小程序/番茄各跑一版,挑最顺的投。
- 基调只调"声音和落点",不覆盖题材招式和本篇情绪目标。基调是投放渠道的声音,与 `short-format.md` 的排版模板是两个轴——小程序基调的稿,排版仍按 short-format 选一套 app 模板。
- **调基调不松去 AI 底线**:强情绪靠具体动作和对白落地,别把话硬拆成三字碎句成片排列;前情背景靠场景带出,别整段插叙成"来龙去脉"说明书;对仗句式("上辈子…这辈子…")别高频复用。去 AI 味仍按本套件既有规则。

## 二、导语(投稿门面,单独打磨)

导语是正文最前面的引子,也是编辑第一眼看的东西。编辑一天审几百篇,**导语不行,正文再好也被一眼刷掉**。所以导语要单独当一件作品来打磨,别指望编辑往下翻。

- **长度与形态**:约 150-220 字(题材包如写 150-300,投稿时按此收紧),就是正文开头那几段。一句一段指完整句各自独立成段,不是把整句拆成三字碎句;这是黑岩/盐言导语的形态,番茄导语按 `short-format.md` 该平台的短段叙织即可。导语写完即接正文(内容本身就是第一章开头,不重写)。
- **四维骨架**:起因(悲剧源头或荒诞开端)+ 核心冲突(被凝视、被剥削、被算计的绝境)+ 人设底色(清醒、有底牌,不是纯惨)+ 情绪反转(钩子卡在最后半句,戛然而止)。
- **黄金三角**:一个有冲击力的具体物件(撕碎的 B 超单、代持协议、落灰的旧相机)+ 一个信息差(主角藏在暗处的底牌,读者知道、施害者不知道)+ 一个留白的致命钩子。
- **反面**:别用导语平铺背景或抒情;别把底牌全亮完(留给正文兑现);别只写惨不写"她有后手"。

## 三、付费点(投稿结构)

短篇按"免费试读 + 付费点 + 付费正文"投稿(投稿语境的"章"即本 skill 的"节")。付费点是免费段和付费段的分界,卡不好读者不付费,卡对了转化高。

- **断点位置**:付费点卡在一个章末的卡脖子断点上——一个即将揭晓、读者必须付费才能看到的反转或对决瞬间(亲子鉴定撕开一半、底牌刚要拍上桌、名字念到一半)。别把核心大爽点在免费段全放完。
- **剧情点密度**:付费点前每章 2-3 个剧情点(快速立处境、埋钩、层层加压);付费点后每章比之前多 1-2 个(集中兑现打脸/反转/爽点)。剧情点适度加,太多会乱。
- **反推法排细纲**:先定付费点卡在第几节章末,先把那一节的剧情想透(如复仇虐爽:前几节情绪递进越来越虐,付费点节章末预示反击),再倒推安排前面各节"情绪冲突逐渐递进",最后排付费点之后的兑现段。
- **投稿成品形态**:标题 + 导语(不写"导语"二字,只留那几句)+ 数字分节的正文(含付费点断点)。投稿建议区间比 SKILL 框架(8000-20000)更收敛:整篇约 8000-12000 字,每章约 1000-1300 字,章数据此约 7-12 章。
references/villain-and-reveal.md
# 反派系统 + 真相揭露机制

> 反派设计模板、真相揭露方式、报应设计。Phase 2 设计反派时加载。

---

**本文件职责**(其他内容去哪看):
- 各题材的反派套路(世情打脸反派、重生复仇反派等)已在 genre-writing-formulas.md,本文件只提供通用设计模板
- 本文件的反派人设模板 `{身份}+{动机}+{作恶方式}+{致命弱点}` 侧重「设计蓝图」,genre-writing-formulas.md 的 `{人设}+{动机}+{行为}+{态度}+{受伤害程度}` 侧重「矛盾冲突公式」——前者用于 Phase 2 设计,后者用于 Phase 3 写作

---

## 1. 反派设计模板

### 人设模板

填写以下模板,写入设定.md:

```
反派:
- 身份:{与主角的关系}
- 动机:{自洽的作恶逻辑:贪婪/嫉妒/控制欲/报复/自保}
- 作恶方式:{具体手段:言语打压/经济控制/社交孤立/人身伤害/法律构陷}
- 致命弱点:{可被主角利用的弱点:贪婪/傲慢/秘密/依赖/恐惧}
- 报应:{与恶行对应的结局}
```

### 嚣张度递增阶梯

反派行为必须逐级递增,不跳跃也不重复:

| 级别 | 行为类型 | 示例 |
|------|----------|------|
| 小恶(前 1/3) | 言语打压、冷暴力 | 「你嫁进来就算烧高香了」 |
| 中恶(中段) | 经济/社交伤害 | 偷拿存折、在外败坏名声、挑拨关系 |
| 大恶(后段) | 人身/法律伤害 | 下药、构陷、逼死、拐卖 |

### 5 种反派类型速查

| 类型 | 特征 | 代表作 | 致命弱点 |
|------|------|--------|----------|
| 操控型 | 以爱为名控制一切 | 王氏(皇弟) | 控制欲导致行事留下证据 |
| 贪婪型 | 无底线索取 | 准儿媳 | 贪心过度暴露真面目 |
| 伪装型 | 隐藏真实身份/目的 | 诈骗犯(重生弟弟) | 身份经不起调查 |
| 嚣张型 | 仗势欺人,有恃无恐 | 萧琅(皇弟) | 傲慢导致不看身后 |
| 软刀型 | 示弱博同情,暗中操控 | 妈妈(姐夫捎路) | 假面被证据击碎 |

---

## 2. 真相揭露机制

### 4 种揭露方式

#### 证物揭露

物理证据一锤定音。

操作:在铺垫段埋入「看似无关紧要的物件/文件」,揭露时翻转其意义。

```
铺垫:妈妈脖子上总戴着一把金锁,说是姐姐送的
揭露:打开金锁,里面刻着「金包铜」三个字
暴击:她从来没打开过这把锁
```

#### 证人揭露

第三方视角打破谎言。

操作:安排一个边缘角色(仆人/邻居/同事),在关键时刻说出主角不知道的信息。

```
铺垫:周嬷嬷总是欲言又止
揭露:周嬷嬷临终前交给主角一封信——先皇后的供状
暴击:王氏知道这封信的存在,所以一直在找周嬷嬷
```

#### 偷听/撞见

主角获取隐藏信息。

操作:设计一个「主角不该在场的场景」,让主角无意中看到/听到真相。

```
铺垫:妈妈每次接电话都走到阳台上
揭露:主角路过阳台,听到妈妈说「那个金锁是假的,她到现在都不知道」
暴击:电话那头是姐夫
```

#### 延迟剥洋葱

一层真相引出更深真相。

操作:每层用一个节揭露,每层都比上一层更残酷。

```
第1层:弟弟的女友不是她说的那个人
第2层:女友是诈骗犯,有犯罪记录
第3层:怀孕是假的
第4层:弟弟已经被骗到电诈园区
```

### 按题材选择揭露节奏

| 题材 | 推荐方式 | 节奏 |
|------|----------|------|
| 世情/爽文 | 证物揭露 或 证人揭露 | 一次大揭露,干净利落 |
| 情感/虐心 | 偷听/撞见 + 延迟剥洋葱 | 分层揭露,层层更痛 |
| 古言/复仇 | 证人揭露 + 证物揭露 | 先证人铺垫再证物一锤 |
| 悬疑/推理 | 延迟剥洋葱 | 全程分层,最后一块拼图 |
| 年代/亲情 | 偷听/撞见 | 意外发现,情绪冲击最大 |

---

## 3. 反派报应设计

### 规则

报应必须与恶行形成对应。恶行有多重,报应就有多重。

### 对应原则

| 恶行 | 报应 | 对应逻辑 |
|------|------|----------|
| 逼死在冷宫 | 自己住进冷宫 | 因果轮回 |
| 偷拿存折 | 账户被冻结,一分拿不到 | 偷失去的,连自己的也失去 |
| 以爱为名操控 | 被所有人抛弃 | 假爱真控,最终无人可控 |
| 诈骗钱财 | 被更狠的人骗进园区 | 骗子被更高级骗子收割 |

### 4 种报应模式

| 模式 | 适用场景 | 操作 |
|------|----------|------|
| 法律制裁 | 现代背景,恶行触犯法律 | 报警/起诉/判决,用简短几句交代 |
| 社会性死亡 | 虚伪型/操控型反派 | 真相公开,所有人看清真面目 |
| 因果轮回 | 古言/复仇题材 | 做过的事回到自己身上 |
| 精神崩溃 | 虐心/意难平题材 | 反派得到一切却发现失去了唯一重要的东西 |

### 报应强度校准

- 报应强度 = 恶行强度 × 1.0-1.2(不过度也不轻描淡写)
- 不写长段报应描写,2-3 句带过,留白给读者
- 报应的「爽」来自对应感,不是来自描写长度

---

## 4. 反转有效性自检(Phase 2 使用)

写完 outline 后,用这个自检确认反转真的能反转读者:

| 检查项 | 通过标准 | 不通过 = 需要 |
|--------|---------|-------------|
| 读者知道真相的时机 | >60% 合理;50-60% 灰区需审视反转力度;<50% 不算反转(三档口径详见 writing-workflow.md「反转信息差验证」) | 调整线索释放节奏,延后真相揭露 |
| 是否有误导假线索 | ≥ 1 条读者会信的假线索 | 加入红鲱鱼或选择性叙述误导 |
| 真相线索隐蔽度 | 回看能找到但正读时会忽略 | 线索藏在日常细节中,不用强调语气 |
| 角色知道 vs 读者知道 | 反转前读者和角色至少有一方不知道真相 | 制造信息差:读者不知道 / 角色不知道 / 双方都不知道 |

**失败模式**:「延迟揭露」不算反转。读者在第 2 节就知道真相,后面 10 节只等角色追上来 = 延迟揭露,读者会无聊。真正的反转发生在揭示那一刻,让读者和角色一起震惊。
references/writing-workflow.md
# writing-workflow.md:短篇写作工作流详解

本文件包含 story-short-write 的详细工作流程指引。SKILL.md 中只保留摘要和触发条件。

---

## Phase 2 设计任务(框架确定后执行)

### 开始前先通过 Reference Gate(逐项检查,不能跳过)

- 工作流与投稿结构 → 加载 `writing-workflow.md`(本文件)+ `submission-craft.md` ✅/❌
- 定题材腔调/招式 → 加载一个精确的 `genre-styles/{题材}.md` + `short-craft.md`;冷门题材改用 `genre-writing-formulas.md` ✅/❌
- 写反派/揭露方式 → 加载 `villain-and-reveal.md`;不适用时写明原因 ✅/❌
- 反转信息差验证 → 加载 `short-reversal.md` ✅/❌
- 伏笔回查 → 见下方「伏笔回查清单」 ✅/❌
- 精确路径缺失或不可读时按 SKILL.md 首屏门禁停止,不创建/修改故事产物

### 设计步骤

1. **设计信物 / 贯穿物**(可选,1-2 个):见题材包招式库「信物翻转」(如追妻火葬场的信物意义翻转)
   - 填写:`- 第1次出现(第X节):{场景}({含义})` 重复三次,含节号定位
2. **设计反派**(如有):用 villain-and-reveal.md 反派模板
   - 填写:`{身份} + {动机} + {作恶方式} + {致命弱点} + {报应}`
3. **确定揭露方式**:从 villain-and-reveal.md 4 种真相揭露机制中选一种
4. **编写 小节大纲.md**:使用固定 12 列 Markdown 表格,每节 1 行,表头如下(列名和顺序不可改):

   `| 结构段/五段功能 | 主事件 | 子事件×3-5 | 情绪 | 人物/关系变化 | 因果/逻辑链 | 读者新获知什么 | 结尾承接/钩子 | 伏笔/物件 | 动静 | 对话密度 | 目标字数 |`

   下一行写 Markdown 分隔行,再逐节填写数据行。
   - `结构段/五段功能` 使用开头/铺垫/升级/反转/结尾(或起因/发展/转折/高潮/结尾),用于轻量对应长篇的内容概括,但不要套长篇完整章节蓝图
   - `人物/关系变化` 写本节前后变化;没有显性变化写“无显性,但关系压力/误解/距离变化为…”或 `[待补充]`
   - `因果/逻辑链` 写 `原因 → 行动 → 结果 → 后果/新问题`,避免小节只是情绪堆叠
   - `结尾承接/钩子` 写本节如何接下一节;最后一节写结尾落点
   - 子事件必须功能明确(推动剧情/铺垫反转/升级情绪),禁止装饰性子事件
   - 子事件不足 3 个时用扩展规则(加阻碍 / 配角反应 / 移动中发现 / 简短倒叙 / 动作递进;见 short-craft.md 第 10-11 节)
   - 子事件用 `->` 连接,每个子事件包含具体情节点描述+功能标签:
     `具体发生了什么{功能标签}` 如 `发现账单上多了一笔4800{发现}` / `把戒指冲进马桶{冲突}` / `他笑了{递进}`
     功能标签:`对话` / `冲突` / `伏笔` / `回忆` / `发现` / `递进`
     情节点描述要具体到"谁做了什么",不能只写标签如`对话`或`冲突`
   - 动静标记:`动` 或 `静` 或 `动静`,相邻两节尽量交替;如因题材需要连续同标记,需确保节内有情绪变化(如:两节都是"静",但第一节"疑惑→心酸"、第二节"心碎→崩溃",情绪曲线仍在上升)
   - 对话密度:`高`(3+轮)/ `中`(1-2轮)/ `零`(全节无对话),尽量不连续 2 节标 `零`;翻阅日记、独自发现等场景允许例外,但连续零对话段落的情绪必须通过揉进感官细节和身体反应来推进
5. **反转信息差验证**(outline 写完后、写作前必须验证):
   - `short-reversal.md` 判为无反转 → 写明理由,改查报应因果或甜度期待是否经过铺垫并得到兑现;跳过以下揭示检查
   - 记录:读者在第几节知道真相?用「揭穿节号 ÷ 全文总节数」计算位置百分比,供核对节次和篇幅分配;百分比本身不是通过阈值
   - 揭示前:错误解释是否已影响至少一次重要决定?还没有 → 误判尚未参与剧情,应补行动后果或调整揭示位置
   - 揭示后:是否留出新的行动、关系变化或代价结算?只剩口头解释 → 提前揭示或压缩说明
   - 双层反转:第一层阶段答案后是否发生基于该答案的新行动?没有 → 两层只是连续口供,应拉开功能距离
   - 检查:至少有 1 条读者被成功误导的假线索(不是读者猜到后等角色追上来)
   - 检查:反转前是否铺垫了 ≥3 条指向真相的线索(线索要隐蔽,不是明示)
   - 验证:找一个人(或自我模拟)只看 outline,能否在反转前猜到真相?猜到 = 失败
6. **伏笔回查清单**(outline 写完后、写作前检查):
   - 列出结尾所有关键元素(物件/信息/角色/关系变化)
   - 每个结尾元素在前文有 ≥1 次铺垫?没有 = 天降,必须补铺垫
   - 贯穿道具三次出现完整?(第 1 次出现在铺垫段,第 2 次出现在反转段,第 3 次出现在结尾段)
   - 铺垫节号与揭穿节号的间距 ≥ 3 节(间距太短铺垫无效)

### Phase 2 设计校验与确定性验收

`设定.md` 写入 `## Phase 2 设计校验`,至少包含:一个真实的题材 reference 路径、2-3 个核心招式、反派五字段或带原因的不适用声明、反转类型、`反转位置:第 X 节 ÷ 共 Y 节 = Z%`、`付费点:第 N 节末`。`short-reversal.md` 判为无反转时,位置改写为带原因的“不适用”。付费点同时写入 `小节大纲.md` 对应节的“结尾承接/钩子”单元格。

生成两份文件后按 SKILL.md 的 Phase 2 完成门禁运行 `node scripts/check-phase2-contract.js --json {短篇目录}`。失败时只按 JSON 的 `repair_scope` 做最小修改并复验,不做无目标整稿重写。

### 写作前加载(按需,同时 ≤ 3 个)

- 必读:`short-format.md`(正文格式)+ `short-craft.md`(通用底座)
- 必读:`short-deslop.md`(写作时自检 AI 腔)
- 按题材加载 1 个:`genre-styles/{题材}.md`(核心 10 题材)或 `genre-writing-formulas.md`(冷门题材速查表)
- 按需加载:`villain-and-reveal.md` / `emotional-methods.md`

### 工作目录结构

工作目录结构见 [Phase 2:构思核心框架](../SKILL.md#phase-2构思核心框架)。设定.md 内容构成:核心框架 + 人设 + 反转铺垫 + 贯穿道具(含三次出现追踪表)+ 反派设计 + 对标摘要。小节大纲.md 每节 1 行,格式以 [设计任务(框架确定后执行)](../SKILL.md#设计任务框架确定后执行) 第 4 步为准。

### 操作原则

- 正文直接写入文件,不要只输出在对话里
- 精修时读取文件再改写,修改记录直接体现在设定.md的贯穿道具追踪中
- 设定.md 维护贯穿道具追踪表:
  `| 物件 | 第1次出现(节号/含义) | 第2次出现(节号/含义) | 第3次出现(节号/含义) |`

---

## Phase 4 精修打磨

### 精修检查清单

```
## 精修清单

### 开头
- [ ] 前 3 句话能抓住人?
- [ ] 没有无意义的背景介绍?
- [ ] 包含一个开篇钩子?

### 情绪
- [ ] 情绪曲线是否有明确走向?
- [ ] 反转前的情绪铺垫够不够?
- [ ] 反转后的冲击够不够?

### 反转
- [ ] 反转是否出乎意料?
- [ ] 反转是否合情合理(回看有铺垫)?
- [ ] 反转时机是否合适?

### 节奏
- [ ] 有没有拖沓的部分?
- [ ] 每句话是否都有存在价值?
- [ ] 字数是否在目标范围内?

### 格式
- [ ] 段落以短为主、有长短交错与疏密变化(非通篇同长度)?无连续超长大段?
- [ ] 段间仅单换行?
- [ ] 对话独立成行、引号风格符合项目/平台约定?
- [ ] 标点节奏匹配语气/人物声线:没有通篇句号化,也没有随机堆砌问号/感叹号,或用 `……`/`——` 硬造停顿?
- [ ] 正文(含对话)里的 `……`、`——`、`—`、`--` 已改成动作、短句、逗号或句号?
- [ ] 无「他说/她道」?

### 钩子
- [ ] 每 2-3 小节有钩子?
- [ ] 章末有钩子(悬念或余韵)?

### AI 腔
- [ ] 无禁止比喻:「命运的齿轮」「如潮水般」「仿佛春风」「心猛地一沉」「眼眶泛红」
- [ ] 同一身体部位/情绪描写不超 5 次(统计:手指/心/膝盖/眼睛,超限必须替换)
- [ ] 「像」使用频率:全文不超 10 处(含「像是」「不像」)
- [ ] 完整 AI 腔黑名单见 short-deslop.md

### 技法
- [ ] 情绪外化:直给的情绪词后面是否接了场景里的具体动作或物件?无空泛情绪总结句(一丝X涌上心头)?
- [ ] 信物(若用):意义翻转是否完整?(查设定.md追踪表)
- [ ] 一动一静:每节是否有动有静?
- [ ] 开头密度:前 100 字事件 ≥ 3?

### 结尾
- [ ] 结尾是否有余韵?
- [ ] 读者会不会想转发?
```

### 标点确定性收尾

精修后对 `正文.md` 先运行 `node scripts/check-ai-patterns.js --check --fail-on=blocking 正文.md`。blocking 先改正文并复扫;advisory 只作读感提示,确属问题才改,功能性写法标 `[需复核]`。
再运行 `node scripts/normalize-punctuation.js 正文.md`(默认 `--quote-mode keep`)清理无功能省略号、破折号、双连字符和独立分隔线;盐言「」不受影响。

### 删减原则

1. 不推动剧情的对话 → 删
2. 不铺垫反转的描写 → 删
3. 不推高情绪的心理活动 → 删
4. 读者能猜到的内容 → 缩短
5. 重复表达的情绪 → 合并

---

## 常见问题与解决方案

| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 开头不抓人 | 在做背景铺垫 | 直接从冲突开始 |
| 中间拖沓 | 信息密度太低 | 删减或合并场景 |
| 反转没力度 | 铺垫不够或太明显 | 增加误导线索 |
| 结尾无力 | 反转后拖太长 | 反转后 500 字内收尾 |
| 全篇平淡 | 情绪曲线太平 | 加大情绪落差 |
| 感觉像流水账 | 缺乏情绪描写 | 加入人物内心感受 |
scripts/author_memory_commit.py
#!/usr/bin/env python3
"""Maintain evidence-backed author preferences and deterministic Markdown views.

The language model supplies compact semantic transactions. This tool validates
and applies them in memory, renders every derived view, and writes the JSON state
last as the commit point. Author memory is workspace-level and deliberately
separate from each book's story-continuity tracking.
"""

from __future__ import annotations

import argparse
import copy
import hashlib
import json
import os
import stat
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


INPUT_SCHEMA_VERSION = 1
STATE_SCHEMA_VERSION = 1
STATE_MAX_BYTES = 2 * 1024 * 1024
PROFILE_MAX_BYTES = 12288
PENDING_MAX_BYTES = 12288
JOURNAL_MAX_BYTES = 24576
QUERY_MAX_BYTES = 2048

KINDS = ("prose_style", "story_design", "workflow", "delivery", "interaction")
KIND_TITLES = {
    "prose_style": "文风与表达",
    "story_design": "故事设计",
    "workflow": "创作流程",
    "delivery": "交付格式",
    "interaction": "协作方式",
}
SCOPE_LEVELS = ("global", "genre", "book", "workflow")
STATUSES = ("active", "pending", "conflict", "rejected", "superseded")
CONFIDENCE_LEVELS = ("low", "medium", "high")
IMPORTANCE_LEVELS = ("low", "medium", "high")
SOURCES = (
    "explicit_user",
    "accepted_suggestion",
    "repeated_correction",
    "inferred_pattern",
    "manual",
)
RANK = {"low": 0, "medium": 1, "high": 2}


class AuthorMemoryError(ValueError):
    """Expected validation or state error."""


def require(condition: bool, message: str) -> None:
    if not condition:
        raise AuthorMemoryError(message)


def as_mapping(value: object, label: str) -> dict[str, Any]:
    require(isinstance(value, dict), f"{label} must be a JSON object")
    return value


def as_list(value: object, label: str) -> list[Any]:
    require(isinstance(value, list), f"{label} must be a JSON array")
    return value


def as_int(value: object, label: str, *, minimum: int = 0) -> int:
    require(isinstance(value, int) and not isinstance(value, bool), f"{label} must be an integer")
    require(value >= minimum, f"{label} must be >= {minimum}")
    return value


def require_known_keys(mapping: dict[str, Any], allowed: set[str], label: str) -> None:
    unknown = set(mapping) - allowed
    require(not unknown, f"{label} contains unsupported fields: {', '.join(sorted(unknown))}")


def clean_text(value: object, label: str, *, max_bytes: int = 768) -> str:
    require(isinstance(value, str), f"{label} must be a string")
    cleaned = " ".join(value.replace("|", "|").split())
    require(bool(cleaned), f"{label} must not be empty")
    require(len(cleaned.encode("utf-8")) <= max_bytes, f"{label} exceeds {max_bytes} bytes")
    return cleaned


def optional_text(value: object, label: str, *, max_bytes: int = 768) -> str | None:
    if value is None:
        return None
    return clean_text(value, label, max_bytes=max_bytes)


def choice(value: object, allowed: tuple[str, ...], label: str) -> str:
    require(isinstance(value, str) and value in allowed, f"{label} must be one of: {', '.join(allowed)}")
    return value


def clean_id_list(value: object, label: str, *, maximum: int = 32) -> list[str]:
    raw = as_list(value, label)
    require(len(raw) <= maximum, f"{label} may contain at most {maximum} items")
    result: list[str] = []
    for index, item in enumerate(raw):
        item_id = clean_text(item, f"{label}[{index}]", max_bytes=32)
        require(item_id.startswith("AP") and item_id[2:].isdigit() and int(item_id[2:]) >= 1, f"{label}[{index}] is not an author-memory id")
        if item_id not in result:
            result.append(item_id)
    return result


def emit(document: object, *, error: bool = False) -> None:
    payload = json.dumps(document, ensure_ascii=False, sort_keys=True)
    stream = sys.stderr if error else sys.stdout
    stream.flush()
    stream.buffer.write((payload + "\n").encode("utf-8"))
    stream.buffer.flush()


def json_payload(document: object) -> str:
    return json.dumps(document, ensure_ascii=False, indent=2, sort_keys=True) + "\n"


def read_json(path: Path) -> object:
    try:
        require(path.stat().st_size <= STATE_MAX_BYTES, f"{path} exceeds {STATE_MAX_BYTES} bytes")
        return json.loads(path.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        raise AuthorMemoryError(f"unable to read JSON {path}: {exc}") from exc


def atomic_write_text(path: Path, payload: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else 0o644
    fd, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
    temporary = Path(temporary_name)
    try:
        with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.chmod(temporary, mode)
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def write_if_changed(path: Path, payload: str) -> None:
    try:
        if path.read_text(encoding="utf-8") == payload:
            return
    except FileNotFoundError:
        pass
    atomic_write_text(path, payload)


def memory_root(workspace: Path) -> Path:
    return workspace.resolve() / ".story" / "作者记忆"


def state_path(workspace: Path) -> Path:
    return memory_root(workspace) / "_author-memory-state.json"


def empty_state() -> dict[str, Any]:
    return {
        "schema_version": STATE_SCHEMA_VERSION,
        "state_revision": 0,
        "next_item_number": 1,
        "items": {},
        "journal": [],
        "applied_transactions": {},
    }


def normalize_scope(value: object, label: str) -> dict[str, str | None]:
    scope = as_mapping(value, label)
    require_known_keys(scope, {"level", "value"}, label)
    level = choice(scope.get("level"), SCOPE_LEVELS, f"{label}.level")
    raw_value = scope.get("value")
    if level == "global":
        require(raw_value is None, f"{label}.value must be null for global scope")
        normalized_value = None
    else:
        normalized_value = clean_text(raw_value, f"{label}.value", max_bytes=180)
    return {"level": level, "value": normalized_value}


def normalize_evidence(value: object, label: str) -> dict[str, str | None]:
    evidence = as_mapping(value, label)
    require_known_keys(evidence, {"quote", "source_ref"}, label)
    return {
        "quote": clean_text(evidence.get("quote"), f"{label}.quote", max_bytes=768),
        "source_ref": optional_text(evidence.get("source_ref"), f"{label}.source_ref", max_bytes=240),
    }


def normalize_item(value: object, label: str) -> dict[str, Any]:
    item = as_mapping(value, label)
    allowed = {
        "id", "kind", "scope", "assertion", "confidence", "importance", "status", "source",
        "reason", "conflicts_with", "confirmation_count", "evidence", "created_revision",
        "updated_revision", "superseded_by",
    }
    require_known_keys(item, allowed, label)
    item_id = clean_text(item.get("id"), f"{label}.id", max_bytes=32)
    require(item_id.startswith("AP") and item_id[2:].isdigit() and int(item_id[2:]) >= 1, f"{label}.id is invalid")
    evidence = [normalize_evidence(entry, f"{label}.evidence[{index}]") for index, entry in enumerate(as_list(item.get("evidence"), f"{label}.evidence"))]
    require(bool(evidence), f"{label}.evidence must not be empty")
    status = choice(item.get("status"), STATUSES, f"{label}.status")
    conflicts = clean_id_list(item.get("conflicts_with"), f"{label}.conflicts_with")
    superseded_by = optional_text(item.get("superseded_by"), f"{label}.superseded_by", max_bytes=32)
    if superseded_by is not None:
        require(superseded_by.startswith("AP") and superseded_by[2:].isdigit(), f"{label}.superseded_by is invalid")
    return {
        "id": item_id,
        "kind": choice(item.get("kind"), KINDS, f"{label}.kind"),
        "scope": normalize_scope(item.get("scope"), f"{label}.scope"),
        "assertion": clean_text(item.get("assertion"), f"{label}.assertion", max_bytes=768),
        "confidence": choice(item.get("confidence"), CONFIDENCE_LEVELS, f"{label}.confidence"),
        "importance": choice(item.get("importance"), IMPORTANCE_LEVELS, f"{label}.importance"),
        "status": status,
        "source": choice(item.get("source"), SOURCES, f"{label}.source"),
        "reason": clean_text(item.get("reason"), f"{label}.reason", max_bytes=480),
        "conflicts_with": conflicts,
        "confirmation_count": as_int(item.get("confirmation_count"), f"{label}.confirmation_count", minimum=1),
        "evidence": evidence,
        "created_revision": as_int(item.get("created_revision"), f"{label}.created_revision", minimum=1),
        "updated_revision": as_int(item.get("updated_revision"), f"{label}.updated_revision", minimum=1),
        "superseded_by": superseded_by,
    }


def validate_state(value: object) -> dict[str, Any]:
    state = as_mapping(value, "state")
    allowed = {"schema_version", "state_revision", "next_item_number", "items", "journal", "applied_transactions"}
    require_known_keys(state, allowed, "state")
    require(state.get("schema_version") == STATE_SCHEMA_VERSION, f"state.schema_version must be {STATE_SCHEMA_VERSION}")
    revision = as_int(state.get("state_revision"), "state.state_revision")
    next_number = as_int(state.get("next_item_number"), "state.next_item_number", minimum=1)
    raw_items = as_mapping(state.get("items"), "state.items")
    items: dict[str, Any] = {}
    max_number = 0
    for raw_id, raw_item in raw_items.items():
        normalized = normalize_item(raw_item, f"state.items.{raw_id}")
        require(raw_id == normalized["id"], f"state.items key {raw_id} does not match item id")
        max_number = max(max_number, int(raw_id[2:]))
        require(normalized["created_revision"] <= normalized["updated_revision"] <= revision, f"state.items.{raw_id} revision is ahead of state")
        items[raw_id] = normalized
    require(next_number > max_number, "state.next_item_number must be greater than every allocated item id")
    for item_id, item in items.items():
        for conflict_id in item["conflicts_with"]:
            require(conflict_id in items and conflict_id != item_id, f"state.items.{item_id} has an invalid conflict id")
        if item["superseded_by"] is not None:
            require(item["superseded_by"] in items and item["superseded_by"] != item_id, f"state.items.{item_id} has an invalid superseded_by id")
        if item["status"] == "active":
            require(not item["conflicts_with"], f"active item {item_id} cannot retain conflicts")
        if item["status"] == "pending":
            require(not item["conflicts_with"], f"pending item {item_id} cannot retain conflicts")
        if item["status"] == "conflict":
            require(bool(item["conflicts_with"]), f"conflict item {item_id} must reference an active item")
            require(all(items[conflict_id]["status"] == "active" for conflict_id in item["conflicts_with"]), f"conflict item {item_id} must reference only active items")
        if item["status"] != "superseded":
            require(item["superseded_by"] is None, f"only superseded item {item_id} may set superseded_by")
    journal = as_list(state.get("journal"), "state.journal")
    require(len(journal) == revision, "state.journal length must equal state.state_revision")
    journal_revisions: dict[str, int] = {}
    for index, entry in enumerate(journal):
        mapping = as_mapping(entry, f"state.journal[{index}]")
        require_known_keys(mapping, {"revision", "transaction_id", "committed_at", "summaries"}, f"state.journal[{index}]")
        entry_revision = as_int(mapping.get("revision"), f"state.journal[{index}].revision", minimum=1)
        require(entry_revision == index + 1, f"state.journal[{index}].revision must be {index + 1}")
        transaction_id = clean_text(mapping.get("transaction_id"), f"state.journal[{index}].transaction_id", max_bytes=128)
        require(transaction_id not in journal_revisions, f"state.journal repeats transaction_id {transaction_id}")
        journal_revisions[transaction_id] = entry_revision
        clean_text(mapping.get("committed_at"), f"state.journal[{index}].committed_at", max_bytes=64)
        summaries = as_list(mapping.get("summaries"), f"state.journal[{index}].summaries")
        require(bool(summaries), f"state.journal[{index}].summaries must not be empty")
        for summary_index, summary in enumerate(summaries):
            clean_text(summary, f"state.journal[{index}].summaries[{summary_index}]", max_bytes=768)
    transactions = as_mapping(state.get("applied_transactions"), "state.applied_transactions")
    require(set(transactions) == set(journal_revisions), "state.applied_transactions must match state.journal transaction ids")
    for transaction_id, record in transactions.items():
        clean_text(transaction_id, "state.applied_transactions key", max_bytes=128)
        mapping = as_mapping(record, f"state.applied_transactions.{transaction_id}")
        require_known_keys(mapping, {"revision", "digest", "item_ids"}, f"state.applied_transactions.{transaction_id}")
        transaction_revision = as_int(mapping.get("revision"), f"state.applied_transactions.{transaction_id}.revision", minimum=1)
        require(transaction_revision == journal_revisions[transaction_id], f"state.applied_transactions.{transaction_id}.revision does not match journal")
        digest = clean_text(mapping.get("digest"), f"state.applied_transactions.{transaction_id}.digest", max_bytes=64)
        require(len(digest) == 64 and all(char in "0123456789abcdef" for char in digest), f"state.applied_transactions.{transaction_id}.digest is invalid")
        item_ids = clean_id_list(mapping.get("item_ids"), f"state.applied_transactions.{transaction_id}.item_ids")
        require(bool(item_ids), f"state.applied_transactions.{transaction_id}.item_ids must not be empty")
        require(all(item_id in items for item_id in item_ids), f"state.applied_transactions.{transaction_id}.item_ids references an unknown item")
    return {
        "schema_version": STATE_SCHEMA_VERSION,
        "state_revision": revision,
        "next_item_number": next_number,
        "items": items,
        "journal": copy.deepcopy(journal),
        "applied_transactions": copy.deepcopy(transactions),
    }


def normalize_preference(value: object, label: str, *, allow_status: bool) -> dict[str, Any]:
    preference = as_mapping(value, label)
    allowed = {"kind", "scope", "assertion", "quote", "source_ref", "source", "confidence", "importance", "reason"}
    if allow_status:
        allowed |= {"status", "conflicts_with"}
    require_known_keys(preference, allowed, label)
    source = choice(preference.get("source"), SOURCES, f"{label}.source")
    status = choice(preference.get("status"), ("active", "pending", "conflict"), f"{label}.status") if allow_status else "active"
    conflicts = clean_id_list(preference.get("conflicts_with", []), f"{label}.conflicts_with") if allow_status else []
    if status == "active":
        require(not conflicts, f"{label}.conflicts_with must be empty for active status")
        require(source not in {"repeated_correction", "inferred_pattern"}, f"{label} inferred evidence must remain pending")
    elif status == "conflict":
        require(bool(conflicts), f"{label}.conflicts_with is required for conflict status")
    else:
        require(not conflicts, f"{label}.conflicts_with is only valid for conflict status")
    return {
        "kind": choice(preference.get("kind"), KINDS, f"{label}.kind"),
        "scope": normalize_scope(preference.get("scope"), f"{label}.scope"),
        "assertion": clean_text(preference.get("assertion"), f"{label}.assertion", max_bytes=768),
        "quote": clean_text(preference.get("quote"), f"{label}.quote", max_bytes=768),
        "source_ref": optional_text(preference.get("source_ref"), f"{label}.source_ref", max_bytes=240),
        "source": source,
        "confidence": choice(preference.get("confidence"), CONFIDENCE_LEVELS, f"{label}.confidence"),
        "importance": choice(preference.get("importance"), IMPORTANCE_LEVELS, f"{label}.importance"),
        "status": status,
        "reason": clean_text(preference.get("reason"), f"{label}.reason", max_bytes=480),
        "conflicts_with": conflicts,
    }


def normalize_transaction(value: object) -> dict[str, Any]:
    transaction = as_mapping(value, "transaction")
    require_known_keys(transaction, {"schema_version", "transaction_id", "expected_state_revision", "operations"}, "transaction")
    require(transaction.get("schema_version") == INPUT_SCHEMA_VERSION, f"transaction.schema_version must be {INPUT_SCHEMA_VERSION}")
    transaction_id = clean_text(transaction.get("transaction_id"), "transaction.transaction_id", max_bytes=128)
    operations = as_list(transaction.get("operations"), "transaction.operations")
    require(1 <= len(operations) <= 32, "transaction.operations must contain 1-32 operations")
    normalized_operations: list[dict[str, Any]] = []
    for index, raw_operation in enumerate(operations):
        label = f"transaction.operations[{index}]"
        operation = as_mapping(raw_operation, label)
        action = operation.get("action")
        if action == "remember":
            require_known_keys(operation, {"action", "preference"}, label)
            normalized_operations.append({"action": action, "preference": normalize_preference(operation.get("preference"), f"{label}.preference", allow_status=True)})
        elif action == "decide":
            require_known_keys(operation, {"action", "item_id", "decision", "quote", "reason"}, label)
            normalized_operations.append({
                "action": action,
                "item_id": clean_id_list([operation.get("item_id")], f"{label}.item_id", maximum=1)[0],
                "decision": choice(operation.get("decision"), ("activate", "reject"), f"{label}.decision"),
                "quote": clean_text(operation.get("quote"), f"{label}.quote", max_bytes=768),
                "reason": clean_text(operation.get("reason"), f"{label}.reason", max_bytes=480),
            })
        elif action == "replace":
            require_known_keys(operation, {"action", "old_ids", "preference"}, label)
            old_ids = clean_id_list(operation.get("old_ids"), f"{label}.old_ids")
            require(bool(old_ids), f"{label}.old_ids must not be empty")
            normalized_operations.append({"action": action, "old_ids": old_ids, "preference": normalize_preference(operation.get("preference"), f"{label}.preference", allow_status=False)})
        elif action == "forget":
            require_known_keys(operation, {"action", "item_id", "quote", "reason"}, label)
            normalized_operations.append({
                "action": action,
                "item_id": clean_id_list([operation.get("item_id")], f"{label}.item_id", maximum=1)[0],
                "quote": clean_text(operation.get("quote"), f"{label}.quote", max_bytes=768),
                "reason": clean_text(operation.get("reason"), f"{label}.reason", max_bytes=480),
            })
        else:
            raise AuthorMemoryError(f"{label}.action must be one of: remember, decide, replace, forget")
    return {
        "schema_version": INPUT_SCHEMA_VERSION,
        "transaction_id": transaction_id,
        "expected_state_revision": as_int(transaction.get("expected_state_revision"), "transaction.expected_state_revision"),
        "operations": normalized_operations,
    }


def normalize_record_event(value: object) -> dict[str, Any]:
    event = as_mapping(value, "event")
    require_known_keys(event, {"schema_version", "event_id", "operation"}, "event")
    require(event.get("schema_version") == INPUT_SCHEMA_VERSION, f"event.schema_version must be {INPUT_SCHEMA_VERSION}")
    event_id = clean_text(event.get("event_id"), "event.event_id", max_bytes=120)
    normalized = normalize_transaction({
        "schema_version": INPUT_SCHEMA_VERSION,
        "transaction_id": f"record:{event_id}",
        "expected_state_revision": 0,
        "operations": [event.get("operation")],
    })
    return {"event_id": event_id, "operation": normalized["operations"][0]}


def transaction_digest(transaction: dict[str, Any]) -> str:
    canonical = json.dumps(transaction, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()


def fingerprint(preference: dict[str, Any]) -> str:
    value = {
        "kind": preference["kind"],
        "scope": preference["scope"],
        "assertion": preference["assertion"].casefold(),
    }
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def allocate_item(state: dict[str, Any], preference: dict[str, Any], revision: int) -> dict[str, Any]:
    item_id = f"AP{state['next_item_number']:03d}"
    state["next_item_number"] += 1
    return {
        "id": item_id,
        "kind": preference["kind"],
        "scope": copy.deepcopy(preference["scope"]),
        "assertion": preference["assertion"],
        "confidence": preference["confidence"],
        "importance": preference["importance"],
        "status": preference["status"],
        "source": preference["source"],
        "reason": preference["reason"],
        "conflicts_with": list(preference["conflicts_with"]),
        "confirmation_count": 1,
        "evidence": [{"quote": preference["quote"], "source_ref": preference["source_ref"]}],
        "created_revision": revision,
        "updated_revision": revision,
        "superseded_by": None,
    }


def best_level(first: str, second: str) -> str:
    return first if RANK[first] >= RANK[second] else second


def add_evidence(item: dict[str, Any], quote: str, source_ref: str | None) -> None:
    evidence = {"quote": quote, "source_ref": source_ref}
    if evidence not in item["evidence"]:
        item["evidence"].append(evidence)


def require_item(state: dict[str, Any], item_id: str, label: str) -> dict[str, Any]:
    require(item_id in state["items"], f"{label} references unknown item {item_id}")
    return state["items"][item_id]


def apply_remember(state: dict[str, Any], preference: dict[str, Any], revision: int) -> str:
    for conflict_id in preference["conflicts_with"]:
        conflict = require_item(state, conflict_id, "remember")
        require(conflict["status"] == "active", f"remember conflict {conflict_id} must be active")
    preference_fingerprint = fingerprint(preference)
    for item in state["items"].values():
        if item["status"] not in {"active", "pending", "conflict"} or fingerprint(item) != preference_fingerprint:
            continue
        require(not (item["status"] == "conflict" and preference["status"] == "active"), f"conflict item {item['id']} must be resolved with replace or rejected")
        require(not (item["status"] == "active" and preference["status"] == "conflict"), f"active item {item['id']} cannot be recategorized as its own conflict")
        add_evidence(item, preference["quote"], preference["source_ref"])
        item["confirmation_count"] += 1
        item["confidence"] = best_level(item["confidence"], preference["confidence"])
        item["importance"] = best_level(item["importance"], preference["importance"])
        item["updated_revision"] = revision
        item["reason"] = preference["reason"]
        if item["status"] == "pending" and preference["status"] == "active":
            item["status"] = "active"
        elif item["status"] == "pending" and preference["status"] == "conflict":
            item["status"] = "conflict"
            item["conflicts_with"] = list(preference["conflicts_with"])
        elif item["status"] == "conflict" and preference["status"] == "conflict":
            item["conflicts_with"] = sorted(set(item["conflicts_with"]) | set(preference["conflicts_with"]))
        return f"强化 {item['id']}:{item['assertion']}"
    item = allocate_item(state, preference, revision)
    state["items"][item["id"]] = item
    return f"新增 {item['id']}({item['status']}):{item['assertion']}"


def apply_decide(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
    item = require_item(state, operation["item_id"], "decide")
    require(item["status"] in {"pending", "conflict"}, f"decide requires pending/conflict item, got {item['status']}")
    if operation["decision"] == "activate":
        require(item["status"] == "pending" and not item["conflicts_with"], "conflict candidates must be activated with replace")
        item["status"] = "active"
        verb = "确认"
    else:
        item["status"] = "rejected"
        verb = "拒绝"
    add_evidence(item, operation["quote"], None)
    item["reason"] = operation["reason"]
    item["updated_revision"] = revision
    return f"{verb} {item['id']}:{item['assertion']}"


def apply_replace(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
    old_items = [require_item(state, item_id, "replace") for item_id in operation["old_ids"]]
    for item in old_items:
        require(item["status"] in {"active", "conflict", "pending"}, f"replace target {item['id']} is already {item['status']}")
    replacement = allocate_item(state, operation["preference"], revision)
    replacement["status"] = "active"
    replacement["conflicts_with"] = []
    state["items"][replacement["id"]] = replacement
    for item in old_items:
        item["status"] = "superseded"
        item["superseded_by"] = replacement["id"]
        item["updated_revision"] = revision
    old_ids = {item["id"] for item in old_items}
    released = 0
    for candidate in state["items"].values():
        if candidate["status"] != "conflict":
            continue
        retained = [item_id for item_id in candidate["conflicts_with"] if item_id not in old_ids]
        if retained == candidate["conflicts_with"]:
            continue
        candidate["conflicts_with"] = retained
        candidate["updated_revision"] = revision
        if not retained:
            candidate["status"] = "pending"
            released += 1
    replaced = ", ".join(item["id"] for item in old_items)
    suffix = f";{released} 个其他冲突候选退回待确认" if released else ""
    return f"用 {replacement['id']} 替代 {replaced}:{replacement['assertion']}{suffix}"


def apply_forget(state: dict[str, Any], operation: dict[str, Any], revision: int) -> str:
    item = require_item(state, operation["item_id"], "forget")
    require(item["status"] in {"active", "pending", "conflict"}, f"forget target {item['id']} is already {item['status']}")
    item["status"] = "superseded"
    item["superseded_by"] = None
    item["reason"] = operation["reason"]
    item["updated_revision"] = revision
    add_evidence(item, operation["quote"], None)
    released = 0
    for candidate in state["items"].values():
        if candidate["status"] != "conflict" or item["id"] not in candidate["conflicts_with"]:
            continue
        candidate["conflicts_with"] = [conflict_id for conflict_id in candidate["conflicts_with"] if conflict_id != item["id"]]
        candidate["updated_revision"] = revision
        if not candidate["conflicts_with"]:
            candidate["status"] = "pending"
            released += 1
    suffix = f";{released} 个冲突候选退回待确认" if released else ""
    return f"忘记 {item['id']}:{item['assertion']}{suffix}"


def apply_transaction(state: dict[str, Any], transaction: dict[str, Any], digest: str) -> tuple[dict[str, Any], list[str]]:
    applied = state["applied_transactions"].get(transaction["transaction_id"])
    if applied is not None:
        require(applied["digest"] == digest, "transaction_id was already used with different content")
        return state, [f"事务已应用于修订 {applied['revision']},本次为幂等重放"]
    require(transaction["expected_state_revision"] == state["state_revision"], f"stale state revision: expected {transaction['expected_state_revision']}, current {state['state_revision']}")
    updated = copy.deepcopy(state)
    revision = updated["state_revision"] + 1
    summaries: list[str] = []
    for operation in transaction["operations"]:
        if operation["action"] == "remember":
            summaries.append(apply_remember(updated, operation["preference"], revision))
        elif operation["action"] == "decide":
            summaries.append(apply_decide(updated, operation, revision))
        elif operation["action"] == "replace":
            summaries.append(apply_replace(updated, operation, revision))
        else:
            summaries.append(apply_forget(updated, operation, revision))
    committed_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
    updated["state_revision"] = revision
    updated["journal"].append({
        "revision": revision,
        "transaction_id": transaction["transaction_id"],
        "committed_at": committed_at,
        "summaries": summaries,
    })
    item_ids = sorted(
        (item_id for item_id, item in updated["items"].items() if item["updated_revision"] == revision),
        key=lambda item_id: int(item_id[2:]),
    )
    require(bool(item_ids), "transaction did not update any author-memory item")
    updated["applied_transactions"][transaction["transaction_id"]] = {
        "revision": revision,
        "digest": digest,
        "item_ids": item_ids,
    }
    return validate_state(updated), summaries


def scope_label(scope: dict[str, str | None]) -> str:
    if scope["level"] == "global":
        return "全局"
    labels = {"genre": "题材", "book": "本书", "workflow": "流程"}
    return f"{labels[scope['level']]}:{scope['value']}"


def render_profile(state: dict[str, Any]) -> str:
    lines = [
        "# 作者画像",
        "",
        "<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
        "",
        f"> 状态修订:{state['state_revision']}。仅列出已确认偏好;当前明确要求、本书设定与硬性门禁优先。",
        "",
    ]
    active = [item for item in state["items"].values() if item["status"] == "active"]
    for kind in KINDS:
        lines.extend([f"## {KIND_TITLES[kind]}", ""])
        items = sorted((item for item in active if item["kind"] == kind), key=lambda item: int(item["id"][2:]))
        if not items:
            lines.extend(["- 暂无", ""])
            continue
        for item in items:
            lines.append(f"- **{item['id']}**〔{scope_label(item['scope'])}|{item['confidence']}|确认 {item['confirmation_count']} 次〕{item['assertion']}")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def render_pending(state: dict[str, Any]) -> str:
    lines = [
        "# 待确认的作者习惯",
        "",
        "<!-- 由 author_memory_commit.py 生成,请勿手改;修改请提交事务。 -->",
        "",
        f"> 状态修订:{state['state_revision']}。待确认项不参与创作约束,也不应打断当前任务。",
        "",
    ]
    items = sorted((item for item in state["items"].values() if item["status"] in {"pending", "conflict"}), key=lambda item: int(item["id"][2:]))
    if not items:
        lines.extend(["暂无待确认项。", ""])
    for item in items:
        lines.extend([
            f"## {item['id']} · {'冲突' if item['status'] == 'conflict' else '待确认'}",
            "",
            f"- 候选习惯:{item['assertion']}",
            f"- 范围:{scope_label(item['scope'])}",
            f"- 原话:\u201c{item['evidence'][-1]['quote']}\u201d",
            f"- 依据:{item['reason']}",
            f"- 置信度 / 重要度:{item['confidence']} / {item['importance']}",
        ])
        if item["conflicts_with"]:
            lines.append(f"- 冲突对象:{', '.join(item['conflicts_with'])}")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def render_journal(state: dict[str, Any]) -> str:
    lines = [
        "# 作者记忆变更记录",
        "",
        "<!-- 由 author_memory_commit.py 生成,请勿手改;最近记录在前。 -->",
        "",
    ]
    if not state["journal"]:
        lines.extend(["暂无变更。", ""])
    for entry in reversed(state["journal"][-100:]):
        lines.extend([f"## r{entry['revision']} · {entry['committed_at']}", "", f"- 事务:`{entry['transaction_id']}`"])
        lines.extend(f"- {summary}" for summary in entry["summaries"])
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def render_views(state: dict[str, Any]) -> dict[str, str]:
    views = {
        "作者画像.md": render_profile(state),
        "待确认.md": render_pending(state),
        "变更记录.md": render_journal(state),
    }
    limits = {"作者画像.md": PROFILE_MAX_BYTES, "待确认.md": PENDING_MAX_BYTES, "变更记录.md": JOURNAL_MAX_BYTES}
    for name, payload in views.items():
        require(len(payload.encode("utf-8")) <= limits[name], f"{name} exceeds {limits[name]} bytes; consolidate old memory first")
    return views


def write_snapshot(workspace: Path, state: dict[str, Any]) -> None:
    root = memory_root(workspace)
    views = render_views(state)
    state_payload = json_payload(state)
    require(len(state_payload.encode("utf-8")) <= STATE_MAX_BYTES, f"_author-memory-state.json exceeds {STATE_MAX_BYTES} bytes")
    for name, payload in views.items():
        write_if_changed(root / name, payload)
    # State is the authority and therefore the last commit point.
    write_if_changed(state_path(workspace), state_payload)


def command_init(workspace: Path) -> dict[str, Any]:
    require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
    path = state_path(workspace)
    if path.exists():
        state = validate_state(read_json(path))
    else:
        state = empty_state()
    write_snapshot(workspace, state)
    return {"ok": True, "command": "init", "revision": state["state_revision"], "root": str(memory_root(workspace))}


def command_commit(workspace: Path, input_path: Path) -> dict[str, Any]:
    require(state_path(workspace).exists(), "author memory is not initialized; run init first")
    state = validate_state(read_json(state_path(workspace)))
    transaction = normalize_transaction(read_json(input_path))
    digest = transaction_digest(transaction)
    updated, summaries = apply_transaction(state, transaction, digest)
    replayed = updated is state
    if not replayed:
        write_snapshot(workspace, updated)
    else:
        # Repair missing or stale views during an idempotent retry.
        write_snapshot(workspace, state)
    return {
        "ok": True,
        "command": "commit",
        "revision": updated["state_revision"],
        "transaction_id": transaction["transaction_id"],
        "replayed": replayed,
        "item_ids": updated["applied_transactions"][transaction["transaction_id"]]["item_ids"],
        "summaries": summaries,
    }


def command_record(workspace: Path, input_path: Path) -> dict[str, Any]:
    require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
    event = normalize_record_event(read_json(input_path))
    path = state_path(workspace)
    state = validate_state(read_json(path)) if path.exists() else empty_state()
    transaction_id = f"record:{event['event_id']}"
    applied = state["applied_transactions"].get(transaction_id)
    expected_revision = applied["revision"] - 1 if applied is not None else state["state_revision"]
    transaction = {
        "schema_version": INPUT_SCHEMA_VERSION,
        "transaction_id": transaction_id,
        "expected_state_revision": expected_revision,
        "operations": [event["operation"]],
    }
    digest = transaction_digest(transaction)
    updated, summaries = apply_transaction(state, transaction, digest)
    replayed = updated is state
    write_snapshot(workspace, updated)
    record = updated["applied_transactions"][transaction_id]
    item_ids = record["item_ids"]
    receipt = f"Author Memory Receipt: r{record['revision']} · {', '.join(item_ids)}"
    return {
        "ok": True,
        "command": "record",
        "revision": updated["state_revision"],
        "applied_revision": record["revision"],
        "event_id": event["event_id"],
        "replayed": replayed,
        "item_ids": item_ids,
        "receipt": receipt,
        "summaries": summaries,
    }


def same_scope_value(item_value: str | None, requested: str | None) -> bool:
    return requested is not None and item_value is not None and item_value.casefold() == requested.casefold()


def command_query(
    workspace: Path,
    kinds: list[str] | None,
    book: str | None,
    genre: str | None,
    workflow: str | None,
) -> dict[str, Any]:
    require(workspace.exists() and workspace.is_dir(), f"workspace does not exist: {workspace}")
    path = state_path(workspace)
    if not path.exists():
        return {"ok": True, "command": "query", "initialized": False, "revision": 0, "items": [], "omitted": 0}
    state = validate_state(read_json(path))
    requested_kinds = set(kinds or KINDS)
    requested_scopes = {
        "book": optional_text(book, "query.book", max_bytes=180),
        "genre": optional_text(genre, "query.genre", max_bytes=180),
        "workflow": optional_text(workflow, "query.workflow", max_bytes=180),
    }

    def relevant(item: dict[str, Any]) -> bool:
        if item["status"] != "active" or item["kind"] not in requested_kinds:
            return False
        level = item["scope"]["level"]
        return level == "global" or same_scope_value(item["scope"]["value"], requested_scopes[level])

    scope_rank = {"book": 0, "genre": 1, "workflow": 2, "global": 3}
    candidates = sorted(
        (item for item in state["items"].values() if relevant(item)),
        key=lambda item: (
            scope_rank[item["scope"]["level"]],
            -RANK[item["importance"]],
            -item["confirmation_count"],
            int(item["id"][2:]),
        ),
    )
    result: dict[str, Any] = {
        "ok": True,
        "command": "query",
        "initialized": True,
        "revision": state["state_revision"],
        "items": [],
        "omitted": len(candidates),
    }
    for item in candidates:
        compact = {
            "id": item["id"],
            "kind": item["kind"],
            "scope": item["scope"],
            "assertion": item["assertion"],
        }
        result["items"].append(compact)
        result["omitted"] = len(candidates) - len(result["items"])
        payload = json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n"
        if len(payload.encode("utf-8")) > QUERY_MAX_BYTES:
            result["items"].pop()
            result["omitted"] += 1
            break
    require(len((json.dumps(result, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")) <= QUERY_MAX_BYTES, "query result exceeds its fixed byte budget")
    return result


def command_check(workspace: Path) -> dict[str, Any]:
    path = state_path(workspace)
    require(path.exists(), "author memory is not initialized")
    state = validate_state(read_json(path))
    views = render_views(state)
    root = memory_root(workspace)
    for name, expected in views.items():
        view_path = root / name
        require(view_path.exists(), f"missing derived view: {view_path}")
        require(view_path.read_text(encoding="utf-8") == expected, f"derived view is stale or edited: {view_path}")
    counts = {status: sum(1 for item in state["items"].values() if item["status"] == status) for status in STATUSES}
    return {"ok": True, "command": "check", "revision": state["state_revision"], "counts": counts}


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest="command", required=True)
    for command in ("init", "check"):
        child = subparsers.add_parser(command)
        child.add_argument("--workspace", required=True, type=Path)
    commit = subparsers.add_parser("commit")
    commit.add_argument("--workspace", required=True, type=Path)
    commit.add_argument("--input", required=True, type=Path)
    record = subparsers.add_parser("record")
    record.add_argument("--workspace", required=True, type=Path)
    record.add_argument("--input", required=True, type=Path)
    query = subparsers.add_parser("query")
    query.add_argument("--workspace", required=True, type=Path)
    query.add_argument("--kind", action="append", choices=KINDS)
    query.add_argument("--book")
    query.add_argument("--genre")
    query.add_argument("--workflow")
    return parser


def main() -> int:
    args = build_parser().parse_args()
    try:
        if args.command == "init":
            result = command_init(args.workspace)
        elif args.command == "commit":
            result = command_commit(args.workspace, args.input)
        elif args.command == "record":
            result = command_record(args.workspace, args.input)
        elif args.command == "query":
            result = command_query(args.workspace, args.kind, args.book, args.genre, args.workflow)
        else:
            result = command_check(args.workspace)
        emit(result)
        return 0
    except AuthorMemoryError as exc:
        emit({"ok": False, "error": str(exc)}, error=True)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())
scripts/check-ai-patterns.js
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');

const USAGE = `Usage: node check-ai-patterns.js [--check] [--json] [--fail-on=blocking|all] <file...>

Detect high-risk AI-flavor prose patterns that need human rewrite:
  - negative setup followed by positive flip in the same sentence
  - comma/semicolon/colon + positive flip
  - sentence break + positive flip
  - repeated negative setup followed by positive flip
  - em-dash (按功能改写), 碎句号 (连续短叙述句), 长段落 (按镜头断段)
  - 微动作复读 (「了下/了一下」式轻量补语高密度,电报体指纹)
  - 套式反应细节 (指尖/指节/目光等无功能微动作与「平静得像在念」式语气比喻成片)
  - 抽象总结复读 (命运/棋局/这一刻终于明白/才刚刚开始,AI 结尾腔)
  - 套词密度过高 (仿佛/一丝/深吸一口气/平静无波等禁用词聚集)
  - 比喻密度过高 (像/好像/仿佛/如同等比喻标记成片复现)
  - 解释链密度过高 (知道/明白/这意味着/必须/需要等判断链聚集)
  - 系统公告公文腔过密 (方括号系统/规则行里硬规则词聚集)
  - 过度精炼短段 (长文本里短叙述段过密且自然连接偏少)
  - 低连接密度 (引号外叙述功能词/白话连接偏少且中长句不足,像提纲/电报体)
  - 监控摄像头式动作清单 (同段连续摆放动作动词,缺少视角温度/情绪缓冲)
  - 音量反差腔 (声音不高/不大…却…, 实战漏网句式)
  - 否定排比 (没有X,没有Y…连排 / 没X…只是Y 先否定后肯定, 实战漏网句式)
  - 工整并列 (至于X不X,怎么X / 同动词「不V A,不V B」,含台词,advisory)
  - 反序对比 (是A,不是B — not-is 的反序变种, 实战漏网句式)
  - 预告式总结收尾 (文末窗口 没人知道/才刚刚开始/正朝着…压了过去, 实战漏网句式)
  - 章尾状态总结体 (文末窗口 这一夜注定/这一切都结束了/新的人生才刚刚开始/命运的齿轮)
  - 引号强调滥用 (叙述里 1-4 字短词加引号强调,密度型)

Each finding carries severity: blocking by default for generation/deslop cleanup (not-is-comparison / em-dash / voice-contrast / negation-parade / reverse-not-is / trailer-ending / trailer-summary). This is a local style/readability gate, not an AIGC detector score; functional human text can be marked for review instead of hard-edited for a detector.
或 advisory (period-stutter / long-paragraph / micro-action-tic / stock-reaction-tic / action-list-tic / abstract-summary-tic / cliche-density-tic / metaphor-density-tic / reasoning-chain-tic / system-notice-formality-tic / overcompressed-prose-tic / low-connective-density-tic / quote-emphasis-tic / formulaic-parallelism,是提示,justified 的长推理/氛围段可保留)。
--fail-on=blocking 只在出现 blocking finding 时退出 1;默认 --fail-on=all 有任何 finding 即退出 1。

The script reports findings only. It never rewrites text, because the safe fix is
contextual: usually delete the negative setup, write the positive term directly,
or show it via action/detail.`;

const STOP_CHARS = new Set(['。', '!', '?', '!', '?', '\n']);
const SOFT_SEPARATORS = new Set([',', ',', '、', ';', ';', ':', ':']);
const HARD_SEPARATORS = new Set(['。', '.', '!', '!', '?', '?']);
const MAX_NEGATIVE_SPAN = 80;
const MAX_POSITIVE_SPAN = 80;

// 碎句号:连续 STUTTER_MIN_RUN 个「叙述」短句(每句可见字数 ≤ STUTTER_MAX_SENTENCE)无呼吸。
// 只数叙述句,跳过对话/弹幕/系统播报(成片短句是这些体裁的正常形态,不算碎句号)。
const STUTTER_MIN_RUN = 6;
const STUTTER_MAX_SENTENCE = 5;
// 长段落:单段原始字符数超过阈值即提示按镜头断段(手机阅读保守阈值,正常单段远低于此)。
const LONG_PARAGRAPH_CHARS = 200;

// 微动作复读:「V了下 / V了一下 / 拍了两下 / 松了半圈」式轻量补语在叙述里高密度复现,
// 容易形成删减过头的电报体指纹。只扫引号外叙述;密度与次数双门槛同时达标才报,
// 单次出现是正常中文。
const MICRO_TIC_PATTERN = /了(?:[一两三几半])?[下阵圈道声眼口气会]/g;
const MICRO_TIC_MIN_HITS = 5;
const MICRO_TIC_PER_KILO = 6;

// 套式反应细节:不是禁写身体,而是提示成片出现的“部位 + 轻微动作/状态”、
// “胸口像被撞了一下”、喉结/眼圈/声音放轻等通用情绪尾巴,以及“平静语气 +
// 像在念/宣判”模板。此类句子词面变化大,不能逐词 blocking;按章聚集到 4 处才
// advisory,要求逐处做删除测试。正常受伤、打斗、生理反应若承担物理后果可保留。
const STOCK_REACTION_PATTERNS = [
  /(?:指尖|手指|指节|手背|掌心|拳头|袖口|衣角|裙角|下唇|嘴唇|唇角|嘴角|眉头|眼底|眸光|目光|视线|肩膀|呼吸)[^。!?!?\n]{0,16}(?:轻轻|微微|缓缓|悄然|不自觉|无意识|下意识|攥紧|握紧|收紧|绞紧|泛白|发白|叩|敲|摩挲|抿紧|抿成|移开|垂下|躲开|一颤|颤了?一下|停了?一下|顿了?一下)/g,
  /(?:语气|声音)[^。!?!?\n]{0,12}(?:平静|冷静|平淡|冷淡|淡漠|平直)[^。!?!?\n]{0,12}(?:像|仿佛|如同|好像)[^。!?!?\n]{0,16}(?:念|读|报|说|陈述|宣判|背诵)/g,
  /(?:胸口|心口)[^。!?!?\n]{0,16}(?:像|仿佛|如同|好像)[^。!?!?\n]{0,16}(?:撞|锤|压|攥|堵)[^。!?!?\n]{0,8}(?:一下|一记|一拳)?/g,
  /(?:声音|嗓音|语气)[^。!?!?\n]{0,12}(?:放轻|压低|发紧|发颤|很轻|轻了些)/g,
  /(?:喉结|喉头|喉咙)[^。!?!?\n]{0,10}(?:滚|动|紧|堵|发涩|发干)/g,
  /(?:眼眶|眼圈|鼻子)[^。!?!?\n]{0,8}(?:发红|红了|发热|发酸|一酸)/g,
  /(?:抿了?下唇|抿了?抿唇|抿了?下嘴|抿着笑)/g,
];
const STOCK_REACTION_MIN_HITS = 4;
// 校准(真人语料,<br> 已还原为换行):qimao 长篇 5584 章 + heiyan 短篇整篇 3983 篇。
// 长篇章尺度(中位约 2100 字)per-kilo 1.0→1.5 误报 0.43%→0.39%,几乎不动;
// 短篇整篇 8000-20000 字下 MIN_HITS 形同虚设、只剩密度门,1.0 时误报 5.57%,
// 1.5 降到 1.46%。故取 1.5,把两个总体拉到同一量级(四份副本共用一组阈值)。
const STOCK_REACTION_PER_KILO = 1.5;

// 监控摄像头式动作清单:同一段连续堆叠通用动作动词(伸手/拿起/取过/挑开/放下/转身等),
// 且用逗号/顿号串联成步骤表时,读感像无视角温度的监控记录。只做 advisory;
// 打斗/追逐等功能性动作编排可保留或人工复核。
const ACTION_LIST_VERB_PATTERN = /伸手|抬手|探手|拿起|拿过|取出|取过|掏出|摸出|抓起|攥住|握住|捏住|按住|推开|拉开|打开|关上|放下|递给|挑开|掀开|扯开|拧开|倒出|端起|转身|回头|抬头|低头|弯腰|俯身|走到|走向|坐下|站起|看向|看着|盯着|扫过/g;
const ACTION_LIST_MIN_HITS = 5;
const ACTION_LIST_MIN_SEPARATORS = 4;

// 抽象总结复读:模板化段落常把角色当下经历拔成「命运/棋局/
// 这一刻终于明白/才刚刚开始」的作者总结。单个词可能服务题材;高密度聚集才报。
const ABSTRACT_SUMMARY_PATTERNS = [
  /这一刻[,,]?[^\n。!?!?]{0,24}(?:终于|才)(?:明白|意识到)/g,
  /从这一刻开始/g,
  /(?:命运|宿命)[^\n。!?!?]{0,28}(?:齿轮|棋局|獠牙|改写|推向|安排)/g,
  /早已[^\n。!?!?]{0,8}(?:布好|安排好)[^\n。!?!?]{0,8}(?:棋局|局)/g,
  /前所未有的(?:决意|清醒|勇气|力量|恐惧|平静|信念)/g,
  /(?:反击|复仇|战争|较量|故事|命运)[^\n。!?!?]{0,12}才刚刚开始/g,
  /(?:新的开始|全新的开始)/g,
];
const ABSTRACT_SUMMARY_MIN_HITS = 3;
const ABSTRACT_SUMMARY_PER_KILO = 4;

// 套词密度:单个「仿佛/一丝」可能是正常中文,高密度聚集才会形成模板腔。
// 词表只收本 repo banned-words 中已明确标为高危的形态,避免把普通功能词一网打尽。
const CLICHE_PATTERNS = [
  /仿佛|犹如|宛若|如同/g,
  /一丝|一抹|些许|几分|隐约/g,
  /深吸一口气|缓缓|微微|轻轻|淡淡/g,
  /眼中闪过|嘴角勾起|眸光微微一闪|指节泛白|目光锐利|眼神锐利/g,
  /心中涌起一股|心头一震|心中一动|心下了然|心中暗道|心中一凛/g,
  /不容置疑|不容置喙|不易察觉|显而易见|毫无疑问|不可否认/g,
  /声音不大[,,]?却带着|语气平静无波|平静无波|声音平直|听不出情绪/g,
  /不知何时|唾手可得|无声翻涌|沉默(?:在[^。!?!?\n]{0,16})?蔓延|难以言说/g,
  /散发着一股|冰冷的光|格外刺眼|深邃而冰冷/g,
];
const CLICHE_DENSITY_MIN_HITS = 8;
const CLICHE_DENSITY_PER_KILO = 12;

// 比喻密度:单个生活化比喻可服务画面;“像/好像/仿佛/如同”成片复现时,
// 容易变成 AI 式修辞堆叠。只做 advisory,修法是删到必要数量并回到具体画面,
// 不是把“像”换成另一组比喻词。
const METAPHOR_MARKER_PATTERN = /好像|像是|仿佛|宛如|如同|犹如|(?<![不头图画影录摄肖])像(?![头像素])/g;
const METAPHOR_LIKE_PHRASE_PATTERN = /(?:死|水|冰|火|潮水|石头|木头|机器|纸|铁|鬼|死人|刀|针|网|墙)一样/g;
const METAPHOR_DENSITY_MIN_HITS = 7;
const METAPHOR_DENSITY_PER_KILO = 3;

// 解释链密度:常见“他知道/他明白/这意味着/必须需要”
// 连续替读者推理,读感像报告。单个判断词可服务推理;高密度聚集才提示回到角色当下证据。
const REASONING_CHAIN_PATTERNS = [
  { key: 'mental', core: true, pattern: /(?<![不没未无])(?:他|她|我)?(?:知道|明白|意识到|清楚|判断|确认|分析)/g },
  { key: 'connector', core: true, pattern: /这意味着|也就是说|换句话说|真正的问题(?:在于)?|问题在于|关键在于|在这种情况下|按照这个逻辑|只有这样|想到这里/g },
  { key: 'modal', core: true, pattern: /(?:(?<!不)(?:必须|需要|应该|只要|就会|可能|可以|能够|无法)|不能)[^。!?!?\n]{0,16}(?:判断|确认|承担|维持|稳住|控制|扩大|失控|带来|造成|理解|默认|回家|进门|核对|筛选|减少|建立|风险|结果|秩序|责任)/g },
  { key: 'abstract', core: false, pattern: /(?:任务|条件|风险|来源|逻辑|局面|结果|责任|秩序|规则|信息不足|决策能力)/g },
];
const REASONING_CHAIN_MIN_HITS = 8;
const REASONING_CHAIN_CORE_MIN_HITS = 4;
const REASONING_CHAIN_MIN_BUCKETS = 2;
const REASONING_CHAIN_PER_KILO = 18;

// 系统公告公文腔:只看成片方括号规则/面板行里的硬规则词。
// 这不是特定题材词表;单条严肃规则、日常叙述或普通对话不触发。
const NOTICE_FORMAL_PATTERNS = [
  /不得|必须|不可|禁止|严禁|应当|须|需|务必/g,
  /当前|本公告|本规则|本系统|提示|任务失败|临时权限|权限|状态|等级/g,
  /维持|公共区域|秩序|优先|惩罚|处罚|违规|指令|执行/g,
  /被视为|同样计入|计入|承担|责任|单位|撤回|转发|截图/g,
];
const NOTICE_FORMAL_CORE_PATTERN = /不得|必须|不可|禁止|严禁|应当|须|需|务必|被视为|同样计入|计入/g;
const NOTICE_FORMAL_MIN_LINES = 4;
const NOTICE_FORMAL_MIN_HITS = 12;
const NOTICE_FORMAL_CORE_MIN_HITS = 5;
const NOTICE_FORMAL_PER_KILO = 60;

// 过度精炼短段:过度处理样本里常见大量 15 字以内叙述段,且“的/了/就/着/过/呢/吧/啊”等
// 自然连接偏少;对照文本通常保留更多自然连接。此项只做 advisory,禁止机械注水。
const OVERCOMPRESSED_PROSE_PARTICLE_PATTERN = /[的了就着过呢吧啊呀嘛]/g;
const OVERCOMPRESSED_PROSE_MIN_CHARS = 1200;
const OVERCOMPRESSED_PROSE_MIN_PARAS = 45;
const OVERCOMPRESSED_PROSE_SHORT_MAX_CHARS = 15;
const OVERCOMPRESSED_PROSE_SHORT_RATIO = 0.58;
const OVERCOMPRESSED_PROSE_PARTICLE_PER_KILO = 85;

// 低连接密度:单纯低功能词会误抓有大量中长句的文本;
// 因此必须叠加“中长句不足”,并只看引号外叙述。这是 overcompressed 的短窗口补充,只做 advisory。
const LOW_CONNECTIVE_FUNCTION_TERMS = ['的', '了', '就', '在', '是', '也', '都', '还', '又', '把', '被', '给', '这个', '那个', '里面', '以后', '时候', '现在', '因为', '所以', '但是', '不过', '然后', '已经', '还是', '起来', '出来', '下去'];
const LOW_CONNECTIVE_PLAIN_TERMS = ['的', '了', '就', '也', '还', '又', '这个', '那个', '东西', '事情', '时候', '里面', '以后', '一下', '一点', '有点', '还是'];
const LOW_CONNECTIVE_MIN_CHARS = 800;
const LOW_CONNECTIVE_FUNCTION_PER_KILO = 100;
const LOW_CONNECTIVE_PLAIN_PER_KILO = 65;
const LOW_CONNECTIVE_LONG_SENTENCE_CHARS = 30;
const LOW_CONNECTIVE_LONG_SENTENCE_RATIO = 0.08;

// either-or「不是A就是B / 不是A也是B」里紧贴的「是」是连词的一部分,不是肯定项系动词。
// 含「不」以沿用「不是A,也不是B」第二个否定段不算翻转的旧排除。
const COMPACT_EITHER_OR_PREV = new Set(['不', '就', '也']);
// 句尾语气/反问助词;「…,是吗 / 是吧 / 是嘛」是反问尾巴,不是否定后的肯定翻转。
const TAG_PARTICLES = new Set(['吗', '吧', '嘛']);
// 段首确认语;「不是第一次来。是的,他还记得……」里的「是的/是啊」
// 是承接确认,不是「不是 A,是 B」的肯定翻转。
const AFFIRMATION_TAG_PARTICLES = new Set(['的', '啊', '呀', '呢']);
const AFFIRMATION_TAG_BOUNDARY = new Set(['', ',', ',', '。', '.', '!', '!', '?', '?', '、', ';', ';', ':', ':', '\n', '\r', '\t', ' ']);

// 成对引号(台词/系统播报/弹幕)的字符对,stripQuoted 与 quotedRanges 共用一份来源。
// 引号片段一律不跨行(字符类里排掉 \n):正文漏一个收引号很常见(多段台词只在末段收尾、
// 全半角引号混用都会漏),若允许跨行配对,一个未闭合的开引号会把后面成百上千字全算成
// 「引号内」,让 quotedRanges 的消费方(not-is 跨行扫描)把整段叙述静默豁免掉。
const QUOTE_PAIRS = [['「', '」'], ['『', '』'], ['【', '】'], ['“', '”'], ['‘', '’'], ['"', '"'], ["'", "'"]];
const QUOTE_SOURCES = QUOTE_PAIRS.map(([open, close]) => `${escapeRegExp(open)}[^${escapeRegExpCharClass(close)}\\n]*${escapeRegExp(close)}`);

// ---- 实战测试漏网句式(来源:实战写作抓到的真实漏网例句;2026-07 校准)----
// 校准基线:《万疆》真人正文 20 章(第1/10/20/…/190章)+ demo 前 20 章。
// blocking 规则要求真人语料命中 ≈0(每 20 章 ≤1 处且人工判定确属该句式);数据见各规则注释。

// 音量反差腔(实战漏网 A):「声音不高,第一句却稳稳压住了整个大厅。」
// 旧网只有套词密度桶里的「声音不大,却带着」,音量词/转折词一换就漏。
// 引号外叙述逐处 blocking;修法是删掉音量铺垫,直接写声音落进场子的具体效果。
// 校准:《万疆》20 章 0 命中,demo 前 20 章 0 命中。
const VOICE_CONTRAST_PATTERN = /声音(?:并)?不[大高响亮][^。!?!?\n]{0,16}[却但偏]/g;

// 否定排比(实战漏网 B):「没有伴奏,没有和声,没有提词器。」同句 ≥2 个「没有X,」连排;
// 变体「他没炫技,没有那种…架势。他只是唱」先否定铺垫、再用「只是/只会/只有」收肯定。
// 只收「没/没有」段,不收「不X」段——真人叙述里「不哭不闹」类太常见,收进来误报换不来收益。
// 光杆「没」还得挡两类非否定用法,否则正常叙述会被判成排比:
//   1) 黏着语素(沉没/淹没/埋没/出没/隐没…)——前字排除,「船沉没在雾里,没人回头,…只有…」不算;
//   2) 时间惯用语(没多久/没过多久/没等X)——后字排除,「没多久,没等她撑伞,…只有…」不算。
// 「没有X」段不带这两种歧义(黏着语素后接不出「有」,时间惯用语已被后字排除覆盖),
// 第一条连排式照旧不加护栏。
// 校准:《万疆》20 章 0 命中,demo 前 20 章 0 命中。
const NEGATION_PARADE_PATTERNS = [
  /(?:没有[^。!?!?\n,,]{1,12}[,,]){2}/g,
  /(?<![沉淹埋出隐湮吞覆漫泯])没(?!有?过?多久)(?:有)?[^。!?!?\n,,]{1,12}[,,]\s*没(?!有?过?多久)(?:有)?[^。!?!?\n,,]{1,16}[,,。.][^。!?!?\n,,]{0,6}只(?:是|会|有)/g,
];
const CROSS_NEGATION_START = /^不是[^。!?!?\n]{1,24}[。!?!?]?$/;
const CROSS_NEGATION_MIDDLE = /^(?:也|还)不是[^。!?!?\n]{1,24}[。!?!?]?$/;
const CROSS_NEGATION_END = /^只是[^。!?!?\n]{1,32}[。!?!?]?$/;

// 两类常见但不能直接判错的工整框架,只做 advisory。与 blocking 规则不同,这里故意扫描
// 台词:自然点单「不放辣,不放葱」靠对象最短长度排除;更长的同动词清单交语义审查判断功能。
const DECISION_FRAME_PATTERN = /至于([\u3400-\u9fff]{1,3})不\1[,,]\s*怎么\1/g;
const REPEATED_NEGATIVE_VERB_PATTERN = /不([\u3400-\u9fff]{1,2})([\u3400-\u9fff]{2,8})[,,]\s*不\1([\u3400-\u9fff]{2,8})/g;

// 反序对比腔(实战漏网 C):「是真嗓子,不是修音修出来的」——not-is-comparison 的反序变种。
// 复用 not-is 的排除基建:引号内剥离(maskQuoted)、「是的/是啊」确认语(isAffirmationTagAt);
// 前字排除从 either-or 的 不/就/也 扩展到全部「X是」连词/副词合成词(还是/只是/可是/但是/
// 于是/倒是/像是/若是/要是/正是/便是/总是/老是/更是/最是/算是/怕是/凡是/或是/即是/自是/
// 竟是/原是/本是/仍是/许是/净是/光是/单是/尽是);「是不是」问句起头与「不是吗/不是么/
// 不是吧」反问尾巴单独排除。
// 校准:《万疆》20 章 0 命中,demo 前 20 章 0 命中,按 blocking 实现。
const REVERSE_NOT_IS_PATTERN = /是([^。!?!?\n,,]{1,12})[,,]\s*(?:而)?不是([^。!?!?\n]{1,20})/g;
const REVERSE_NOT_IS_PREV_EXCLUDE = new Set([...COMPACT_EITHER_OR_PREV, '还', '只', '可', '但', '于', '倒', '像', '若', '要', '正', '便', '总', '老', '更', '最', '算', '怕', '凡', '或', '即', '自', '竟', '原', '本', '仍', '许', '净', '光', '单', '尽']);

// 预告式总结收尾(实战漏网 D):「没人知道,这才刚刚开头。」「一场…震惊接力,正朝着…缓缓压了过去。」
// 章尾替读者预告下一章走向是 AI 收尾腔。只扫文末窗口(剥引号后可见字数,按行取整),
// 正文中段的「没人知道」多为普通叙述,不误伤;引号内台词(「没人知道…」)不计。
// 「正式拉开序幕/帷幕」是场内事件的报幕式陈述(真人语料「钟声再度响起,比赛正式拉开序幕」),
// 不是叙述者预告,前置 lookbehind 排除。
// 校准:《万疆》20 章排除「正式拉开序幕」2 处报幕句后 0 命中,demo 前 20 章 0 命中。
const TRAILER_ENDING_PATTERN = /没人知道|谁也不知道|谁也没想到|殊不知|(?:这)?才刚刚开(?:始|头)|正(?:朝着|向着)[^。!?!?\n]{0,24}(?:压|涌|袭|逼)(?:了?过去|了?过来|来)|(?<!正式)拉开(?:序幕|帷幕)|即将(?:开始|来临|降临)/g;
const TRAILER_ENDING_WINDOW_CHARS = 600;

// 章尾状态总结体:把细纲「结尾设定/收束状态」原样写成总结句收章(「这一夜注定无人入眠」
// 「这一切都结束了」「新的人生才刚刚开始」「命运的齿轮」)。与 trailer-ending 共用文末窗口,
// 区别是它盖章过去、trailer-ending 预告将来;收的都是 banned-words 已按名禁掉的形态。
// 不收「(这|那)一刻…终于明白」:真人语料里那是正常的认知节拍,短篇第一人称审判句还是卖点
// (short-craft「审判金句 / 心死余韵」),密度型由 advisory 的 abstract-summary-tic 兜。
// 各分支都要求落在句末断言位,否则会吃进条件从句(等这一切结束了,我们就…)、动补
// (这一切都说明得非常清楚)、成语跨匹配(这一刻…命中注定)、系表(这一战的结果是注定的)、
// 及物用法(就这样…才结束了这个话题)、场内报幕(就这样…宣布…圆满落幕)和否定认知
// (他不知道这一切意味着什么)——最后一类靠 (?!什么) 排掉间接疑问,那是盖章的反面。
// 校准(文末 600 字窗口,命中逐条人工复核):qimao 章中段 20000 章命中 1 处(0.005%)、
// heiyan 整篇 3999 篇命中 22 处(0.550%,全部是上列禁用形态);同批既有 trailer-ending
// 分别命中 1.345% / 6.602%——本规则误报面显著小于已上线的同窗口规则。短篇整篇即收口,
// 基线天然高于长篇章中段,故两个总体分别报数。
const TRAILER_SUMMARY_PATTERN = /这一(?:夜|天|刻|战|年|局|役)[,,]?[^。!?!?,,\n]{0,6}(?<!命中)(?<!是)注定[^。!?!?\n]{0,8}[。!]|就这样[,,][^。!?!?,,\n]{0,8}(?:一切|全部)[^。!?!?,,\n]{0,4}(?:结束了|落幕|收场)[。!]|这一切[,,]?[^。!?!?,,\n]{0,6}(?:都)?(?:说明|意味着|结束了)(?!的)(?:(?!什么)[^。!?!?\n]){0,6}[。!]|(?:新的篇章|新的旅程|崭新的篇章|新的人生)[^。!?!?\n]{0,6}(?:开始|拉开|展开)|命运[^。!?!?\n]{0,6}齿轮/g;

// 引号强调滥用(实战漏网 E,advisory 密度型,风格照 metaphor-density-tic):
// 叙述里短词加引号强调(他是被请来"把关"的)。只数叙述层 1-4 字成对引号片段;
// 排除项:【】系统面板载体、引语动词(说|道|问|喊|答|念|叫|回|吼|嘀咕,加细 骂|写|读|唱)
// 前 6 字/后 3 字邻接的极短台词、引号内含句读的台词、引号外无叙述的行(独立台词/
// 弹幕流/拟声词连发)、引号套引号(台词内强调)。全文 ≥3 处报一条——单处强调是
// 正常修辞,密度高才是模板腔。
// 校准:demo 前 20 章 0 章过阈值;《万疆》20 章 2 章过阈值(海报标语“我在番城”系列、
// “邀战书”等转述载体,真人也这么写),所以该规则只做 advisory,不升 blocking。
const QUOTE_EMPHASIS_MIN_HITS = 3;
const QUOTE_EMPHASIS_MAX_VISIBLE = 4;
const QUOTE_EMPHASIS_SPEECH_VERB_PATTERN = /[说道问喊答念叫回吼骂写读唱嘀咕]/;

const options = {
  json: false,
  files: [],
  failOn: 'all',
};

for (let i = 2; i < process.argv.length; i += 1) {
  const arg = process.argv[i];
  if (arg === '--check') {
    // Accepted for symmetry with normalize-punctuation.js; detection is always check-only.
  } else if (arg === '--json') {
    options.json = true;
  } else if (arg.startsWith('--fail-on=')) {
    const v = arg.slice('--fail-on='.length);
    if (v !== 'blocking' && v !== 'all') die(`--fail-on must be 'blocking' or 'all'`);
    options.failOn = v;
  } else if (arg === '-h' || arg === '--help') {
    process.stdout.write(`${USAGE}\n`);
    process.exit(0);
  } else if (arg.startsWith('-')) {
    die(`Unknown option: ${arg}`);
  } else {
    options.files.push(arg);
  }
}

if (options.files.length === 0) {
  die('No files provided');
}

let failed = false;
const allFindings = [];

for (const file of options.files) {
  const fullPath = path.resolve(file);
  let input;
  try {
    input = fs.readFileSync(fullPath, 'utf8');
  } catch (error) {
    failed = true;
    if (!options.json) console.error(`${file}: unable to read (${error.message})`);
    continue;
  }

  const findings = scanDocument(input).map((finding) => ({ file, ...finding }));
  allFindings.push(...findings);
}

if (options.json) {
  process.stdout.write(`${JSON.stringify({ findings: allFindings }, null, 2)}\n`);
} else {
  for (const finding of allFindings) {
    console.log(`${finding.file}:${finding.line}:${finding.column}: [${finding.severity}] ${finding.type}: ${finding.message} (${finding.excerpt})`);
  }
}

if (failed) process.exit(2);
// --fail-on=blocking 只在出现 blocking finding 时退出 1(advisory 仅报告);默认 all 沿用「有任何 finding 即 1」。
const hasBlocking = allFindings.some((f) => f.severity === 'blocking');
if (options.failOn === 'blocking' ? hasBlocking : allFindings.length > 0) process.exit(1);

function escapeRegExp(text) {
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function escapeRegExpCharClass(text) {
  return text.replace(/[\\\]^-]/g, '\\$&');
}

function die(message) {
  console.error(message);
  console.error(USAGE.trimEnd());
  process.exit(2);
}

function scanDocument(input) {
  const lines = input.split(/\r?\n/);
  const findings = [];
  let fence = null;
  let inFrontMatter = hasYamlFrontMatter(lines);
  let block = [];
  const proseLines = [];

  const flushBlock = () => {
    if (block.length === 0) return;
    findings.push(...scanBlock(block));
    block = [];
  };

  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index];
    const trimmed = line.trim();

    if (inFrontMatter) {
      if (index > 0 && trimmed === '---') inFrontMatter = false;
      continue;
    }

    const fenceMarker = parseFenceMarker(trimmed);
    if (fence) {
      if (fenceMarker && fenceMarker.char === fence.char && fenceMarker.length >= fence.length) {
        fence = null;
      }
      continue;
    }

    if (fenceMarker) {
      flushBlock();
      fence = fenceMarker;
      continue;
    }

    block.push({ text: line, lineNo: index + 1 });
    proseLines.push({ text: line, lineNo: index + 1 });
  }

  flushBlock();
  findings.push(...scanProsePatterns(proseLines));
  findings.sort((a, b) => a.line - b.line || a.column - b.column);
  return findings;
}

// 段落级检测:碎句号(连续短叙述句)、长段落、破折号(按功能改写,非机械替换)。
function scanProsePatterns(proseLines) {
  const findings = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;

    const dashPattern = /——|—|--+/g;
    let dash;
    while ((dash = dashPattern.exec(text)) !== null) {
      findings.push({
        line: lineNo,
        column: dash.index + 1,
        type: 'em-dash',
        severity: 'blocking',
        message: '破折号按功能改写:打断→动作 beat/短句,拖长音→省略或动作,插入说明→逗号/冒号;勿一律改句号。',
        excerpt: compact(text.slice(Math.max(0, dash.index - 8), dash.index + dash[0].length + 8)),
      });
    }

    if (trimmed.length > LONG_PARAGRAPH_CHARS) {
      findings.push({
        line: lineNo,
        column: 1,
        type: 'long-paragraph',
        severity: 'advisory',
        message: `段落过长(${trimmed.length} 字):按镜头/新动作/新线索/视线切换断段,别一段到底。`,
        excerpt: compact(trimmed.slice(0, 40)),
      });
    }
  }

  findings.push(...findVoiceContrast(proseLines));
  findings.push(...findNegationParade(proseLines));
  findings.push(...findFormulaicParallelism(proseLines));
  findings.push(...findReverseNotIs(proseLines));
  findings.push(...findTrailerEnding(proseLines));
  findings.push(...findQuoteEmphasisTic(proseLines));
  findings.push(...findPeriodStutter(proseLines));
  findings.push(...findMicroActionTic(proseLines));
  findings.push(...findStockReactionTic(proseLines));
  findings.push(...findActionListTic(proseLines));
  findings.push(...findAbstractSummaryTic(proseLines));
  findings.push(...findClicheDensityTic(proseLines));
  findings.push(...findMetaphorDensityTic(proseLines));
  findings.push(...findReasoningChainTic(proseLines));
  findings.push(...findNoticeFormalityTic(proseLines));
  findings.push(...findOvercompressedProseTic(proseLines));
  findings.push(...findLowConnectiveDensityTic(proseLines));
  return findings;
}

// 音量反差腔(实战漏网 A):引号外叙述逐处 blocking,位置与摘录取自原文
// (maskQuoted 等长占位保偏移;命中片段不含问号占位符,故不会落进占位区)。
function findVoiceContrast(proseLines) {
  const findings = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const masked = maskQuoted(text);
    VOICE_CONTRAST_PATTERN.lastIndex = 0;
    let match;
    while ((match = VOICE_CONTRAST_PATTERN.exec(masked)) !== null) {
      findings.push({
        line: lineNo,
        column: match.index + 1,
        type: 'voice-contrast',
        severity: 'blocking',
        message: '音量反差腔:「声音不大/不高…却/但…」是 AI 高频反差模板;删掉音量铺垫,直接写声音落进场子的具体效果(谁停了手、哪排安静了)。',
        excerpt: compact(text.slice(match.index, match.index + match[0].length)),
      });
    }
  }

  return findings;
}

// 否定排比(实战漏网 B):同句「没有X,」连排 / 先否定后「只是」收肯定。
// 可能在同一片文字上重叠命中,按区间去重只报一次。
function findNegationParade(proseLines) {
  const findings = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const masked = maskQuoted(text);

    const spans = [];
    for (const pattern of NEGATION_PARADE_PATTERNS) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(masked)) !== null) {
        spans.push([match.index, match.index + match[0].length]);
      }
    }
    spans.sort((a, b) => a[0] - b[0]);

    let lastEnd = -1;
    for (const [start, end] of spans) {
      if (start < lastEnd) {
        lastEnd = Math.max(lastEnd, end);
        continue;
      }
      lastEnd = end;
      findings.push({
        line: lineNo,
        column: start + 1,
        type: 'negation-parade',
        severity: 'blocking',
        message: '否定排比:「没有X,没有Y…」/「没X,没有Y,只是Z」是 AI 高频排比模板;删掉否定清单,直接写现场实际有什么,最多留一个最有信息量的否定。',
        excerpt: compact(text.slice(start, end)),
      });
    }
  }

  return findings;
}

function findFormulaicParallelism(proseLines) {
  const findings = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    for (const [pattern, message] of [
      [DECISION_FRAME_PATTERN, '「至于X不X,怎么X」把同一决定拆成工整栏目;若只是复述细纲,压成角色当下的一次判断或直接动作。'],
      [REPEATED_NEGATIVE_VERB_PATTERN, '同动词「不V A,不V B」容易写成否定清单;含台词也要按语境复核,保留真正有功能的一项即可。'],
    ]) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(text)) !== null) {
        findings.push({
          line: lineNo,
          column: match.index + 1,
          type: 'formulaic-parallelism',
          severity: 'advisory',
          message,
          excerpt: compact(match[0]),
        });
      }
    }
  }

  // 跨段「不是A / 也不是B / 只是C」既可能是细纲复述,也可能是正常的
  // 辩解、悬念排除或情绪递进。纯句法无法稳定区分,因此只给 advisory,交给语义复核。
  const window = [];
  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed) continue;
    if (isDivider(trimmed) || isStructural(trimmed)) {
      window.length = 0;
      continue;
    }
    if (window.length && lineNo - window[window.length - 1].lineNo > 2) window.length = 0;
    window.push({ text: maskQuoted(trimmed), original: trimmed, lineNo });
    if (window.length > 3) window.shift();
    if (window.length !== 3) continue;
    if (!CROSS_NEGATION_START.test(window[0].text)
      || !CROSS_NEGATION_MIDDLE.test(window[1].text)
      || !CROSS_NEGATION_END.test(window[2].text)) continue;
    findings.push({
      line: window[0].lineNo,
      column: 1,
      type: 'formulaic-parallelism',
      severity: 'advisory',
      message: '跨段「不是… / 也不是… / 只是…」可能是工整否定铺排,也可能承担辩解或悬念排除;通读语境,只在重复细纲或拖慢画面时改写。',
      excerpt: compact(window.map((entry) => entry.original).join(' / ')),
    });
  }

  return findings;
}

// 反序对比腔(实战漏网 C):「是A,不是B」。排除基建复用 not-is-comparison:
// 引号内剥离、「是的/是啊」确认语;前字合成词与反问尾巴见 REVERSE_NOT_IS_PREV_EXCLUDE 注释。
function findReverseNotIs(proseLines) {
  const findings = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const masked = maskQuoted(text);
    REVERSE_NOT_IS_PATTERN.lastIndex = 0;
    let match;
    while ((match = REVERSE_NOT_IS_PATTERN.exec(masked)) !== null) {
      const start = match.index;
      // 「就是/也是/还是/只是/可是…」里的「是」是合成词一部分,不是肯定项系动词。
      if (REVERSE_NOT_IS_PREV_EXCLUDE.has(masked[start - 1])) continue;
      // 「是不是…」问句起头。
      if (masked[start + 1] === '不') continue;
      // 「是的,…不是…」承接确认语(复用 not-is 的判定)。
      if (isAffirmationTagAt(masked, start)) continue;
      // 「…,不是吗/不是么/不是吧」反问尾巴。
      if (/^[吗么吧]/.test(match[2])) continue;
      findings.push({
        line: lineNo,
        column: start + 1,
        type: 'reverse-not-is',
        severity: 'blocking',
        message: '反序对比腔:「是A,不是B」与「不是A,是B」同族;删掉后置否定,直接写 A 的具体表现,或用细节让读者自己对比。',
        excerpt: compact(text.slice(start, start + match[0].length)),
      });
    }
  }

  return findings;
}

// 预告式总结收尾(实战漏网 D):只扫文末窗口。从文末往回收集叙述行,
// 直到剥引号后的可见字数达到窗口大小(按行取整,边界行整行计入)。
function findTrailerEnding(proseLines) {
  const windowLines = [];
  let accumulated = 0;

  for (let i = proseLines.length - 1; i >= 0 && accumulated < TRAILER_ENDING_WINDOW_CHARS; i -= 1) {
    const { text } = proseLines[i];
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    windowLines.unshift(proseLines[i]);
    accumulated += visibleLength(stripQuoted(trimmed));
  }

  const findings = [];
  for (const { text, lineNo } of windowLines) {
    const masked = maskQuoted(text);
    TRAILER_ENDING_PATTERN.lastIndex = 0;
    let match;
    while ((match = TRAILER_ENDING_PATTERN.exec(masked)) !== null) {
      findings.push({
        line: lineNo,
        column: match.index + 1,
        type: 'trailer-ending',
        severity: 'blocking',
        message: '预告式总结收尾:「没人知道/才刚刚开始/正朝着…压了过去」是 AI 章尾预告腔;结尾停在具体动作、画面或一句台词上,悬念让事件自己挂住,别替读者预告下一章。',
        excerpt: compact(text.slice(match.index, match.index + match[0].length)),
      });
    }
    TRAILER_SUMMARY_PATTERN.lastIndex = 0;
    let summaryMatch;
    while ((summaryMatch = TRAILER_SUMMARY_PATTERN.exec(masked)) !== null) {
      findings.push({
        line: lineNo,
        column: summaryMatch.index + 1,
        type: 'trailer-summary',
        severity: 'blocking',
        message: '章尾状态总结体:「这一夜注定…/这一切都结束了/新的人生才刚刚开始/命运的齿轮」是把细纲的收束状态原样写成了总结句;收束状态是规划口径,正文落到最后一个具体动作、画面或台词上,别替读者盖章。',
        excerpt: compact(text.slice(summaryMatch.index, summaryMatch.index + summaryMatch[0].length)),
      });
    }
  }

  return findings;
}

// 引号强调滥用(实战漏网 E):统计叙述层 1-4 字成对引号强调片段,全文只报一条
// (密度型分布指纹)。台词类排除见 QUOTE_EMPHASIS_* 常量注释。
function findQuoteEmphasisTic(proseLines) {
  let hits = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    // 引号外没有叙述的行(独立台词/弹幕流/拟声词连发「“叮咚~”“叮咚~”」)整行跳过:
    // 强调滥用是叙述层指纹,没有叙述就无所谓强调。
    if (visibleLength(stripQuoted(trimmed)) === 0) continue;
    const ranges = quotedRanges(text);

    for (const [start, end] of ranges) {
      if (text[start] === '【') continue; // 系统面板/公告载体,不是强调引号
      // 引号套引号:台词内部的强调属于角色语言,不算叙述层强调滥用。
      if (ranges.some(([s2, e2]) => s2 <= start && end <= e2 && (s2 !== start || e2 !== end))) continue;
      const inner = text.slice(start + 1, end - 1);
      const visible = visibleLength(inner);
      if (visible < 1 || visible > QUOTE_EMPHASIS_MAX_VISIBLE) continue;
      if (/[。!?!?…,,;;::]/.test(inner)) continue; // 含句读的是台词/播报,不是强调
      const before = text.slice(Math.max(0, start - 6), start);
      const after = text.slice(end, end + 3);
      if (QUOTE_EMPHASIS_SPEECH_VERB_PATTERN.test(before) || QUOTE_EMPHASIS_SPEECH_VERB_PATTERN.test(after)) continue; // 引语动词邻接=极短台词
      hits += 1;
      if (firstLine === null) firstLine = lineNo;
      if (samples.length < 6 && !samples.includes(inner)) samples.push(inner);
    }
  }

  if (hits < QUOTE_EMPHASIS_MIN_HITS) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'quote-emphasis-tic',
    severity: 'advisory',
    message: `引号强调滥用:叙述里 1-4 字短词加引号强调 ${hits} 处;只留真正反讽/转述必要的一两处,其余去掉引号直接写,或换成具体动作让读者自己品。`,
    excerpt: compact(samples.join(' ')),
  }];
}

// 微动作复读:统计引号外叙述里「了X量词」轻量补语的密度。次数与每千字密度双门槛,
// 全文只报一条(这是分布级指纹,不是逐处问题)。
function findMicroActionTic(proseLines) {
  let hits = 0;
  let narrativeChars = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed);
    narrativeChars += visibleLength(narrative);
    MICRO_TIC_PATTERN.lastIndex = 0;
    let match;
    while ((match = MICRO_TIC_PATTERN.exec(narrative)) !== null) {
      hits += 1;
      if (firstLine === null) firstLine = lineNo;
      if (samples.length < 6 && !samples.includes(match[0])) samples.push(match[0]);
    }
  }

  if (narrativeChars === 0 || hits < MICRO_TIC_MIN_HITS) return [];
  const perKilo = (hits / narrativeChars) * 1000;
  if (perKilo < MICRO_TIC_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'micro-action-tic',
    severity: 'advisory',
    message: `微动作复读:「了下/了一下」式轻量补语 ${hits} 处(${perKilo.toFixed(1)}/千字);同一反应模板高密度复现是机械指纹,合并动作 beat、换具体细节,别每个动作都补一个轻反应尾巴。`,
    excerpt: compact(samples.join(' ')),
  }];
}

// 套式反应细节:统计引号外叙述中通用的部位/声线反应与固定语气比喻。
// 这是删除测试的候选集,不是身体描写黑名单;全篇只报一条,保留有动作后果、
// 伤势、人物习惯或情节功能的细节。
function findStockReactionTic(proseLines) {
  let hits = 0;
  let narrativeChars = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed);
    narrativeChars += visibleLength(narrative);

    for (const pattern of STOCK_REACTION_PATTERNS) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(narrative)) !== null) {
        hits += 1;
        if (firstLine === null) firstLine = lineNo;
        const sample = sentenceAround(narrative, match.index);
        if (samples.length < 6 && sample && !samples.includes(sample)) samples.push(sample);
      }
    }
  }

  if (narrativeChars === 0 || hits < STOCK_REACTION_MIN_HITS) return [];
  const perKilo = (hits / narrativeChars) * 1000;
  if (perKilo < STOCK_REACTION_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'stock-reaction-tic',
    severity: 'advisory',
    message: `套式反应细节:指尖/指节/喉结/眼圈/声音放轻等通用反应或“平静得像在念”式语气比喻 ${hits} 处(${perKilo.toFixed(1)}/千字);逐处做删除测试,只标注情绪、不改变选择、关系、物件或动作结果的删掉,不要换部位或同义动作。`,
    excerpt: compact(samples.join(' | ')),
  }];
}

function findActionListTic(proseLines) {
  const findings = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed).trim();
    if (!narrative) continue;

    ACTION_LIST_VERB_PATTERN.lastIndex = 0;
    const verbs = [];
    let match;
    while ((match = ACTION_LIST_VERB_PATTERN.exec(narrative)) !== null) {
      verbs.push(match[0]);
    }

    if (verbs.length < ACTION_LIST_MIN_HITS) continue;
    const separators = (narrative.match(/[,、;;]/g) || []).length;
    if (separators < ACTION_LIST_MIN_SEPARATORS) continue;

    findings.push({
      line: lineNo,
      column: 1,
      type: 'action-list-tic',
      severity: 'advisory',
      message: `监控摄像头式动作清单:同段连续动作动词 ${verbs.length} 个、分隔符 ${separators} 个;合并琐碎步骤,只保留有情绪/情节功能的动作,必要时用角色犹豫、误判或环境反馈做缓冲。`,
      excerpt: compact(verbs.slice(0, 8).join(' ')),
    });
  }

  return findings;
}

// 套词密度:统计引号外叙述中的高危禁用词聚集。不是逐词替换器;只在密度高到
// 形成模板腔时提示,修法是删总结、换具体动作/物件/对话,不是同义词轮换。
function findClicheDensityTic(proseLines) {
  let hits = 0;
  let narrativeChars = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed);
    narrativeChars += visibleLength(narrative);

    for (const pattern of CLICHE_PATTERNS) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(narrative)) !== null) {
        hits += 1;
        if (firstLine === null) firstLine = lineNo;
        if (samples.length < 8 && !samples.includes(match[0])) samples.push(match[0]);
      }
    }
  }

  if (narrativeChars === 0 || hits < CLICHE_DENSITY_MIN_HITS) return [];
  const perKilo = (hits / narrativeChars) * 1000;
  if (perKilo < CLICHE_DENSITY_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'cliche-density-tic',
    severity: 'advisory',
    message: `套词密度过高:高危 AI 套词 ${hits} 处(${perKilo.toFixed(1)}/千字);不要同义词轮换,改成角色当下可见的动作、物件、对话和具体后果。`,
    excerpt: compact(samples.join(' ')),
  }];
}

// 比喻密度:统计引号外叙述中“像/好像/仿佛/如同”等比喻标记。
// 单个比喻不是问题;高密度成片时才提示,避免把文本改成另一种修辞模板。
function findMetaphorDensityTic(proseLines) {
  let hits = 0;
  let narrativeChars = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed);
    narrativeChars += visibleLength(narrative);

    METAPHOR_MARKER_PATTERN.lastIndex = 0;
    let match;
    while ((match = METAPHOR_MARKER_PATTERN.exec(narrative)) !== null) {
      hits += 1;
      if (firstLine === null) firstLine = lineNo;
      const sample = sentenceAround(narrative, match.index);
      if (samples.length < 6 && sample && !samples.includes(sample)) samples.push(sample);
    }

    METAPHOR_LIKE_PHRASE_PATTERN.lastIndex = 0;
    while ((match = METAPHOR_LIKE_PHRASE_PATTERN.exec(narrative)) !== null) {
      const prefix = narrative.slice(Math.max(0, match.index - 8), match.index);
      if (/好像|像是|像|仿佛|宛如|如同|犹如/.test(prefix)) continue;
      hits += 1;
      if (firstLine === null) firstLine = lineNo;
      const sample = sentenceAround(narrative, match.index);
      if (samples.length < 6 && sample && !samples.includes(sample)) samples.push(sample);
    }
  }

  if (narrativeChars === 0 || hits < METAPHOR_DENSITY_MIN_HITS) return [];
  const perKilo = (hits / narrativeChars) * 1000;
  if (perKilo < METAPHOR_DENSITY_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'metaphor-density-tic',
    severity: 'advisory',
    message: `比喻密度过高:像/好像/仿佛/如同等比喻标记 ${hits} 处(${perKilo.toFixed(1)}/千字);保留最有叙事功能的少数比喻,其余回到具体动作、物件、声音或后果,不要换成新比喻。`,
    excerpt: compact(samples.join(' | ')),
  }];
}

// 解释链密度:统计引号外叙述中“知道/明白/这意味着/必须需要”等判断链。
// 全篇只报一条;修法不是补结构虚词,而是把判断落到动作、物件、对话和现场反馈。
function findReasoningChainTic(proseLines) {
  let hits = 0;
  let coreHits = 0;
  let narrativeChars = 0;
  let firstLine = null;
  const samples = [];
  const buckets = new Set();

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed);
    narrativeChars += visibleLength(narrative);

    for (const { pattern, key, core } of REASONING_CHAIN_PATTERNS) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(narrative)) !== null) {
        hits += 1;
        if (core) coreHits += 1;
        buckets.add(key);
        if (firstLine === null) firstLine = lineNo;
        const sample = compact(match[0]);
        if (samples.length < 8 && !samples.includes(sample)) samples.push(sample);
      }
    }
  }

  if (narrativeChars === 0 || hits < REASONING_CHAIN_MIN_HITS) return [];
  if (coreHits < REASONING_CHAIN_CORE_MIN_HITS || buckets.size < REASONING_CHAIN_MIN_BUCKETS) return [];
  const perKilo = (hits / narrativeChars) * 1000;
  if (perKilo < REASONING_CHAIN_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'reasoning-chain-tic',
    severity: 'advisory',
    message: `解释链密度过高:知道/明白/这意味着/必须/需要等判断链 ${hits} 处(${perKilo.toFixed(1)}/千字);像逻辑报告时,把判断落到角色当下可见的动作、物件、对话和现场反馈。`,
    excerpt: compact(samples.join(' | ')),
  }];
}

// 系统/规则行如果连续像 API 文档或政府公文,读者容易闻到机器味。
// 修法不是删除规则,而是保留功能后把一部分硬词改成白话或具体后果。
function findNoticeFormalityTic(proseLines) {
  let hits = 0;
  let noticeChars = 0;
  let noticeLines = 0;
  let coreHits = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!/^【[^】]+】$/.test(trimmed)) continue;
    noticeLines += 1;
    noticeChars += visibleLength(trimmed);

    NOTICE_FORMAL_CORE_PATTERN.lastIndex = 0;
    while (NOTICE_FORMAL_CORE_PATTERN.exec(trimmed) !== null) coreHits += 1;

    for (const pattern of NOTICE_FORMAL_PATTERNS) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(trimmed)) !== null) {
        hits += 1;
        if (firstLine === null) firstLine = lineNo;
        const sample = compact(match[0]);
        if (samples.length < 8 && !samples.includes(sample)) samples.push(sample);
      }
    }
  }

  if (noticeLines < NOTICE_FORMAL_MIN_LINES || noticeChars === 0 || hits < NOTICE_FORMAL_MIN_HITS || coreHits < NOTICE_FORMAL_CORE_MIN_HITS) return [];
  const perKilo = (hits / noticeChars) * 1000;
  if (perKilo < NOTICE_FORMAL_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'system-notice-formality-tic',
    severity: 'advisory',
    message: `系统公告公文腔过密:方括号规则行中硬规则词 ${hits} 处(${perKilo.toFixed(1)}/千字);保留为角色看见的屏幕/公告/规则载体,只在载体内部白话化部分硬词,或补角色当场看懂的具体后果,不改成叙述者解释。`,
    excerpt: compact(samples.join(' | ')),
  }];
}

// 长文本整体过于“精炼”:短段很多、自然连接偏少,读起来像处理过的梗概/分镜表。
// 修法是通读后补断裂处,不是为凑阈值全局加“的/了/就”。
function findOvercompressedProseTic(proseLines) {
  let narrativeChars = 0;
  let narrativeParas = 0;
  let shortParas = 0;
  let particles = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed) || /^【[^】]+】$/.test(trimmed)) continue;
    const narrative = stripQuoted(trimmed).trim();
    const len = visibleLength(narrative);
    if (len === 0) continue;

    if (firstLine === null) firstLine = lineNo;
    narrativeParas += 1;
    narrativeChars += len;
    if (len <= OVERCOMPRESSED_PROSE_SHORT_MAX_CHARS) {
      shortParas += 1;
      if (samples.length < 6) samples.push(narrative);
    }

    OVERCOMPRESSED_PROSE_PARTICLE_PATTERN.lastIndex = 0;
    while (OVERCOMPRESSED_PROSE_PARTICLE_PATTERN.exec(narrative) !== null) particles += 1;
  }

  if (narrativeChars < OVERCOMPRESSED_PROSE_MIN_CHARS || narrativeParas < OVERCOMPRESSED_PROSE_MIN_PARAS) return [];
  const shortRatio = shortParas / narrativeParas;
  if (shortRatio < OVERCOMPRESSED_PROSE_SHORT_RATIO) return [];
  const particlePerKilo = (particles / narrativeChars) * 1000;
  if (particlePerKilo >= OVERCOMPRESSED_PROSE_PARTICLE_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'overcompressed-prose-tic',
    severity: 'advisory',
    message: `过度精炼短段:叙述段 ${narrativeParas} 个,其中 ${shortParas} 个≤${OVERCOMPRESSED_PROSE_SHORT_MAX_CHARS}字(${(shortRatio * 100).toFixed(0)}%),自然连接 ${particlePerKilo.toFixed(1)}/千字偏少;先通读判断,确有提纲感再补断裂处和必要结构虚词,有意短镜头可留,别机械注水。`,
    excerpt: compact(samples.join(' | ')),
  }];

}

// 低连接密度:长文本/中短窗口里,引号外叙述的功能词和白话连接同时偏低,且缺少中长承接句,
// 会呈现“提纲/电报体”分布。修法是恢复必要连接和句群,不是全局补词。
function findLowConnectiveDensityTic(proseLines) {
  let bodyChars = 0;
  let functionHits = 0;
  let plainHits = 0;
  let firstLine = null;
  const sentences = [];
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;

    // 只看引号外叙述。台词/弹幕/系统播报可以天然短促,混入统计会把体裁特征误当电报体。
    const narrative = stripQuoted(trimmed).trim();
    const narrativeLen = visibleLength(narrative);
    if (narrativeLen === 0) continue;

    if (firstLine === null) firstLine = lineNo;
    bodyChars += narrativeLen;
    functionHits += countTerms(narrative, LOW_CONNECTIVE_FUNCTION_TERMS);
    plainHits += countTerms(narrative, LOW_CONNECTIVE_PLAIN_TERMS);

    for (const sentence of splitSentences(narrative)) {
      const len = visibleLength(sentence);
      if (len === 0) continue;
      sentences.push(len);
      if (len <= 12 && samples.length < 6) samples.push(sentence);
    }
  }

  if (bodyChars < LOW_CONNECTIVE_MIN_CHARS || sentences.length === 0) return [];
  const functionPerKilo = (functionHits / bodyChars) * 1000;
  if (functionPerKilo >= LOW_CONNECTIVE_FUNCTION_PER_KILO) return [];
  const plainPerKilo = (plainHits / bodyChars) * 1000;
  if (plainPerKilo >= LOW_CONNECTIVE_PLAIN_PER_KILO) return [];
  const longSentenceRatio = sentences.filter((len) => len >= LOW_CONNECTIVE_LONG_SENTENCE_CHARS).length / sentences.length;
  if (longSentenceRatio >= LOW_CONNECTIVE_LONG_SENTENCE_RATIO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'low-connective-density-tic',
    severity: 'advisory',
    message: `低连接密度:引号外叙述功能词 ${functionPerKilo.toFixed(1)}/千字、白话连接 ${plainPerKilo.toFixed(1)}/千字,且≥${LOW_CONNECTIVE_LONG_SENTENCE_CHARS}字承接句仅 ${(longSentenceRatio * 100).toFixed(0)}%;容易像提纲/电报体。通读后补必要连接和中长句群,别机械注水。`,
    excerpt: compact(samples.join(' | ')),
  }];
}

// 抽象总结复读:统计引号外叙述中的高抽象收束模板。全篇只报一条,提醒回到角色
// 当下可见的文件、动作、对话或物理后果;不要用命运大词替读者总结。
function findAbstractSummaryTic(proseLines) {
  let hits = 0;
  let narrativeChars = 0;
  let firstLine = null;
  const samples = [];

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed || isDivider(trimmed) || isStructural(trimmed)) continue;
    const narrative = stripQuoted(trimmed);
    narrativeChars += visibleLength(narrative);

    for (const pattern of ABSTRACT_SUMMARY_PATTERNS) {
      pattern.lastIndex = 0;
      let match;
      while ((match = pattern.exec(narrative)) !== null) {
        hits += 1;
        if (firstLine === null) firstLine = lineNo;
        const sample = compact(match[0]);
        if (samples.length < 6 && !samples.includes(sample)) samples.push(sample);
      }
    }
  }

  if (narrativeChars === 0 || hits < ABSTRACT_SUMMARY_MIN_HITS) return [];
  const perKilo = (hits / narrativeChars) * 1000;
  if (perKilo < ABSTRACT_SUMMARY_PER_KILO) return [];

  return [{
    line: firstLine,
    column: 1,
    type: 'abstract-summary-tic',
    severity: 'advisory',
    message: `抽象总结复读:命运/棋局/这一刻终于明白/才刚刚开始等作者总结 ${hits} 处(${perKilo.toFixed(1)}/千字);回到角色当下可见的文件、动作、对话或物理后果,别替读者盖章。`,
    excerpt: compact(samples.join(' | ')),
  }];
}

function findPeriodStutter(proseLines) {
  const findings = [];
  let runLen = 0;
  let runStartLine = null;
  let runSample = [];

  const flush = () => {
    if (runLen >= STUTTER_MIN_RUN) {
      findings.push({
        line: runStartLine,
        column: 1,
        type: 'period-stutter',
        severity: 'advisory',
        message: `碎句号:连续 ${runLen} 个短句无呼吸;按目标句长把碎句合并成中长句、补回画面与连接(见本 skill 句长/疏密节奏规则)。`,
        excerpt: compact(runSample.join(' ')),
      });
    }
    runLen = 0;
    runStartLine = null;
    runSample = [];
  };

  for (const { text, lineNo } of proseLines) {
    const trimmed = text.trim();
    if (!trimmed) continue; // 空行是一句一段排版,不打断叙述连贯
    if (isDivider(trimmed) || isStructural(trimmed)) {
      flush(); // 分隔线/markdown 结构行:重置碎句计数
      continue;
    }
    const narrative = stripQuoted(trimmed);
    if (visibleLength(narrative) === 0) {
      flush(); // 纯对话/弹幕/系统播报:成片短句是正常形态,重置碎句计数
      continue;
    }
    // 只数引号外叙述句:混合行(叙述+引号内物件/短台词)的引号外片段仍参与碎句计数。
    for (const sentence of splitSentences(narrative)) {
      if (visibleLength(sentence) <= STUTTER_MAX_SENTENCE) {
        if (runLen === 0) runStartLine = lineNo;
        runLen += 1;
        if (runSample.length < 6) runSample.push(sentence);
      } else {
        flush();
      }
    }
  }
  flush();
  return findings;
}

function isDivider(trimmed) {
  return /^-{3,}$/.test(trimmed) || /^[*_]{3,}$/.test(trimmed);
}

// markdown 结构行(标题/列表/引用/表格)不是叙述正文,长段落/碎句号/破折号检测都跳过。
function isStructural(trimmed) {
  return /^(#{1,6}\s|>\s?|[-*+]\s|\d+[.)]\s|\|)/.test(trimmed)
    || /^第[零一二三四五六七八九十百千万\d]+章(?:\s|_|$)/.test(trimmed);
}

// 去掉成对引号内的片段(台词/系统播报),只留引号外叙述。碎句号判定用:纯对话/弹幕成片短句
// 是体裁正常形态(豁免),但「叙述 + 引号内物件/短台词」混合行的引号外叙述仍要参与短句计数。
function stripQuoted(text) {
  let out = text;
  for (const src of QUOTE_SOURCES) out = out.replace(new RegExp(src, 'g'), '');
  return out;
}

// 把成对引号片段(含引号)替换为等长问号占位:既豁免引号内台词/播报,又保住原文
// 偏移量,供逐处 blocking 规则定位与截取原文摘录(stripQuoted 会移位,不适合定位)。
// 占位字符用「?」而不是「。」:占位既要截断各规则的 [^。!?!?…] 否定类(?与句号在每条
// 规则的否定类里等效),又不能落在任何规则的接受位。句号占位会替 trailer-summary 的句末
// [。!] 伪造出终止符,让「这一战注定是「血屠」的开端,…」这类引号里放代号/绰号的叙述行
// 被误报,且报出的『这一战注定是。』在原文里 grep 不到。占位长度不变,故偏移与摘录窗口不漂移。
function maskQuoted(text) {
  let out = text;
  for (const src of QUOTE_SOURCES) {
    out = out.replace(new RegExp(src, 'g'), (m) => '?'.repeat(m.length));
  }
  return out;
}

// 返回引号内片段(含引号本身)的 [start, end) 区间,供 not-is 对比句豁免台词用。
function quotedRanges(text) {
  const ranges = [];
  for (const src of QUOTE_SOURCES) {
    const re = new RegExp(src, 'g');
    let match;
    while ((match = re.exec(text)) !== null) ranges.push([match.index, match.index + match[0].length]);
  }
  return ranges;
}

function insideRanges(pos, ranges) {
  return ranges.some(([start, end]) => pos >= start && pos < end);
}

function splitSentences(trimmed) {
  return trimmed
    .split(/[。!?!?]/)
    .map((s) => s.trim())
    .filter(Boolean);
}

function sentenceAround(text, index) {
  let start = index;
  while (start > 0 && !STOP_CHARS.has(text[start - 1])) start -= 1;
  let end = index;
  while (end < text.length && !STOP_CHARS.has(text[end])) end += 1;
  return compact(text.slice(start, end).trim());
}

function visibleLength(sentence) {
  const matched = sentence.match(/[一-鿿A-zA-Za-z0-9]/g);
  return matched ? matched.length : 0;
}

function countTerms(text, terms) {
  let count = 0;
  for (const term of terms) {
    let index = text.indexOf(term);
    while (index !== -1) {
      count += 1;
      index = text.indexOf(term, index + term.length);
    }
  }
  return count;
}

function parseFenceMarker(trimmedLine) {
  const match = /^(?:`{3,}|~{3,})/.exec(trimmedLine);
  if (!match) return null;
  return { char: match[0][0], length: match[0].length };
}

function hasYamlFrontMatter(lines) {
  if (!lines[0] || lines[0].trim() !== '---') return false;
  let sawYamlField = false;
  for (let i = 1; i < Math.min(lines.length, 40); i += 1) {
    const trimmed = lines[i].trim();
    if (trimmed === '---') return sawYamlField;
    if (/^[A-Za-z0-9_-]+:\s*/.test(trimmed)) sawYamlField = true;
  }
  return false;
}

function scanBlock(block) {
  const text = block.map((entry) => entry.text).join('\n');
  const lineStarts = [];
  let cursor = 0;

  for (const entry of block) {
    lineStarts.push({ offset: cursor, lineNo: entry.lineNo });
    cursor += entry.text.length + 1;
  }

  return findNotIsComparisons(text, (offset) => positionForOffset(lineStarts, offset));
}

function positionForOffset(lineStarts, offset) {
  let low = 0;
  let high = lineStarts.length - 1;

  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    const current = lineStarts[mid];
    const next = lineStarts[mid + 1];

    if (offset < current.offset) {
      high = mid - 1;
    } else if (next && offset >= next.offset) {
      low = mid + 1;
    } else {
      return {
        line: current.lineNo,
        column: offset - current.offset + 1,
      };
    }
  }

  return { line: lineStarts[0].lineNo, column: 1 };
}

function findNotIsComparisons(text, getPosition) {
  const findings = [];
  const quoted = quotedRanges(text);
  let offset = 0;

  while (offset < text.length) {
    const start = text.indexOf('不是', offset);
    if (start === -1) break;

    // 引号内是台词/系统播报:口语里「不是A,是B」是自然辩解/反问,不算叙述层 AI 对比句式
    // (与碎句号一致豁免引号内容)。
    if (insideRanges(start, quoted)) {
      offset = start + 2;
      continue;
    }

    // Avoid the common yes/no question fragment “是不是”.
    if (start > 0 && text[start - 1] === '是') {
      offset = start + 2;
      continue;
    }

    const candidate = text.slice(start);
    const markerEnd = findPositiveFlipEnd(candidate);

    if (markerEnd === -1) {
      offset = start + 2;
      continue;
    }

    const raw = trimTrailingNoise(extractFinding(candidate, markerEnd));
    if (raw.length >= 4) {
      const position = getPosition(start);
      findings.push({
        line: position.line,
        column: position.column,
        type: 'not-is-comparison',
        severity: 'blocking',
        message: '高频 AI 对比句式;删掉否定铺垫,直接写后项,或改成动作/细节呈现。',
        excerpt: compact(raw),
      });
    }

    offset = start + Math.max(raw.length, 2);
  }

  return findings;
}

function findPositiveFlipEnd(candidate) {
  let index = 2; // after “不是”
  let scanned = 0;
  let crossedSeparator = false;

  while (index < candidate.length && scanned <= MAX_NEGATIVE_SPAN) {
    const char = candidate[index];

    if (startsWithAt(candidate, index, '而是')) return index + 2;

    if (SOFT_SEPARATORS.has(char)) {
      const next = skipGap(candidate, index + 1);
      if (startsWithAt(candidate, next, '而是')) return next + 2;
      if (candidate[next] === '是' && !TAG_PARTICLES.has(candidate[next + 1]) && !isAffirmationTagAt(candidate, next)) return next + 1;
      crossedSeparator = true;
    }

    if (HARD_SEPARATORS.has(char)) {
      const next = skipGap(candidate, index + 1);
      if (candidate[next] === '是' && !TAG_PARTICLES.has(candidate[next + 1]) && !isAffirmationTagAt(candidate, next)) return next + 1;
      if (char !== '.') break;
      crossedSeparator = true;
    }

    if (STOP_CHARS.has(char)) break;

    // Catch compact forms such as “不是A是B”, but only within the first clause —
    // before any separator. After a separator the trailing “是” of a conjunction
    // (只是/可是/但是/还是/于是/倒是/总是…) is part of that word, not a positive
    // copula (issue #166 false-positive class). Post-separator flips are still
    // caught when separator-adjacent (“,是”/“,而是”) by the separator branches
    // above; subject-present flips like “,他是”/“,那是” are intentionally NOT
    // caught here — there is no separator-local way to tell them from a
    // conjunction without a word list, and on a hard rescan-to-0 gate a false
    // positive (forcing a rewrite of good prose) costs more than missing this
    // rarer form. The “是” in the either-or idiom “不是A就是B / 也是B” is part of
    // the 就是/也是 conjunction, not a copula, so 就/也 are excluded too. Also never
    // treat the “是” inside a second negative fragment (“不是A,也不是B”) as the flip.
    if (char === '是' && !COMPACT_EITHER_OR_PREV.has(candidate[index - 1]) && !crossedSeparator) {
      return index + 1;
    }

    index += 1;
    scanned += 1;
  }

  return -1;
}

function extractFinding(candidate, markerEnd) {
  let end = markerEnd;
  const limit = Math.min(candidate.length, markerEnd + MAX_POSITIVE_SPAN);

  while (end < limit) {
    if (STOP_CHARS.has(candidate[end])) break;
    end += 1;
  }

  return candidate.slice(0, end);
}

function startsWithAt(text, index, needle) {
  return text.slice(index, index + needle.length) === needle;
}

function isAffirmationTagAt(text, index) {
  if (text[index] !== '是') return false;
  const particle = text[index + 1];
  if (!AFFIRMATION_TAG_PARTICLES.has(particle)) return false;
  const boundary = text[index + 2] || '';
  return AFFIRMATION_TAG_BOUNDARY.has(boundary);
}

// 跳过行内空白与换行(含空行/段落间距),停在下一个实义字符。原实现只吞一个换行,
// 会漏掉跨空行的「不是A。(空行)是B」这类分段揭示句。
function skipGap(text, index) {
  while (index < text.length && (isInlineSpace(text[index]) || text[index] === '\n')) index += 1;
  return index;
}

function isInlineSpace(char) {
  return char === ' ' || char === '\t' || char === '\r';
}

function trimTrailingNoise(text) {
  return text.replace(/[\s|))】\]]+$/u, '');
}

function compact(text) {
  const normalized = text.replace(/\s+/g, ' ').trim();
  return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
}
scripts/check-degeneration.js
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');

const USAGE = `Usage: node check-degeneration.js [--check] [--json] [--fail-on=blocking|all] <file...>

Detect model-degeneration fingerprints that a degrading model cannot self-report:
  - verbatim repetition (复读/打转): a long sentence repeated, or back-to-back identical lines
  - mid-sentence truncation (截断): file ends without terminal/closing punctuation
  - placeholder / refusal / meta leakage (元信息泄漏): 作为AI / 我无法继续 / 此处省略 / 乱码
  - engineering-word leakage (工程词泄漏): 细纲 / 情节点 / 本章 / 下一章 / 任务描述 漏进正文

Each finding carries severity: blocking (复读/截断/占位拒绝语/tier1 纯工程词,正文里永不合法,
命中即重写) 或 advisory (tier2 章节/歧义词、对话行里的工程词,只提示、交人/LLM 判)。
--fail-on=blocking 只在出现 blocking finding 时退出 1;默认 --fail-on=all 有任何 finding 即退出 1。

Report-only. The script never rewrites — the safe response is to regenerate the
affected unit (chapter / 摘要) with the finding fed back as a constraint, cap retries,
then surface the evidence to the user. Conservative by design: 通俗网文 deliberately
uses 排比/复沓/弹幕刷屏/重复台词 for rhythm, so short and dialogue repetition is exempt.`;

// 复读:长句(可见字数 ≥ REPEAT_MIN_LEN)出现 ≥ REPEAT_MIN_COUNT 次判为打转;
// 紧邻整行重复(可见字数 ≥ ADJACENT_MIN_LEN)判为即时循环。短句/弹幕/对话刷屏豁免。
const REPEAT_MIN_LEN = 12;
const REPEAT_MIN_COUNT = 3;
const ADJACENT_MIN_LEN = 8;

// hard = 任何行都判(正文里永不合法);soft = 只在「非对话」叙述行判(角色台词里可能合法,
// 如「对不起,我无法答应你」是正常对话,不是模型拒绝语)。
const PLACEHOLDER_PATTERNS = [
  // 「作为AI」需在自指位置(其后是断句/我/无法… 或句末),避免误报「人工智能时代的产物」这类
  // 复合名词;并对对话行豁免(系统流/AI 伴侣题材里 AI 角色台词「作为AI,我会保护你」是合法对话)。
  // 型号后缀(AI语言模型/AI助手/人工智能语言模型/AI模型/AI大模型)必须可选吃掉:否则前视断言紧跟
  // 在「AI」后面看到的是「语」/「助」/「模」,最典型的退化开场整类漏检(与写后网 story_hook_core.js
  // SOFT_PATTERNS / story_codex_hook.py _NET_SOFT_PATTERNS 同语义)。
  { re: /作为(一个)?(AI|人工智能|大?语言模型|智能助手|聊天助手)(?:语言模型|大?模型|助手|机器人)?(?=[,,。、;;::!!??\s))」』"】]|我|无法|不能|没法|$)/, label: '元信息泄漏(AI 自指)', hard: false },
  { re: /�/, label: '乱码(替换字符 �)', hard: true },
  { re: /^(Sure|Certainly|Here'?s|As an AI|I (?:cannot|can't|am unable|apologize))/, label: '元信息泄漏(英文 AI 腔)', hard: true },
  { re: /[((](此处|以下|这里|下文|后续)?\s*(省略|略)(去|过)?[^))]{0,10}[))]/, label: '占位符(括号省略)', hard: true },
  { re: /(未完待续|TODO|占位符|placeholder)/, label: '占位符', hard: true },
  { re: /我(无法|不能)(继续(写|创作|生成|下去)|生成(内容|文本|正文)?|创作|续写|完成(这个|本)?(章|篇|创作|请求))/, label: '元信息泄漏(生成拒绝语)', hard: false },
];

// 工程词泄漏(正文元信息扫描的确定性版):弱模型把写作工程词漏进正文,破坏代入感
// (DeepSeek-v4 这类会在对话里冒「该到下一章了」)。漏词的模型自己发现不了,靠脚本兜。
// tier1 = 纯写作流水线术语,正文里几乎永不合法;tier2 = 章节结构/歧义词,角色在故事内
// 真实阅读/讨论「第X章」或故事内系统/界面用语时属例外(report-only,交人/LLM 判)。
const META_TIER1_RE = /细纲|情节点|卷纲|功能标签|目标情绪|字数目标|章首钩子|章尾钩子/;
const META_TIER2_RE = /第[一二三四五六七八九十百千万两0-9]+章|本章|这一章|上一章|下一章|上章|下章|前一章|后一章|前文|后文|伏笔|读者|任务描述/;

const options = { json: false, files: [], failOn: 'all' };

for (let i = 2; i < process.argv.length; i += 1) {
  const arg = process.argv[i];
  if (arg === '--check') {
    // Accepted for symmetry with the other detectors; detection is always check-only.
  } else if (arg === '--json') {
    options.json = true;
  } else if (arg.startsWith('--fail-on=')) {
    const v = arg.slice('--fail-on='.length);
    if (v !== 'blocking' && v !== 'all') die(`--fail-on must be 'blocking' or 'all'`);
    options.failOn = v;
  } else if (arg === '-h' || arg === '--help') {
    process.stdout.write(`${USAGE}\n`);
    process.exit(0);
  } else if (arg.startsWith('-')) {
    die(`Unknown option: ${arg}`);
  } else {
    options.files.push(arg);
  }
}

if (options.files.length === 0) {
  die('No files provided');
}

let failed = false;
const allFindings = [];

for (const file of options.files) {
  const fullPath = path.resolve(file);
  let input;
  try {
    input = fs.readFileSync(fullPath, 'utf8');
  } catch (error) {
    failed = true;
    if (!options.json) console.error(`${file}: unable to read (${error.message})`);
    continue;
  }
  const findings = scanDocument(input).map((finding) => ({ file, ...finding }));
  allFindings.push(...findings);
}

if (options.json) {
  process.stdout.write(`${JSON.stringify({ findings: allFindings }, null, 2)}\n`);
} else {
  for (const f of allFindings) {
    console.log(`${f.file}:${f.line}:${f.column}: [${f.severity}] ${f.type}: ${f.message} (${f.excerpt})`);
  }
}

if (failed) process.exit(2);
// --fail-on=blocking 只在出现 blocking finding 时退出 1(advisory 仅报告);默认 all 沿用「有任何 finding 即 1」。
const hasBlocking = allFindings.some((f) => f.severity === 'blocking');
if (options.failOn === 'blocking' ? hasBlocking : allFindings.length > 0) process.exit(1);

function die(message) {
  console.error(message);
  console.error(USAGE.trimEnd());
  process.exit(2);
}

function scanDocument(input) {
  const lines = input.split(/\r?\n/);
  const content = []; // { text, trimmed, lineNo } for body lines outside front-matter/fences
  let fence = null;
  let inFrontMatter = hasYamlFrontMatter(lines);

  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index];
    const trimmed = line.trim();
    if (inFrontMatter) {
      if (index > 0 && trimmed === '---') inFrontMatter = false;
      continue;
    }
    const fenceMarker = /^(?:`{3,}|~{3,})/.exec(trimmed);
    if (fence) {
      if (fenceMarker && trimmed[0] === fence) fence = null;
      continue;
    }
    if (fenceMarker) {
      fence = trimmed[0];
      continue;
    }
    content.push({ text: line, trimmed, lineNo: index + 1 });
  }

  const findings = [];
  findings.push(...findRepetition(content));
  findings.push(...findTruncation(content));
  findings.push(...findPlaceholders(content));
  findings.push(...findMetaLeak(content));
  findings.sort((a, b) => a.line - b.line || a.column - b.column);
  return findings;
}

function isContent(trimmed) {
  return trimmed && !trimmed.startsWith('#') && !/^-{3,}$/.test(trimmed);
}

function isDialogueLike(trimmed) {
  return /[“”"'‘’「」『』【】]/.test(trimmed);
}

// 去掉成对引号内的片段(台词/系统词/引用物件),只留引号外叙述。复读判定用:重复台词是体裁
// 手法(豁免),但「叙述 + 引号内物件/短台词」混合行里引号外叙述的复读仍是退化,不能整行豁免。
function stripQuoted(text) {
  return text
    .replace(/「[^」]*」/g, '')
    .replace(/『[^』]*』/g, '')
    .replace(/【[^】]*】/g, '')
    .replace(/“[^”]*”/g, '')
    .replace(/‘[^’]*’/g, '')
    .replace(/"[^"]*"/g, '')
    .replace(/'[^']*'/g, '');
}

function visibleLength(text) {
  const m = text.match(/[一-鿿A-zA-Za-z0-9]/g);
  return m ? m.length : 0;
}

function findRepetition(content) {
  const findings = [];
  const body = content.filter((c) => isContent(c.trimmed));

  // (1) back-to-back identical lines (immediate loop). 纯台词/弹幕复沓(引号外叙述很短)豁免;
  // 「叙述 + 引号内物件」混合行的整行复读仍判(去引号后叙述够长)。
  for (let i = 1; i < body.length; i += 1) {
    if (
      body[i].trimmed === body[i - 1].trimmed &&
      visibleLength(stripQuoted(body[i].trimmed)) >= ADJACENT_MIN_LEN
    ) {
      findings.push({
        line: body[i].lineNo,
        column: 1,
        type: 'verbatim-repeat',
        severity: 'blocking',
        message: '逐行复读(紧邻整行重复):疑似模型打转,重写本段、删掉重复。',
        excerpt: compact(body[i].trimmed),
      });
    }
  }

  // (2) any long sentence repeated >= REPEAT_MIN_COUNT times across the file.
  // 只豁免引号内台词(体裁手法),引号外叙述句仍参与复读计数(含「叙述+引号内物件」混合行)。
  const counts = new Map();
  for (const { trimmed } of body) {
    for (const sentence of stripQuoted(trimmed).split(/[。!?!?]/)) {
      const s = sentence.trim();
      if (visibleLength(s) < REPEAT_MIN_LEN) continue;
      const entry = counts.get(s) || { count: 0, firstLine: null };
      entry.count += 1;
      counts.set(s, entry);
    }
  }
  // record first line for repeated sentences
  const flagged = new Set();
  for (const [s, entry] of counts) {
    if (entry.count >= REPEAT_MIN_COUNT) flagged.add(s);
  }
  if (flagged.size) {
    for (const { trimmed, lineNo } of body) {
      for (const sentence of stripQuoted(trimmed).split(/[。!?!?]/)) {
        const s = sentence.trim();
        if (flagged.has(s)) {
          findings.push({
            line: lineNo,
            column: 1,
            type: 'verbatim-repeat',
            severity: 'blocking',
            message: `长句复读(同句出现 ${counts.get(s).count} 次):疑似模型打转,重写、保留一处。`,
            excerpt: compact(s),
          });
          flagged.delete(s); // report each repeated sentence once, at its first occurrence
        }
      }
    }
  }

  return findings;
}

function findTruncation(content) {
  const body = content.filter((c) => isContent(c.trimmed));
  if (body.length === 0) return [];
  const last = body[body.length - 1];
  // a finished chapter ends on terminal/closing punctuation; otherwise it was cut off.
  if (/[。!?!?…”"』」))】]$/.test(last.trimmed)) return [];
  return [{
    line: last.lineNo,
    column: last.trimmed.length,
    type: 'truncated',
    severity: 'blocking',
    message: '疑似截断:正文末尾未以句末/收尾标点结束,可能被模型中途切断;补完结尾或重写收尾。',
    excerpt: compact(last.trimmed.slice(-24)),
  }];
}

function findPlaceholders(content) {
  const findings = [];
  for (const { trimmed, lineNo } of content) {
    if (!isContent(trimmed)) continue;
    const dialogue = isDialogueLike(trimmed);
    for (const { re, label, hard } of PLACEHOLDER_PATTERNS) {
      if (!hard && dialogue) continue; // soft 拒绝语在对话行里可能是正常台词,豁免
      const m = re.exec(trimmed);
      if (m) {
        findings.push({
          line: lineNo,
          column: (m.index || 0) + 1,
          type: 'placeholder-leak',
          severity: 'blocking',
          message: `${label}:正文混入元信息/拒绝语/占位符,重写本段干净落地。`,
          excerpt: compact(trimmed.slice(Math.max(0, (m.index || 0) - 4), (m.index || 0) + 20)),
        });
        break; // one finding per line is enough
      }
    }
  }
  return findings;
}

function findMetaLeak(content) {
  const findings = [];
  let firstContentSeen = false;
  for (const { trimmed, lineNo } of content) {
    if (!isContent(trimmed)) continue;
    if (!firstContentSeen) {
      firstContentSeen = true;
      // 标题行(第N章 章名,无 ## 前缀时也算)属「标题行以外的正文」之外,排除
      if (/^第[一二三四五六七八九十百千万两0-9]+章/.test(trimmed)) continue;
    }
    const dialogue = isDialogueLike(trimmed);
    let m = META_TIER1_RE.exec(trimmed);
    if (m) {
      // tier1 纯工程词正文里几乎永不合法→blocking;但写手/编剧题材里角色在故事内真讨论创作,
      // 台词(对话行)里可能合法,降级为 advisory(仍报告,交人/LLM 判,不强制回炉)。
      findings.push({
        line: lineNo,
        column: m.index + 1,
        type: 'meta-leak',
        severity: dialogue ? 'advisory' : 'blocking',
        message: `工程词泄漏:「${m[0]}」是写作流水线术语,正文里不该出现;改成角色/场景内表达。${dialogue ? '例外:角色为作者/编剧、在故事内真实讨论创作时,台词里可能合法。' : ''}`,
        excerpt: compact(trimmed.slice(Math.max(0, m.index - 6), m.index + 18)),
      });
      continue; // tier1 命中即可,不再叠 tier2
    }
    m = META_TIER2_RE.exec(trimmed);
    if (m) {
      findings.push({
        line: lineNo,
        column: m.index + 1,
        type: 'meta-leak',
        severity: 'advisory',
        message: `元信息泄漏:「${m[0]}」疑似工程/章节结构词混入正文;改成角色当下可感知的事件锚点或相对时间。例外:角色在故事内真实阅读/讨论「第X章」、真身为作者/读者、或故事内系统/界面用语。`,
        excerpt: compact(trimmed.slice(Math.max(0, m.index - 6), m.index + 18)),
      });
    }
  }
  return findings;
}

function hasYamlFrontMatter(lines) {
  if (!lines[0] || lines[0].trim() !== '---') return false;
  let sawYamlField = false;
  for (let i = 1; i < Math.min(lines.length, 40); i += 1) {
    const trimmed = lines[i].trim();
    if (trimmed === '---') return sawYamlField;
    if (/^[A-Za-z0-9_-]+:\s*/.test(trimmed)) sawYamlField = true;
  }
  return false;
}

function compact(text) {
  const normalized = text.replace(/\s+/g, ' ').trim();
  return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
}
scripts/check-delivery-contract.js
#!/usr/bin/env node
/**
 * Deterministic delivery verifier for story-short-write.
 *
 * Usage:
 *   node scripts/check-delivery-contract.js --json \
 *     --min-chars N --max-chars N --sections N [project-dir]
 * Exit: 0 = pass, 1 = blocking delivery failures, 2 = invalid invocation.
 *
 * The verifier only checks user-visible size and shape. It does not score prose,
 * causality, emotion, or any other semantic quality dimension.
 */

'use strict'

const fs = require('fs')
const path = require('path')

const MARKERS = [
  { style: 'numeric-heading', pattern: /^###\s*\d+\.$/ },
  { style: 'chapter-heading', pattern: /^###\s*第[一二三四五六七八九十百千万两〇零0-9]+章$/ },
  { style: 'zhihu-numeric', pattern: /^\d+\.$/ },
]

function readBody(file) {
  try {
    const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')
    return { ok: text.trim().length > 0, text }
  } catch (error) {
    return { ok: false, text: '', error: error.message }
  }
}

function visibleChars(text) {
  return Array.from(text).filter((character) => !/\s/u.test(character)).length
}

function markerFor(line) {
  const trimmed = line.trim()
  return MARKERS.find((entry) => entry.pattern.test(trimmed)) || null
}

function makeCheck(id, ok, evidence, expected, repair) {
  return {
    id,
    ok,
    severity: 'blocking',
    file: '正文.md',
    evidence,
    expected,
    references: ['references/short-format.md'],
    repair,
  }
}

function verify(projectDir, contract) {
  const project = path.resolve(projectDir)
  const bodyFile = path.join(project, '正文.md')
  const bodyRead = readBody(bodyFile)
  const checks = []

  checks.push(makeCheck(
    'delivery.body-readable',
    bodyRead.ok,
    bodyRead.ok ? '正文.md 存在且非空' : (bodyRead.error || '正文.md 为空'),
    '项目目录下存在非空的 正文.md',
    '只补建或恢复 正文.md;不要改动设定与大纲。'
  ))
  if (!bodyRead.ok) return report(project, contract, checks)

  const actual = visibleChars(bodyRead.text)
  checks.push(makeCheck(
    'delivery.visible-chars',
    actual >= contract.minChars && actual <= contract.maxChars,
    `非空白 Unicode 字符:${actual}`,
    `用户要求 ${contract.minChars}-${contract.maxChars} 个非空白字符`,
    actual < contract.minChars
      ? '只扩写既有情节点的行动、后果或对话,不新增支线。'
      : '只压缩重复解释、复述和无功能过场,不删关键证据或结算。'
  ))

  const lines = bodyRead.text.replace(/\r\n?/g, '\n').replace(/\n+$/, '\n').split('\n')
  const markers = lines
    .map((line, index) => ({ line: index + 1, marker: markerFor(line), text: line.trim() }))
    .filter((entry) => entry.marker)
  checks.push(makeCheck(
    'delivery.section-count',
    markers.length === contract.sections,
    `识别到 ${markers.length} 个小节标记:${markers.map((entry) => entry.text).join('、') || '无'}`,
    `正文恰好 ${contract.sections} 节`,
    '只校正缺失、多余或误写的小节标记;不要为凑节数新开支线。'
  ))

  const styles = new Set(markers.map((entry) => entry.marker.style))
  checks.push(makeCheck(
    'delivery.section-style',
    markers.length > 0 && styles.size === 1,
    markers.length ? `小节标记样式:${Array.from(styles).join('、')}` : '没有可识别的小节标记',
    '全文统一使用 short-format.md 允许的一种小节标记',
    '只统一小节标记的样式,不改正文内容。'
  ))

  const numericSequence = markers.map((entry) => {
    const match = entry.text.match(/\d+/)
    return match ? Number(match[0]) : null
  })
  const sequenceOk = markers.length > 0 && (
    numericSequence.every((number) => number !== null)
      ? numericSequence.every((number, index) => number === index + 1)
      : new Set(markers.map((entry) => entry.text)).size === markers.length
  )
  checks.push(makeCheck(
    'delivery.section-sequence',
    sequenceOk,
    `小节序列:${markers.map((entry) => entry.text).join('、') || '无'}`,
    '数字小节从 1 连续递增;中文章名不得重复',
    '只修正小节标记编号,不改正文内容。'
  ))

  const markerLines = new Set(markers.map((entry) => entry.line - 1))
  const invalidBlankLines = []
  for (let index = 0; index < lines.length; index++) {
    if (lines[index].trim() !== '') continue
    const isEdge = index === 0 || index === lines.length - 1
    const touchesMarker = markerLines.has(index - 1) || markerLines.has(index + 1)
    if (!isEdge && !touchesMarker) invalidBlankLines.push(index + 1)
  }
  checks.push(makeCheck(
    'delivery.blank-lines',
    invalidBlankLines.length === 0,
    invalidBlankLines.length ? `正文段落间空行:${invalidBlankLines.join('、')}` : '正文段落间无空行',
    '只允许文件边缘或小节标记相邻处出现空行,正文段落之间紧密单换行',
    '只删除报告行的多余空行。'
  ))

  return report(project, contract, checks)
}

function report(project, contract, checks) {
  const failures = checks.filter((check) => !check.ok)
  return {
    schema_version: 1,
    verifier: 'story-short-write.delivery-contract',
    project,
    contract: {
      metric: 'non_whitespace_unicode_chars_v1',
      min_chars: contract.minChars,
      max_chars: contract.maxChars,
      sections: contract.sections,
    },
    ok: failures.length === 0,
    checks,
    failures,
    repair_scope: failures.map((failure) => ({
      id: failure.id,
      file: failure.file,
      evidence: failure.evidence,
      expected: failure.expected,
      references: failure.references,
      repair: failure.repair,
    })),
  }
}

function parsePositiveInteger(value) {
  if (!/^[1-9]\d*$/.test(value || '')) return null
  const number = Number(value)
  return Number.isSafeInteger(number) ? number : null
}

function parseArgs(argv) {
  const values = {}
  const positional = []
  for (let index = 0; index < argv.length; index++) {
    const arg = argv[index]
    if (arg === '--json') continue
    if (['--min-chars', '--max-chars', '--sections'].includes(arg)) {
      if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) return null
      values[arg] = parsePositiveInteger(argv[++index])
      if (values[arg] === null) return null
      continue
    }
    if (arg.startsWith('--')) return null
    positional.push(arg)
  }
  if (positional.length > 1) return null
  if (!values['--min-chars'] || !values['--max-chars'] || !values['--sections']) return null
  if (values['--min-chars'] > values['--max-chars']) return null
  return {
    project: positional[0] || '.',
    contract: {
      minChars: values['--min-chars'],
      maxChars: values['--max-chars'],
      sections: values['--sections'],
    },
  }
}

function main(argv) {
  const parsed = parseArgs(argv)
  if (!parsed) {
    process.stderr.write('用法: node scripts/check-delivery-contract.js --json --min-chars N --max-chars N --sections N [project-dir]\n')
    return 2
  }
  const result = verify(parsed.project, parsed.contract)
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
  return result.ok ? 0 : 1
}

if (require.main === module) process.exitCode = main(process.argv.slice(2))

module.exports = { markerFor, visibleChars, verify }
scripts/check-outline-copy.js
#!/usr/bin/env node
/**
 * check-outline-copy.js — 细纲照搬检测
 *
 * 治的病:细纲把情节点写成成品散文句,正文只剩誊抄,质量被锁死在细纲水平。
 * 实测正文与细纲连续重合最高 13.5%、单段最长 40 字,且重合段落多为叙述而非台词
 * ——即全章最好的那几句在细纲阶段就写完了。
 *
 * 判定:正文与同章细纲连续重合 > 阈值(默认 15 字)即报出
 * ——细纲只锁功能与结果,句子一律在正文现场写。
 *
 * 词面相同不等于不良照搬:系统面板、任务要求、固定专名本就该保持一致。因此本脚本
 * 只提供证据(位置与片段),是否重写由 agent 读上下文语义判断,同 check-ai-patterns.js
 * 的 advisory 轨——但每条都要有结论,不允许只报不改,也不为归零机械改写。
 * 判定保留的只由主会话补进细纲锚句:子代理不改大纲细纲(见 narrative-writer「不自行改纲」),
 * 否则子代理误判的重合会被自己写进白名单,主会话复扫时不再报出,第二层复核失明。
 *
 * 免报:细纲「复沓锚句」字段下列出的原话允许逐字落地——誓言、系统面板、
 * 旧案原话等写细纲时判定必须原文出现的部分,逐行一条。只扣除锚句自身的精确区间,
 * 前后剩余片段照常按阈值判定,避免紧挨锚句的照搬被顺带赦免。
 * 豁免量单独统计并在报告末尾列出,滥用锚句绕过检测时一眼可见。
 *
 * 由 narrative-writer 落盘后自查、主会话收尾复扫时调用(两侧同一份实现,口径一致)。
 * 不进 hook:正文兜底 hook 的共享核是四端共用的,不为单项检测扩面。
 *
 * 用法:
 *   node check-outline-copy.js <正文路径...>                    # 自动找同章细纲;短篇找同目录小节大纲
 *   node check-outline-copy.js --outline <细纲路径> <正文路径...> # 指定细纲
 *
 * 位置参数一律按正文处理,与 check-ai-patterns.js 的 `<file...>` 口径一致:
 * 收尾复扫用 `正文/第XXX章_*.md` 这类通配传多章时,多出来的正文不能被当成细纲吞掉
 * ——那会让首个文件比错对象、其余文件根本不检,静默退 0 报「干净」。
 *
 * 退出码:0 = 干净或无法判定(缺细纲/非分章正文);1 = 有重合待复核。
 * 无发现时完全静默,不污染上下文。
 */

'use strict'
const fs = require('fs')
const path = require('path')

const MIN_RUN = 16 // 判定阈值:连续重合 >15 字,即 >=16
const REPORT_TOP = 8 // 最多列出的片段数

function read(p) {
  try {
    return fs.readFileSync(p, 'utf8').replace(/^/, '')
  } catch {
    return null
  }
}

/** 只留汉字——剥掉标点/加粗/【】后比对,防止细纲标注造成假阴性 */
function hanOnly(s) {
  return s.replace(/[^一-鿿]/g, '')
}

/**
 * 抽出细纲「复沓锚句」字段下的原话,一行一条。
 * 只认这一个字段,不扫情节点序列——锚句集中在固定区块,情节点保持只写「要发生什么」。
 * 区块终止于行首无缩进的下一个字段(`- xxx`)或下一个小节标题,因此条目本身
 * 用 `1.` 编号、缩进 `-` 列表或纯文本都能正确提取,不必额外标记。
 */
function extractAnchors(outline) {
  const m = outline.match(/复沓锚句[^::\n]*[::]([\s\S]*?)(?=\n[-*+]\s|\n#{1,6}\s|$)/)
  if (!m) return []
  return m[1]
    .split("\n")
    // 去掉列表符号与「点N:」这类落点前缀,只留原话本身
    .map((line) => line.replace(/^\s*[-*+]?\s*(?:\d+[.、)]\s*)?(?:点\s*\d+\s*[::])?/, ""))
    .map((line) => hanOnly(line))
    .filter((a) => a.length >= 2)
}

/**
 * 在片段里定位锚句的精确出现区间,挖掉后返回剩余子段与豁免字数。
 * 不能只判「片段包含锚句」就整段放行——贪心扫描求的是最长延伸,锚句嵌在中间时
 * 会连同紧挨着它的未授权重合一起赦免。
 */
function splitByAnchors(frag, anchors) {
  // 片段完全落在某条锚句内:整段豁免(抄了锚句的一部分,仍在授权范围)
  if (anchors.some((a) => a.includes(frag))) return { rest: [], anchoredLen: frag.length }

  const spans = []
  for (const a of anchors) {
    for (let from = 0; from + a.length <= frag.length; ) {
      const at = frag.indexOf(a, from)
      if (at < 0) break
      spans.push([at, at + a.length])
      from = at + 1 // 同一锚句重复出现要逐次记录
    }
  }
  if (!spans.length) return { rest: [frag], anchoredLen: 0 }

  spans.sort((x, y) => x[0] - y[0])
  const merged = [spans[0]] // 多锚句重叠/相邻时并成一段,避免重复计数
  for (const [s, e] of spans.slice(1)) {
    const last = merged[merged.length - 1]
    if (s <= last[1]) last[1] = Math.max(last[1], e)
    else merged.push([s, e])
  }

  const rest = []
  let cursor = 0
  let anchoredLen = 0
  for (const [s, e] of merged) {
    if (s > cursor) rest.push(frag.slice(cursor, s))
    anchoredLen += e - s
    cursor = e
  }
  if (cursor < frag.length) rest.push(frag.slice(cursor))
  return { rest, anchoredLen }
}

/** 定位同章细纲:遍历 大纲/ 按章号正则匹配,支持带后缀的文件名 */
function findOutline(proseFile) {
  const base = path.basename(proseFile)
  // 短篇没有章号:正文.md 与 小节大纲.md 在同目录平铺
  if (base === '正文.md') {
    const sibling = path.join(path.dirname(proseFile), '小节大纲.md')
    return fs.existsSync(sibling) ? sibling : null
  }
  const m = base.match(/^第\s*0*(\d+)\s*章/)
  if (!m) return null
  const chapter = m[1]
  const dir = path.join(path.dirname(path.dirname(proseFile)), '大纲')
  try {
    for (const file of fs.readdirSync(dir)) {
      const fm = file.match(/^细纲_第0*(\d+)章.*\.md$/)
      if (fm && fm[1] === chapter) return path.join(dir, file)
    }
  } catch {}
  return null
}

function main() {
  const proseFiles = []
  let explicitOutline = null
  const argv = process.argv.slice(2)
  for (let k = 0; k < argv.length; k++) {
    if (argv[k] === '--outline') explicitOutline = argv[++k] || null
    else proseFiles.push(argv[k])
  }
  if (!proseFiles.length) {
    process.stderr.write('用法: node check-outline-copy.js [--outline <细纲路径>] <正文路径...>\n')
    return 0
  }
  // 逐个文件独立判定;任一文件有重合即整体退 1
  let status = 0
  for (const proseFile of proseFiles) {
    if (checkOne(proseFile, explicitOutline)) status = 1
  }
  return status
}

function checkOne(proseFile, explicitOutline) {
  const prose = read(proseFile)
  if (prose === null) return 0

  const outlineFile = explicitOutline || findOutline(proseFile)
  if (!outlineFile) return 0
  const outline = read(outlineFile)
  if (outline === null) return 0

  // 正文去掉标题行后比对
  const P = hanOnly(prose.replace(/^#.*$/gm, ''))
  const O = hanOnly(outline)
  if (P.length < MIN_RUN || O.length < MIN_RUN) return 0

  // 复沓锚句列出的原话允许逐字落地,命中后计入豁免、不判誊抄
  const anchors = extractAnchors(outline)

  // 贪心扫描:每个起点二分求「仍是细纲子串」的最长延伸,命中区间不重叠
  const hits = []
  let copied = 0
  let anchored = 0
  let anchoredCount = 0
  let i = 0
  while (i < P.length) {
    let best = 0
    if (i + MIN_RUN <= P.length && O.includes(P.substr(i, MIN_RUN))) {
      best = MIN_RUN
      let lo = MIN_RUN
      let hi = Math.min(P.length - i, 200)
      while (lo <= hi) {
        const mid = (lo + hi) >> 1
        if (O.includes(P.substr(i, mid))) {
          best = mid
          lo = mid + 1
        } else hi = mid - 1
      }
    }
    if (best) {
      const frag = P.substr(i, best)
      const { rest, anchoredLen } = splitByAnchors(frag, anchors)
      if (anchoredLen) {
        anchored += anchoredLen
        anchoredCount++
      }
      // 挖掉锚句后的剩余子段仍是细纲子串,只需按阈值重判
      for (const seg of rest) {
        if (seg.length >= MIN_RUN) {
          hits.push({ frag: seg, len: seg.length })
          copied += seg.length
        }
      }
      i += best
    } else i++
  }
  if (!hits.length) {
    // 全部命中都是锚句豁免:静默放行,但把豁免量报出来供人工复核滥用
    if (anchoredCount) {
      process.stdout.write(
        `细纲照搬检测(${path.basename(proseFile)}):无未授权誊抄;` +
          `另有 ${anchoredCount} 处 ${anchored} 字为复沓锚句的逐字落地。\n`
      )
    }
    return 0
  }

  const rate = ((copied * 100) / P.length).toFixed(1)
  const out = [
    `=== 细纲照搬检测(${path.basename(proseFile)})===`,
    `正文 ${P.length} 字,与 ${path.basename(outlineFile)} 连续重合 >${MIN_RUN - 1} 字的片段 ${hits.length} 处,共 ${copied} 字(${rate}%)。`,
    `逐条对照原文判断:确属把细纲叙述搬进正文就重写——细纲只锁功能与结果,句子在正文现场写;系统面板、誓词、案卷原话、固定专名等功能性重合可保留。每条都要有结论,不为归零机械改写。保留项由主会话补进细纲「复沓锚句」——子代理只重写正文或标 \`[需复核]\` 交回,不改大纲细纲。`,
  ]
  hits
    .sort((a, b) => b.len - a.len)
    .slice(0, REPORT_TOP)
    .forEach((h) => out.push(`  · ${h.len} 字「${h.frag}」`))
  if (hits.length > REPORT_TOP) out.push(`  · …另有 ${hits.length - REPORT_TOP} 处`)
  if (anchoredCount) out.push(`(另有 ${anchoredCount} 处 ${anchored} 字为复沓锚句的逐字落地,不计入誊抄)`)
  process.stdout.write(out.join('\n') + '\n')
  return 1
}

try {
  process.exit(main())
} catch {
  process.exit(0)
}
scripts/check-phase2-contract.js
#!/usr/bin/env node
/**
 * Phase 2 deterministic contract verifier for story-short-write.
 *
 * This script is intentionally mechanical: it checks whether the two Phase 2
 * artifacts expose the fields and table shape required by the skill. It does
 * not pretend to judge story quality or whether a reference was genuinely
 * understood.
 *
 * Usage: node scripts/check-phase2-contract.js --json [project-dir]
 * Exit: 0 = pass, 1 = blocking contract failures, 2 = invalid invocation.
 */

'use strict'

const fs = require('fs')
const path = require('path')

const OUTLINE_HEADERS = [
  '结构段/五段功能',
  '主事件',
  '子事件×3-5',
  '情绪',
  '人物/关系变化',
  '因果/逻辑链',
  '读者新获知什么',
  '结尾承接/钩子',
  '伏笔/物件',
  '动静',
  '对话密度',
  '目标字数',
]

const FUNCTION_TAG = /\{(?:对话|冲突|伏笔|回忆|发现|递进)\}/

function cleanCell(value) {
  return value.replace(/\*\*/g, '').replace(/`/g, '').trim()
}

function parseTableLine(line) {
  const trimmed = line.trim()
  if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) return null
  return trimmed.slice(1, -1).split('|').map(cleanCell)
}

function isSeparatorRow(cells) {
  return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell))
}

function readUtf8(file) {
  try {
    const text = fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')
    return { ok: text.trim().length > 0, text }
  } catch (error) {
    return { ok: false, text: '', error: error.message }
  }
}

function makeCheck(id, ok, file, evidence, expected, references, repair) {
  return { id, ok, severity: 'blocking', file, evidence, expected, references, repair }
}

function verify(projectDir) {
  const project = path.resolve(projectDir)
  const skillRoot = path.resolve(__dirname, '..')
  const settingsFile = path.join(project, '设定.md')
  const outlineFile = path.join(project, '小节大纲.md')
  const settingsRead = readUtf8(settingsFile)
  const outlineRead = readUtf8(outlineFile)
  const checks = []

  checks.push(makeCheck(
    'phase2.settings-readable',
    settingsRead.ok,
    '设定.md',
    settingsRead.ok ? '文件存在且非空' : (settingsRead.error || '文件为空'),
    '项目目录下存在非空的 设定.md',
    ['references/writing-workflow.md'],
    '只补建或补全 设定.md,不改动已通过的 小节大纲.md。'
  ))
  checks.push(makeCheck(
    'phase2.outline-readable',
    outlineRead.ok,
    '小节大纲.md',
    outlineRead.ok ? '文件存在且非空' : (outlineRead.error || '文件为空'),
    '项目目录下存在非空的 小节大纲.md',
    ['references/writing-workflow.md'],
    '只补建或补全 小节大纲.md,不改动已通过的 设定.md。'
  ))

  if (!settingsRead.ok || !outlineRead.ok) return report(project, checks)

  const settings = settingsRead.text
  const outline = outlineRead.text

  const platformMatch = settings.match(/目标平台\s*[::]\s*(知乎(?:盐选)?|小程序|番茄(?:短篇)?)/)
  checks.push(makeCheck(
    'phase2.platform-declared',
    Boolean(platformMatch),
    '设定.md',
    platformMatch ? `目标平台:${platformMatch[1]}` : '未找到可识别的目标平台字段',
    '写明“目标平台:知乎盐选 / 小程序 / 番茄短篇”之一',
    ['references/submission-craft.md'],
    '只在 设定.md 的基本信息或 Phase 2 设计校验区补目标平台。'
  ))

  const genreMatch = settings.match(/(?:题材参考|题材包)\s*[::]\s*`?(references\/(?:genre-styles\/[^`\s|]+\.md|genre-writing-formulas\.md))`?/)
  let genreEvidence = '未找到题材参考路径'
  let genreOk = false
  if (genreMatch) {
    const resolved = path.resolve(skillRoot, genreMatch[1])
    const insideSkill = resolved.startsWith(`${skillRoot}${path.sep}`)
    genreOk = insideSkill && fs.existsSync(resolved) && fs.statSync(resolved).isFile()
    genreEvidence = genreOk ? `题材参考:${genreMatch[1]}` : `题材参考不可读:${genreMatch[1]}`
  }
  checks.push(makeCheck(
    'phase2.genre-reference-declared',
    genreOk,
    '设定.md',
    genreEvidence,
    '写明一个真实存在的 references/genre-styles/{题材}.md;冷门题材写 references/genre-writing-formulas.md',
    ['references/writing-workflow.md', 'references/short-craft.md'],
    '只补题材参考路径与从该参考选出的 2-3 个核心招式,不重写故事框架。'
  ))

  const movesMatch = settings.match(/核心招式\s*[::]\s*([^\n]+)/)
  const moves = movesMatch
    ? movesMatch[1].split(/[;;]|\s\/\s/).map((item) => item.trim()).filter(Boolean)
    : []
  checks.push(makeCheck(
    'phase2.genre-moves-declared',
    moves.length >= 2 && moves.length <= 3,
    '设定.md',
    moves.length ? `识别到 ${moves.length} 个核心招式` : '未找到核心招式字段',
    '“核心招式:招式一;招式二[;招式三]”,共 2-3 个',
    ['references/writing-workflow.md'],
    '只补或收敛核心招式字段到 2-3 个,并保持已选题材参考不变。'
  ))

  const villainSkipped = /反派(?:设计)?\s*[::]\s*(?:无|不适用)\s*[((][^))]+[))]/.test(settings)
  const villainFields = ['身份', '动机', '作恶方式', '致命弱点', '报应']
  const missingVillainFields = villainFields.filter((field) => !new RegExp(`(?:^|\\n)\\s*[-*]?\\s*${field}\\s*[::]`, 'm').test(settings))
  const villainOk = villainSkipped || missingVillainFields.length === 0
  checks.push(makeCheck(
    'phase2.villain-contract',
    villainOk,
    '设定.md',
    villainSkipped ? '已明确反派不适用' : (villainOk ? '反派五字段齐全' : `缺字段:${missingVillainFields.join('、')}`),
    '无反派时写“反派设计:不适用(原因)”;有反派时填写身份、动机、作恶方式、致命弱点、报应',
    ['references/villain-and-reveal.md'],
    '只补反派缺失字段;若确无反派,改为带原因的不适用声明。'
  ))

  const placeholderFields = ['题材参考', '核心招式', '反派设计', '反转类型', '反转位置', '付费点', '目标平台', '目标字数']
  const placeholderLines = settings.split(/\r?\n/)
    .map((line, index) => ({ line: index + 1, text: line }))
    .filter((entry) => placeholderFields.some((field) => entry.text.includes(`${field}:`) || entry.text.includes(`${field}:`)))
    .filter((entry) => /\{[^}]*\}/.test(entry.text))
  checks.push(makeCheck(
    'phase2.no-template-placeholders',
    placeholderLines.length === 0,
    '设定.md',
    placeholderLines.length ? `仍是模板占位符的行:${placeholderLines.map((entry) => entry.line).join('、')}` : '契约字段没有未填写的 {} 占位符',
    'Phase 2 契约字段写实际设计内容,不保留 {} 模板占位符',
    ['references/writing-workflow.md'],
    '只把报告行的占位符替换成本篇的实际设计,不重写其他字段。'
  ))

  const lines = outline.split(/\r?\n/)
  let headerAt = -1
  let headerCells = null
  for (let index = 0; index < lines.length; index++) {
    const cells = parseTableLine(lines[index])
    if (cells && cells[0] === OUTLINE_HEADERS[0]) {
      headerAt = index
      headerCells = cells
      break
    }
  }
  const headerOk = Boolean(headerCells) && headerCells.length === OUTLINE_HEADERS.length &&
    headerCells.every((cell, index) => cell === OUTLINE_HEADERS[index])
  checks.push(makeCheck(
    'phase2.outline-12-columns',
    headerOk,
    '小节大纲.md',
    headerCells ? `表头 ${headerCells.length} 列:${headerCells.join(' | ')}` : '未找到以“结构段/五段功能”开头的 Markdown 表格',
    `使用固定 12 列表头:${OUTLINE_HEADERS.join(' | ')}`,
    ['references/writing-workflow.md'],
    '只修表头和错位单元格,不改动各节已经成立的剧情内容。'
  ))

  const rows = []
  if (headerAt >= 0) {
    for (let index = headerAt + 1; index < lines.length; index++) {
      const cells = parseTableLine(lines[index])
      if (!cells) {
        if (rows.length) break
        continue
      }
      if (isSeparatorRow(cells)) continue
      rows.push({ line: index + 1, cells })
    }
  }
  const rowShapeFailures = rows.filter((row) => row.cells.length !== OUTLINE_HEADERS.length)
  checks.push(makeCheck(
    'phase2.outline-data-rows',
    rows.length > 0 && rowShapeFailures.length === 0,
    '小节大纲.md',
    rows.length === 0 ? '固定表头下没有数据行' : (rowShapeFailures.length ? `列数错误行:${rowShapeFailures.map((row) => row.line).join('、')}` : `识别到 ${rows.length} 节,均为 12 列`),
    '固定表头下至少一行数据,且每节恰好 12 列',
    ['references/writing-workflow.md'],
    '只补缺失节或修复报告行的列错位。'
  ))

  const subeventFailures = []
  const subeventRows = rows.filter((item) => item.cells.length === OUTLINE_HEADERS.length)
  for (const row of subeventRows) {
    const subevents = row.cells[2].split(/\s*(?:->|→)\s*/).filter(Boolean)
    if (subevents.length < 3 || subevents.length > 5 || subevents.some((item) => !FUNCTION_TAG.test(item))) {
      subeventFailures.push(row.line)
    }
  }
  checks.push(makeCheck(
    'phase2.outline-subevents',
    rows.length > 0 && subeventFailures.length === 0,
    '小节大纲.md',
    subeventFailures.length
      ? `子事件数量或标签错误行:${subeventFailures.join('、')}`
      : (subeventRows.length === rows.length
          ? `所有 ${rows.length} 节均有 3-5 个带功能标签的子事件`
          : `已检查 ${subeventRows.length}/${rows.length} 行;其余行由 phase2.outline-data-rows 报告列错位`),
    '每节 3-5 个以 -> 连接的子事件,每个含 {对话/冲突/伏笔/回忆/发现/递进} 标签之一',
    ['references/writing-workflow.md', 'references/short-craft.md'],
    '只修报告行的子事件单元格,不重写其他列或其他节。'
  ))

  const targetMatch = settings.match(/目标字数\s*[::]\s*(?:约\s*)?([\d,,]+)\s*(?:字)?\s*(?:[-–—~~]|到|至)\s*([\d,,]+)\s*字?/) ||
    settings.match(/目标字数\s*[::]\s*(?:约\s*)?([\d,,]+)\s*字?/)
  const toNumber = (value) => Number(String(value).replace(/[,,]/g, ''))
  const targetLow = targetMatch ? toNumber(targetMatch[1]) : null
  const targetHigh = targetMatch && targetMatch[2] !== undefined ? toNumber(targetMatch[2]) : targetLow
  const statedTarget = targetLow
  const rowTargets = subeventRows.map((row) => {
    const match = row.cells[11].match(/([\d,,]+)/)
    return match ? Number(match[1].replace(/[,,]/g, '')) : null
  })
  const targetSum = rowTargets.every((value) => Number.isFinite(value))
    ? rowTargets.reduce((sum, value) => sum + value, 0)
    : null
  const isRange = targetHigh !== null && targetHigh !== targetLow
  const targetOk = statedTarget !== null && statedTarget >= 1000 && targetHigh <= 200000 &&
    targetHigh >= targetLow && subeventRows.length === rows.length && targetSum !== null &&
    (isRange
      ? targetSum >= targetLow && targetSum <= targetHigh
      : Math.abs(targetSum - statedTarget) <= statedTarget * 0.05)
  checks.push(makeCheck(
    'phase2.target-word-sum',
    targetOk,
    '设定.md、小节大纲.md',
    `设定目标:${statedTarget === null ? '未识别' : (isRange ? `${targetLow}-${targetHigh}` : statedTarget)};大纲合计:${targetSum === null ? '有非数字目标字数' : targetSum}`,
    '设定.md 的目标字数为 1000-200000 的正整数或区间(用户明确范围优先,未指定时默认 8000-20000);大纲每行目标字数可解析,写区间时合计落在区间内,写单值时误差不超过 5%',
    ['references/writing-workflow.md', 'references/submission-craft.md'],
    '只修目标字数字段或各节“目标字数”单元格,使合计匹配;不要用增删剧情绕过数字错误。'
  ))

  const reversalMatch = settings.match(/反转位置\s*[::]\s*第?\s*(\d+)\s*节?\s*(?:÷|\/)\s*(?:共|全文)?\s*(\d+)\s*节?\s*=\s*(\d+(?:\.\d+)?)\s*%/)
  const noReversal = /反转类型\s*[::]\s*无反转/.test(settings) &&
    /反转位置\s*[::]\s*不适用\s*[((][^))]+[))]/.test(settings)
  let reversalOk = false
  let reversalEvidence = '未找到“反转位置:第 X 节 ÷ 共 Y 节 = Z%”'
  if (noReversal) {
    reversalOk = true
    reversalEvidence = '反转类型为无反转,位置已带原因标为不适用'
  } else if (reversalMatch) {
    const reveal = Number(reversalMatch[1])
    const total = Number(reversalMatch[2])
    const stated = Number(reversalMatch[3])
    const calculated = total > 0 ? (reveal / total) * 100 : NaN
    reversalOk = reveal >= 1 && reveal <= total && total === rows.length &&
      Math.abs(stated - calculated) <= 0.6
    reversalEvidence = `声明第 ${reveal} 节 ÷ 共 ${total} 节 = ${stated}%;大纲识别 ${rows.length} 节`
  }
  checks.push(makeCheck(
    'phase2.reversal-position',
    reversalOk,
    '设定.md',
    reversalEvidence,
    '有反转时写明揭穿节号、全文总节数和计算百分比;总节数须与大纲一致,计算误差不超过 0.6%。无反转时写明“反转类型:无反转”和带原因的“反转位置:不适用(…)”',
    ['references/writing-workflow.md', 'references/short-reversal.md'],
    '只修正反转位置计算或对应节次;位置质量按 short-reversal.md 的误判决策与揭示后果功能链判断,不为跨过固定百分比而搬节。'
  ))

  const paywallMatch = settings.match(/付费点\s*[::]\s*第?\s*(\d+)\s*节末/)
  const paywallSection = paywallMatch ? Number(paywallMatch[1]) : null
  const paywallRows = rows
    .map((row, index) => ({ section: index + 1, hook: row.cells[7] || '' }))
    .filter((row) => /付费点/.test(row.hook))
  const paywallOk = paywallSection !== null && paywallRows.length === 1 &&
    paywallRows[0].section === paywallSection
  let paywallEvidence = '设定.md 未按“付费点:第 X 节末”声明'
  if (paywallSection !== null && paywallRows.length === 0) {
    paywallEvidence = `设定.md 声明第 ${paywallSection} 节末;大纲“结尾承接/钩子”列未标出付费点`
  } else if (paywallSection !== null && paywallRows.length > 0) {
    paywallEvidence = `设定.md 声明第 ${paywallSection} 节末;大纲标在第 ${paywallRows.map((row) => row.section).join('、')} 节`
  }
  checks.push(makeCheck(
    'phase2.paywall-in-both',
    paywallOk,
    '设定.md、小节大纲.md',
    paywallEvidence,
    '设定.md 写“付费点:第 X 节末”,且小节大纲.md 第 X 行数据的“结尾承接/钩子”单元格只标一次“付费点”',
    ['references/submission-craft.md', 'references/writing-workflow.md'],
    '只在缺失的文件补同一个付费断点,并确保它位于对应节末的钩子单元格。'
  ))

  return report(project, checks)
}

function report(project, checks) {
  const failures = checks.filter((check) => !check.ok)
  return {
    schema_version: 1,
    verifier: 'story-short-write.phase2-contract',
    project,
    ok: failures.length === 0,
    checks,
    failures,
    repair_scope: failures.map((failure) => ({
      id: failure.id,
      file: failure.file,
      evidence: failure.evidence,
      expected: failure.expected,
      references: failure.references,
      repair: failure.repair,
    })),
  }
}

function main(argv) {
  const args = argv.filter((arg) => arg !== '--json')
  if (args.length > 1 || args.some((arg) => arg.startsWith('--'))) {
    process.stderr.write('用法: node scripts/check-phase2-contract.js --json [project-dir]\n')
    return 2
  }
  const result = verify(args[0] || '.')
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
  return result.ok ? 0 : 1
}

if (require.main === module) process.exitCode = main(process.argv.slice(2))

module.exports = { OUTLINE_HEADERS, verify }
scripts/normalize-punctuation.js
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');

const USAGE = `Usage: node normalize-punctuation.js [--check] [--quote-mode keep|ascii|yan] <file...>

Normalize正文 punctuation deterministically:
  - replace ellipses, em dashes, and double hyphens with Chinese punctuation
  - remove markdown divider lines (---) from正文
  - keep quote style by default; convert quotes only when explicitly requested
`;

const options = {
  check: false,
  quoteMode: 'keep',
  files: [],
};

for (let i = 2; i < process.argv.length; i += 1) {
  const arg = process.argv[i];
  if (arg === '--check') {
    options.check = true;
  } else if (arg === '--quote-mode') {
    const value = process.argv[i + 1];
    if (!value) die('--quote-mode requires keep, ascii, or yan');
    options.quoteMode = value;
    i += 1;
  } else if (arg.startsWith('--quote-mode=')) {
    options.quoteMode = arg.slice('--quote-mode='.length);
  } else if (arg === '-h' || arg === '--help') {
    process.stdout.write(USAGE);
    process.exit(0);
  } else if (arg.startsWith('-')) {
    die(`Unknown option: ${arg}`);
  } else {
    options.files.push(arg);
  }
}

if (!['keep', 'ascii', 'yan'].includes(options.quoteMode)) {
  die(`Invalid --quote-mode: ${options.quoteMode}`);
}
if (options.files.length === 0) {
  die('No files provided');
}

let totalFindings = 0;
let changedFiles = 0;
let failed = false;

for (const file of options.files) {
  const fullPath = path.resolve(file);
  let input;
  try {
    input = fs.readFileSync(fullPath, 'utf8');
  } catch (error) {
    failed = true;
    console.error(`${file}: unable to read (${error.message})`);
    continue;
  }

  const result = normalizeDocument(input, options.quoteMode);
  totalFindings += result.findings.length;

  if (options.check) {
    for (const finding of result.findings) {
      console.log(`${file}:${finding.line}:${finding.column}: ${finding.type}: ${finding.message}`);
    }
    continue;
  }

  if (result.output !== input) {
    fs.writeFileSync(fullPath, result.output, 'utf8');
    changedFiles += 1;
    console.log(`${file}: normalized (${result.findings.length} issue${result.findings.length === 1 ? '' : 's'})`);
  }
}

if (failed) {
  process.exit(2);
}
if (options.check && totalFindings > 0) {
  process.exit(1);
}
if (!options.check) {
  console.log(`Done. Changed files: ${changedFiles}`);
}

function die(message) {
  console.error(message);
  console.error(USAGE.trimEnd());
  process.exit(2);
}

function normalizeDocument(input, quoteMode) {
  const { lines, endings } = splitLinesKeepingEndings(input);

  const findings = [];
  const outputLines = [];
  let fence = null;
  let inFrontMatter = hasYamlFrontMatter(lines);
  let quoteOpen = false;
  let commentOpen = false;
  let commentStart = null;
  const commentCloseAhead = new Array(lines.length + 1).fill(false);
  for (let index = lines.length - 1; index >= 0; index -= 1) {
    commentCloseAhead[index] = lines[index].includes('-->') || commentCloseAhead[index + 1];
  }

  for (let index = 0; index < lines.length; index += 1) {
    const lineNo = index + 1;
    const ending = endings[index];
    let line = lines[index];
    const trimmed = line.trim();

    // 未闭合的 `<!--` 不能把余下整篇伪装成注释。确认 EOF 前已无 `-->` 时,
    // 在起始位置具名报错,并从当前行恢复正文扫描;起始符所在行仍原样保护。
    if (commentOpen && !commentCloseAhead[index]) {
      findings.push({
        line: commentStart?.line || lineNo,
        column: commentStart?.column || 1,
        type: 'html-comment-unclosed',
        message: 'HTML 注释未闭合;后续内容仍按正文检查。',
      });
      commentOpen = false;
      commentStart = null;
    }

    if (inFrontMatter) {
      outputLines.push(line + ending);
      if (index > 0 && trimmed === '---') inFrontMatter = false;
      continue;
    }

    if (fence) {
      outputLines.push(line + ending);
      if (isClosingFence(line, fence)) fence = null;
      continue;
    }

    const openingFence = parseOpeningFence(line);
    if (openingFence) {
      fence = openingFence;
      outputLines.push(line + ending);
      continue;
    }

    // 跨行 HTML 注释里的 `---` 是注释内容,不是正文分隔线。
    if (trimmed === '---' && !commentOpen) {
      findings.push({
        line: lineNo,
        column: line.indexOf('-') + 1,
        type: 'markdown-divider',
        message: '正文中不要使用 markdown 分隔线;建议移除该行。',
      });
      continue;
    }

    const commentOpenBefore = commentOpen;
    const punctuationResult = normalizePausePunctuation(line, lineNo, commentOpen);
    findings.push(...punctuationResult.findings);
    line = punctuationResult.line;
    commentOpen = punctuationResult.commentOpen;
    if (!commentOpenBefore && commentOpen) {
      commentStart = { line: lineNo, column: Math.max(1, line.lastIndexOf('<!--') + 1) };
    } else if (!commentOpen) {
      commentStart = null;
    }

    const quoteResult = normalizeQuotes(line, quoteMode, quoteOpen, lineNo);
    findings.push(...quoteResult.findings);
    line = quoteResult.line;
    quoteOpen = quoteResult.quoteOpen;

    outputLines.push(line + ending);
  }

  if (commentOpen) {
    findings.push({
      line: commentStart?.line || lines.length,
      column: commentStart?.column || 1,
      type: 'html-comment-unclosed',
      message: 'HTML 注释未闭合;后续内容仍按正文检查。',
    });
  }

  return {
    output: outputLines.join(''),
    findings,
  };
}

// 逐行记住原始行尾。整篇按「文件里出现过 \r\n」统一行尾会让一个孤立 CRLF 把全文
// 行尾都翻成 CRLF——那是一次没人要求的全文件 diff,而 --check 对行尾一个 finding
// 都不报,只改标点的这一步不该动它。
function splitLinesKeepingEndings(input) {
  const lines = [];
  const endings = [];
  let cursor = 0;

  while (cursor < input.length) {
    const newlineIndex = input.indexOf('\n', cursor);
    if (newlineIndex === -1) {
      lines.push(input.slice(cursor));
      endings.push('');
      break;
    }
    const crlf = newlineIndex > cursor && input[newlineIndex - 1] === '\r';
    lines.push(input.slice(cursor, crlf ? newlineIndex - 1 : newlineIndex));
    endings.push(crlf ? '\r\n' : '\n');
    cursor = newlineIndex + 1;
  }

  return { lines, endings };
}

function parseOpeningFence(line) {
  const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
  if (!match) return null;

  const marker = match[1];
  const rest = match[2];
  if (marker[0] === '`' && rest.includes('`')) return null;

  return { marker: marker[0], minimumLength: marker.length };
}

function isClosingFence(line, fence) {
  const marker = fence.marker === '`' ? '`' : '~';
  const match = line.match(new RegExp(`^ {0,3}(${marker}{3,})[\\t ]*$`));
  return Boolean(match && match[1].length >= fence.minimumLength);
}

// 删空停顿符会把两侧的半角点/连字符粘成新的 `...`/`--`(`他.……..说` → `他...说`),
// 一遍归一化留不干净,再跑一遍还会改已定稿的正文;所以反复归一化到不动点。
// 每遍至少把一个 `…/./—/-` 换成非停顿字符,字符数严格递减,必然收敛。
// findings 只留第一遍:同一处不重复计数,column 也仍然是原行的偏移。
function normalizePausePunctuation(line, lineNo, commentOpen) {
  let current = line;
  let findings = null;
  let commentOpenAfter = commentOpen;

  for (;;) {
    const comments = htmlCommentSpans(current, commentOpen);
    commentOpenAfter = comments.open;
    const pass = normalizePausePunctuationPass(current, lineNo, comments.spans);
    if (findings === null) findings = pass.findings;
    if (pass.line === current) break;
    current = pass.line;
  }

  return { line: current, findings, commentOpen: commentOpenAfter };
}

function normalizePausePunctuationPass(line, lineNo, commentSpans) {
  const findings = [];
  const original = line;
  const pattern = /…+|\.{3,}|——|—|--+/g;
  let output = '';
  let lastIndex = 0;
  let match;

  while ((match = pattern.exec(original)) !== null) {
    const token = match[0];
    // HTML 注释是正文里的元信息(如 `<!-- 去味:跳过 -->` 豁免标记):`<!--`/`-->` 里的
    // `--` 不是停顿标点,改掉它注释就散了,标记会变成读者看得见的正文。
    if (insideSpans(match.index, match.index + token.length, commentSpans)) continue;
    output += original.slice(lastIndex, match.index);
    const replacement = choosePauseReplacement(original, match.index, token.length);
    output += replacement;
    findings.push({
      line: lineNo,
      column: match.index + 1,
      type: getPauseType(token),
      message: replacement ? `替换为「${replacement}」。` : '移除重复标点。',
    });
    lastIndex = match.index + token.length;
  }

  output += original.slice(lastIndex);
  return { line: output, findings };
}

// 行内 HTML 注释区间(含 `<!--`、`-->` 本身);注释可跨行,未闭合时把状态交给下一行。
function htmlCommentSpans(line, openBefore) {
  const spans = [];
  let open = openBefore;
  let cursor = 0;

  while (cursor < line.length) {
    if (open) {
      const close = line.indexOf('-->', cursor);
      if (close === -1) {
        spans.push([cursor, line.length]);
        return { spans, open: true };
      }
      spans.push([cursor, close + 3]);
      cursor = close + 3;
      open = false;
      continue;
    }

    const start = line.indexOf('<!--', cursor);
    if (start === -1) break;
    cursor = start;
    open = true;
  }

  return { spans, open };
}

function insideSpans(start, end, spans) {
  return spans.some(([spanStart, spanEnd]) => start < spanEnd && end > spanStart);
}

function hasYamlFrontMatter(lines) {
  if (!lines[0] || lines[0].trim() !== '---') return false;
  let sawYamlField = false;
  for (let i = 1; i < Math.min(lines.length, 40); i += 1) {
    const trimmed = lines[i].trim();
    if (trimmed === '---') return sawYamlField;
    if (/^[A-Za-z0-9_-]+:\s*/.test(trimmed)) sawYamlField = true;
  }
  return false;
}

function getPauseType(token) {
  if (token.startsWith('-')) return 'double-hyphen';
  if (token.includes('—')) return 'em-dash';
  return 'ellipsis';
}

function choosePauseReplacement(text, start, length) {
  const before = previousNonSpace(text, start - 1);
  const after = nextNonSpace(text, start + length);
  const rest = text.slice(start + length).trimStart();

  // 正文产物不保留 `……`、`——`、`—` 或 `--`;对话打断和数字区间不设例外。
  if (before === '') return '';
  // 紧跟开引号/开括号的停顿符号属于句首边界,删空即可,避免产出 `「,…」` 或 `「。」`。
  if (isOpeningDelimiter(before)) return '';
  if (/\d/.test(before) && /\d/.test(after)) return '到';
  if (isClosingQuote(after)) return isSentencePunctuation(before) ? '' : '。';

  if (!after) return isSentencePunctuation(before) ? '' : '。';
  if (isSentencePunctuation(before) || isPunctuation(after)) return '';
  if (/^(因为|原来|这是|那是|也就是|换句话|说白了|所谓|答案|原因|结果|真相|问题在于)/.test(rest)) return ':';
  if (/(原因|答案|真相|结果|结论|问题|选择|意思)$/.test(text.slice(0, start).trim())) return ':';
  return ',';
}

function previousNonSpace(text, index) {
  for (let i = index; i >= 0; i -= 1) {
    if (!/\s/.test(text[i])) return text[i];
  }
  return '';
}

function nextNonSpace(text, index) {
  for (let i = index; i < text.length; i += 1) {
    if (!/\s/.test(text[i])) return text[i];
  }
  return '';
}

function isSentencePunctuation(ch) {
  return /[,,。.!!??;;::…]$/.test(ch || '');
}

function isPunctuation(ch) {
  return /[,,。.!!??;;::、…"“”'‘’」』))]/.test(ch || '');
}

function isClosingQuote(ch) {
  return /["”」』]/.test(ch || '');
}

function isOpeningDelimiter(ch) {
  return /[「『((“‘]/.test(ch || '');
}

function normalizeQuotes(line, quoteMode, quoteOpen, lineNo) {
  if (quoteMode === 'keep') {
    return { line, findings: [], quoteOpen };
  }

  const findings = [];
  let output = '';

  for (let i = 0; i < line.length; i += 1) {
    const ch = line[i];
    if (quoteMode === 'ascii' && /[「」『』“”]/.test(ch)) {
      output += '"';
      findings.push({ line: lineNo, column: i + 1, type: 'quote-style', message: '按显式 quote-mode 转为半角双引号。' });
      continue;
    }
    if (quoteMode === 'yan' && (ch === '"' || ch === '“' || ch === '”')) {
      const replacement = quoteOpen || ch === '”' ? '」' : '「';
      output += replacement;
      quoteOpen = replacement === '「';
      findings.push({ line: lineNo, column: i + 1, type: 'quote-style', message: '按显式 quote-mode 转为盐言引号。' });
      continue;
    }
    output += ch;
  }

  return { line: output, findings, quoteOpen };
}
SKILL.md
---
name: story-short-write
version: 1.0.0
description: "短篇网文写作。辅助短篇小说创作,从构思到成稿,聚焦情绪拉扯与节奏把控。触发方式:/story-short-write、/写短篇、「帮我写一篇短篇」「写个盐言故事」。"
metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}}
---
# story-short-write:短篇网文写作

你是短篇网文写作执行器。从构思到成稿,完成一篇完整的短篇小说。

**执行规则:短篇以情绪为目标,所有内容为情绪服务。**

## 阶段 Reference Gate(强制,先读后写)

任何创建或修改故事文件的动作之前,先判断当前 Phase,并完成该阶段的 reference gate。**只读本 SKILL.md 不算完成门禁。**

Phase 2 必须在第一次写入 `设定.md` / `小节大纲.md` 前按顺序完整读取(分块直到 EOF;`rg` 检索或局部摘读不算读完):

1. `references/writing-workflow.md`、`references/submission-craft.md`、`references/short-craft.md`、`references/short-reversal.md`
2. 核心 10 题材再读取一个精确的 `references/genre-styles/{题材}.md`;冷门题材改读 `references/genre-writing-formulas.md`
3. 有反派或真相揭露设计时再读 `references/villain-and-reveal.md`;不适用时在设计校验区写明原因

任一必需路径不存在、不可读或题材尚未解析到唯一 reference 时,立即停止,报告准确路径/待定项,**不得创建或修改故事产物**。不要把“已读 references”的回执写进故事文件;要把选出的题材招式、反转计算等应用证据写进正常设计字段。Phase 3/4 的按需加载仍分别服从下文“写前准备”和精修检查,不得用早先读过代替当前任务完整回读。

---

> Agent 只查当前端 canonical 目录(Claude `.claude/agents`、OpenCode `.opencode/agents`、Codex `.codex/agents` TOML、Antigravity `.agents/agents`),不借其他端文件误判。Claude/OpenCode 用 `subagent_type`,Codex 用 `agent_type`,Antigravity 用 `invoke_subagent` + `TypeName`;能力/文件缺失、unknown agent 或 ZCode 3.3.4 时报告 `Fallback: project custom agents unavailable -> solo` 并 solo/direct。
>
> Spawn 版本提示(不阻断 spawn):先读取项目根 `.story-deployed` 的 `agents_version`。与本版 `agents_version: 28` 不一致时(标记缺失、字段缺失/非整数、小于或大于 28)**照常按文件存在性检查并 spawn**,同时报告 `Notice: agents bundle 版本不匹配(项目 {N},本版 28)` 并提示重新运行 `/story-setup` 后新开会话;大于 28 时额外提示先更新 oh-story-claudecode,不要用本地旧版 setup 降级覆盖。只有 agent 文件缺失、或运行时不暴露 custom agent 时才降级 solo/direct,报告 `Fallback: ... -> solo`。

## 执行规则

1. **先定情绪,再定故事**。动笔前必须确定目标情绪(意难平/反转震撼/爽感释放/治愈温暖/细思极恐/共鸣感动),所有内容为这个情绪服务。
2. **一个核心支点撑一篇**。反转型围绕一次主揭示蓄力;无反转型围绕报应兑现或甜度递进积累期待。不多线、不铺世界观。
3. **每句话必须有用**。不推动剧情、不铺垫反转、不推高情绪的句子 → 删。
4. **开头 3 句定生死,结尾定传播**。开头必须包含钩子,结尾必须有余韵。
5. **默认第一人称**。短篇网文(盐言/七猫短篇等)绝大多数用第一人称,代入感最强。除非题材明确需要第三人称(如多视角悬疑),否则一律用「我」。

---

## 格式规范(最高优先级)

详细规则见 `references/short-format.md`,写作前必须加载。**主会话与 narrative-writer 子代理使用同一套正文格式**:正文只允许保存在 `正文.md`,正文相邻段落之间只允许一个换行符 `\n`(不得出现空行/`\n\n`),对话引号风格按项目/平台约定统一(默认半角双引号,盐言可用「」),短篇小节标记全文统一(默认 `###1.`/`###2.`)。如果子代理输出与主会话格式不一致,按本格式规范重排后再写入文件。

---

## 核心方法

除了上面的执行规则,构思和写作时遵循:

- **从验证过的模式出发**:有对标书就先拆解,没有就从 `genre-styles/{题材}.md`(核心 10 题材)或 `genre-writing-formulas.md`(冷门题材)找对应的短篇剧情模式
- **定方向就换风格**:题材方向一旦确定(如追妻火葬场),立刻加载 `references/genre-styles/{题材}.md`——正文的腔调、开篇、钩子、情绪烈度、对话金句、招式、收尾全部切到该题材。核心 10 题材(追妻火葬场 / 世情打脸 / 复仇打脸 / 总裁豪门 / 宅斗宫斗 / 民俗怪谈 / 悬疑 / 甜宠 / 双男主 / 沙雕脑洞)有专属风格包,其中追妻含 现代/古代/民国 时代变体与 小三文学/死人文学 流派分支;冷门题材用 `genre-writing-formulas.md` 的结构骨架兜底,腔调仍按 `short-craft.md` 通用底座
- **只加载必需信息**:写每节前明确目标情绪和要用的技法,答不出就先回读参考
- **复用作者习惯**:若作者记忆 state 已存在,正文前用 `scripts/author_memory_commit.py query --kind prose_style --kind story_design` 获取相关 active 条目(总输出 ≤2KB),传给实际正文/改写 agent 作为自然倾向,不逐条展示或最大化命中,不牺牲连贯、节奏和字数;硬门禁、当前请求和本篇设定优先。明确长期声明在收尾用 `record` 写入并回传回执,细则见 [references/author-memory.md](references/author-memory.md)。

---

## 写作流程

### Phase 1:确定情绪目标

问用户:**「你想让读者读完什么感觉?有没有想写的题材方向或灵感?」**

如果用户有明确想法 → 直接进入 Phase 2。

如果用户只有模糊想法 → 帮用户做情绪选择:

| 情绪类型 | 适合场景 | 难度 | 市场热度 | 常配题材包 |
|----------|----------|------|----------|------------|
| 意难平 | 虐恋、遗憾、错过 | 中 | 🔥🔥🔥 | 追妻火葬场 / 甜宠(先虐后甜) |
| 反转震撼 | 悬疑、身份错位 | 高 | 🔥🔥🔥 | 悬疑 / 沙雕脑洞(反套路) |
| 爽感释放 | 打脸、逆袭 | 低 | 🔥🔥 | 世情打脸 / 复仇打脸 / 总裁豪门 / 宅斗宫斗(古代上位) |
| 治愈温暖 | 成长、亲情、友情 | 中 | 🔥🔥 | 甜宠 / 双男主(救赎线) |
| 细思极恐 | 悬疑、心理 | 高 | 🔥 | 悬疑 / 民俗怪谈 |
| 共鸣感动 | 现实、职场、婚姻 | 中 | 🔥🔥🔥 | 世情打脸(共鸣模式) / 追妻火葬场(小三文学) |

---

### Phase 2:构思核心框架

> 如果用户有参考小说,先用 `/story-short-analyze` 拆解。默认输出存入项目根目录 `拆文库/{书名}/`;如用户指定当前短篇引用目录,则可输出/同步到 `{短篇标题}/对标/{书名}/`。写作时会自动查找并读取这些拆文结果,不需要用户手动复制到 prompt。

#### 对标上下文加载

> **拆文库/对标关系**:`拆文库/` = analyze skill 的原始产出(数据源),位于项目根目录。`对标/` = 当前短篇的引用视图,位于 `{短篇标题}/对标/`。短篇写作优先读取 `{短篇标题}/对标/{书名}/`,不存在则读取项目根 `拆文库/{书名}/`。

推荐目录结构:

```
项目根/
├── 拆文库/
│   └── {书名}/
│       ├── 拆文报告.md
│       ├── 情节节点.md
│       └── 写作手法.md
└── {短篇标题}/
    ├── 设定.md
    ├── 小节大纲.md
    ├── 正文.md
    └── 对标/
        └── {书名}/
            ├── 拆文报告.md
            ├── 情节节点.md
            └── 写作手法.md
```

**对标发现(先于下方反应式加载)**:项目根 `拆文库/` 有拆过的短篇时,先按题材主动推荐一本对标,不要被动等用户开口。

1. `ls 拆文库/` 列书目;先从当前项目目录名和 `设定.md`「基本信息」识别本篇标题,排除同名或来源指向当前 `正文.md` 的 `拆文库/{当前书}/`。story-import 生成的本书拆文分析属于续写基线,不是对标候选。排除后为空 → 跳过(无对标按题材包写,见 Phase 1 情绪→题材包表)。
2. 逐本读 `拆文库/{书}/_meta.json` 的 `genre_detected`,与本篇题材比对,标 同题材 / 弱相关。
3. 有候选 → 用 AskUserQuestion 推荐(列候选书 +「不用,按题材包写」)。选定后记入本篇 `设定.md`「对标摘要」区作主对标,并按上方「拆文库/对标关系」规则把 `拆文库/{书}/` 同步到 `{短篇标题}/对标/{书}/`。

如果工作目录下存在 `对标/` 或项目根存在 `拆文库/`,或用户提到参考小说:

1. 先按上方「对标发现」第 1 条的同一口径识别本篇,另排除历史误建的 `对标/{当前书}/`;排除后没有外部对标时按题材包写,不进入下面几步。
2. 按上述顺序查找 `拆文报告.md`、`情节节点.md`、`写作手法.md`、`_meta.json`
3. **读 `_meta.json.genre_detected`,按下表加载对应题材风格包**(analyze 识别的题材 → write 的 genre-styles 包),正文腔调/招式随之切换:

   | analyze 的 `genre_detected` | 加载 `genre-styles/` 包 |
   |---|---|
   | 追妻(现代 / 古代 / 民国) | `追妻火葬场.md`(按「时代变体」节切换身份词与招式) |
   | 小三 / 死人文学 | `追妻火葬场.md`(「流派分支」节) |
   | 世情 / 打脸爽文 / 家庭伦理 | `世情打脸.md` |
   | 重生复仇 | `复仇打脸.md` |
   | 豪门 / 总裁(豪门联姻虐恋) | `总裁豪门.md` |
   | 宫斗宅斗 / 宫斗 / 宅斗 / 古言重生 | `宅斗宫斗.md` |
   | 民俗 / 怪谈 / 灵异 | `民俗怪谈.md` |
   | 悬疑 / 推理 / 惊悚 | `悬疑.md` |
   | 甜宠 / 先虐后甜 / 先婚后爱 | `甜宠.md` |
   | 双男主 | `双男主.md` |
   | 沙雕 / 脑洞 / 弹幕 / 系统 | `沙雕脑洞.md` |
   | 仙侠 / 通用 | 无专属包 → `short-craft.md` 底座 + `genre-writing-formulas.md` 兜底 |

4. 读取核心发现:结构段落、情绪曲线、反转位置、铺垫方式、句式节奏、可借鉴技法。**把拆文报告里的具体招式对到题材包招式库**:拆文给「这一篇怎么做的」,题材包给「这一类通用怎么做」,两者合用——拆文是当前对标书的实证,题材包是该题材的通法
5. 写入本篇 `设定.md` 的“对标摘要”区,写作时每个场景从中召回 1-2 个相关技法
6. 如只找到原文、未找到拆文报告,提示用户先运行 `/story-short-analyze`;如用户要求继续,也可只按原文做弱参考

> **拆文产出格式**:analyze 落盘的完整文件树、`_meta.json` schema、Stage→文件映射,以及「story-short-write 怎么读这些产出」的下游消费规范,见 [references/output-contract.md](references/output-contract.md)。

> **多对标书时**:参 `references/cross-book-recall.md`,副对标 anchor 入「对标摘要」区

#### Agent 调用:story-architect

构思阶段,如果项目已部署 story-architect agent(查找顺序见顶部),可 spawn `Agent(subagent_type: "story-architect", prompt: "项目目录:{dir}\n任务类型:短篇构思\n查询参数:{情绪目标+题材方向}")` 辅助框架设计。如 agent 不可用,由主线程直接执行。

帮用户确定短篇的核心框架:

```
## 短篇核心框架

### 基本信息
- 标题(暂定):{}
- 目标字数:{} 字(短篇通常 8000-20000 字)
- 目标平台:{知乎盐选 / 小程序 / 番茄短篇}(三选一)
- 情绪目标:{读者读完的感受}

### 一句话梗概
{主角 + 困境 + 反转 + 情绪落点}

### 核心支点
- 类型:{身份/视角/动机/时间线/信息/认知反转,或无反转}
- 核心兑现:{一句话描述主揭示;无反转时写报应或关系兑现}
- 铺垫/期待:{关键铺垫点;无反转时写读者等待兑现的因果或关系节拍}

### 情绪设计
- 开头情绪:{}(强度 {1-10})
- 中段情绪:{}(强度 {1-10})
- 反转情绪:{}(强度 {1-10},峰值维持 ≥2 节)
- 结尾情绪:{}(强度 {1-10})
- 反转高潮不要骤降:反转前 1 节开始升温,反转节达到峰值,反转后 1 节维持峰值不骤降

### 人设速写
- 主角:{一句话人设}
- 关键角色:{一句话人设}
- 关系:{他们之间的关系}
```

框架确定后,完成设计任务,然后在工作目录下创建文件。

#### 设计任务(框架确定后执行)

详细步骤和模板见 `references/writing-workflow.md`。构思时从目标情绪反推剧情,不是从灵感正向构建。按顺序完成:

1. 定平台基调 + 加载题材风格包 → 先读 `references/submission-craft.md` 定投稿平台(知乎/小程序/番茄),正文视角、矛盾烈度、章末落点随之切换;再读 `references/genre-styles/{题材}.md`(核心 10 题材)+ 通用底座 `references/short-craft.md`,从招式库选 2-3 个核心招式(如追妻的白月光触发链 / 信物翻转 / 火葬场预告),写入 设定.md「题材招式」区,全程照此招式与腔调写
2. 设计反派(如有)→ 加载 `villain-and-reveal.md`
3. 确定揭露方式 → 同上
4. 编写 小节大纲.md(格式见 writing-workflow.md):短篇只做轻量蓝图,每节包含结构段/五段功能、人物/关系变化、因果/逻辑链、结尾承接/钩子,不套长篇完整章节蓝图。**标出付费点卡在哪一节末**(见 `submission-craft.md`「付费点」:卡脖子断点、付费点前每章 2-3 剧情点、付费点后每章多 1-2);用反推法先想透付费点那一节,再倒排前后。每节可选一个任务卡点,但必须服务情绪升级、证据推进、关系撕裂、反转铺垫或反击动作;没有就不强补
5. 反转信息差验证(公式见 writing-workflow.md)
6. 伏笔回查清单(标准见 writing-workflow.md)

`设定.md` 必须包含以下机器可验收字段(内容本身也是后续写作依据,不是读文件回执):

```markdown
## Phase 2 设计校验
- 题材参考:`references/genre-styles/{题材}.md`(冷门题材写 `references/genre-writing-formulas.md`)
- 核心招式:{招式一};{招式二}[;{招式三}]
- 反派设计:不适用({原因})
- 反转类型:{身份/视角/动机/时间线/信息/认知/无反转}
- 反转位置:第 {X} 节 ÷ 共 {Y} 节 = {Z}%
- 付费点:第 {N} 节末
```

有反派时用 `villain-and-reveal.md` 的身份、动机、作恶方式、致命弱点、报应五字段替换“不适用”。`short-reversal.md` 判断确属无反转题材时,写 `反转类型:无反转` 和 `反转位置:不适用({报应兑现/甜度递进等原因})`,不要硬编节号。`小节大纲.md` 必须使用 `writing-workflow.md` 规定的固定 12 列 Markdown 表格,并在对应节末明确标出“付费点”。

#### Phase 2 完成门禁

两份文件生成后、向用户声明构思完成或进入 Phase 3 前,运行 `node scripts/check-phase2-contract.js --json {短篇目录}`:

- exit 0:Phase 2 机械契约通过,才可进入下一阶段;这不替代故事质量判断
- exit 1:只把 `repair_scope` 中的检查 ID、证据、期望、reference 路径和修复范围交给本轮 writer;只改失败字段,再运行同一命令
- 最多做 2 轮定向 repair;仍失败则停止并报告剩余检查 ID,不得声称 Phase 2 已完成
- exit 2、脚本缺失或不可执行:报告 verifier 不可用,不得用泛化“自检”替代后继续

#### Agent 调用:character-designer

设计任务完成后,如果项目已部署 character-designer agent(查找顺序见顶部),可 spawn `Agent(subagent_type: "character-designer", prompt: "项目目录:{dir}\n任务类型:角色设定\n查询参数:{人设速写+关系}")` 辅助角色设定和语言风格档案。如 agent 不可用,由主线程直接执行。

---

### Phase 3:逐场景写作

**项目文件结构**:文件结构见 Phase 2;设定.md/小节大纲.md 为 Phase 2 产出,正文.md 为 Phase 3 产出。

**导入项目续写基线**:`设定.md` 存在「本书续写基线」时先读取,作为已写内容的内部连续性与既有写法约束;它不是对标摘要,不参与主/副对标排序,也不复制到 `对标/`。

> 术语说明:Phase 3 按「段」划分叙事结构(开头段/铺垫段/升级段/反转段/结尾段),每段包含若干「小节」(数字编号的 beat)。「场景」指写作时的具体画面。

**交付参数先锁定**:用户明确的字数范围优先,逐字取其最小值/最大值与节数;只给单一目标时用目标的 95%-105%;都未给时用 8000-20000 字和大纲节数。后文的默认字数不得覆盖用户范围。

**写前准备**(每个场景写前执行 2 步,是核心方法的落地:确认情绪目标 → 召回技法模块):
- **步骤 1:记忆+召回**:① 本场景目标情绪词?② 借鉴哪个参考文件的哪个技法?③ 具体用在哪个段落?答不出 → 先回读参考再动笔。如有 `对标/` 或 `拆文库/` 结构化产出,按“对标上下文加载”规则检索与当前场景最相关的结构/情绪/反转/写作手法模块作为参考,并写入“拆文召回摘要”
  - **多对标书时**:参 `references/cross-book-recall.md`,副对标/参考对标按阶段预算进入"副对标召回摘要";正文只传摘要,不传副书文风或原文
- **步骤 2:指令确认**:用一句话概括本场景写作意图(情绪+技法+适配段落),并确认本场景是否有任务卡点、它卡出哪种情绪变化或新证据;没有就不强补。确认后开始写作

**写作指令:按三维度揉进逐场景写作,不照搬大纲腔。**

- 每个场景让读者和主角一起经历;发生、感知、反应揉在同一段连续正文里,不按维度分三段。
- 段落按戏剧单元/画面自然断开:新动作、新线索、新对话、视线切换另起;完整推理、氛围或情绪链可稍长。
- 高潮/打脸/反转压短,沉淀/推理/收束可长一点;爽点 beat 写密,过场 beat 写疏,避免通篇同长度。
- 主语节奏:段首或主语重置时可点名;同一动作链内优先代词/省略;关键转折再点名。
- 标点跟语气走:质问用问号,爆发处少量感叹;犹豫、未尽、打断用动作停顿、短句或换行处理,正文不使用 `……` / `——` / `—` / `--`。
- 短篇默认第一人称在场:受虐段可直白宣泄,反击段可冷静审判;只删中立无情绪的作者讲解,不删带主角偏色的审判/预告。
- 情绪可以直写,但后面要接场景里特有的动作或物件;没有具体承接的情绪总结句才删。
- 任务卡点也可以承接情绪,但必须直接加重羞辱、误会、背叛、证据、反击或心死节点;删掉后情绪/证据/关系无损就压缩。
- 情绪宁烈不温,冲突前置、爽点具体、台词带刺;心死/余韵等以克制为爽感的桥段按题材包收敛。

#### Agent 调用:narrative-writer

正文写作阶段默认由主会话按 2-3 节/批分批写正文;主会话输出是短篇正文的标准形态,不要求单次 agent spawn 完成 8000+ 字全文。

- 每批写完后更新“已写小节摘要”(3-5 条:已揭示信息、情绪位置、未回收伏笔、下一批衔接句)。
- 下一批先读该摘要和 `正文.md` 尾部 300-500 字再续写。
- 只有用户明确要求子代理、主会话上下文不足,或需要隔离试写时,才检查 narrative-writer agent(查找顺序见顶部)。
- 如可用,spawn `Agent(subagent_type: "narrative-writer", prompt: ...)`,只传项目目录、输出文件、情绪目标、题材风格包、小节大纲、角色、主/副对标召回摘要、作者偏好 query 中的文风/故事设计项、格式硬约束和写作硬约束。
- 不把本 skill 整段规则塞进 prompt;细节以已加载的 `short-format.md`、题材包和 `short-craft.md` 为准。
- 无论谁写,写入 `正文.md` 前都按同一格式规范重排,保证主会话与子代理输出一致。

⚠️ **硬约束:每节 ≥ 800 字 / 50-65 行**。
题材例外:爽文、打脸、系统流等高信息密度题材可降至 ≥ 500 字/节(见 genre-writing-formulas.md 各题材速查表),但不得低于 500 字。
写完每节后必须按 `short-format.md`「字数统计」的跨平台命令统计字数和行数。不足 800 字(高信息密度题材不足 500 字)的节不得跳过,必须补充既有情节点/对话后再写下一节。整篇以锁定的用户交付范围为准;未给范围时才使用 8000-20000 字默认值。
**⚠️ 未进入锁定范围 = 正文未完成。禁止越界后结束;不足只扩已有情节点,超出只压重复解释,不借 repair 新增或删除关键剧情。**

**节数守恒**:正文节数必须等于小节大纲规划节数。不得合并多节为一节。如果写作中发现某节不需要独立存在,应回到大纲阶段调整,而非在写作时偷减。

**节长达标流程**:
1. **写作时**:按三维度揉进写每个子事件——发生、感知、反应揉进同一段连续正文,不按维度分段写
2. **字数不足时**(逐节统计后):用以下方法补足(优先级从高到低):
	   - 补充更多子事件(回到小节大纲补充)
	   - 补有用的任务卡点(只在角色本来有要办的事时使用;必须卡出情绪、证据、关系或反转变化,并通过“删掉试试”)
	   - 加一轮对话(参考 short-craft.md 第6节 / dialogue-mastery.md 对话权力模式)
	   - 加回忆闪回(1-2 句关联记忆)
	   - 加环境物件(通过动作带出,不独立成句)
   - **禁止凑字**:每个添加必须推动情绪/铺垫/代入感,不得灌水。禁止用"加感知层""加反应层"的方式在已有动作上叠加描写

**节长验证(分批写作,每批写完后执行)**:
分批写作:每次输出 2-3 节,写完后统一检查本批所有节的字数。
如果任何一节 < 800 字(高信息密度题材 < 500 字)→ 补充更多子事件/对话来补足后再写下一批。
禁止跳过未达标的小节。

> **节长速算**:平均每行 15 字 × 55 行 ≈ 825 字。写到第 30 行时如果还不到 500 字,说明子事件数量不够,需要补充更多子事件或对话。

每个小节按「场景信息揉进」写作(详见 short-craft.md 第 10 节):发生是主干,感知和反应只在提供新信息时加入;用到的维度揉进同一镜头,子事件合计 ≥150 字。揉进不等于按维度分段——禁止"先写发生再补感知再补反应"的堆叠写法,也不要求三项齐全;同样不等于一段到底,按新动作/新物件/新信息/新对话断段。长度只是诊断,先判断是否完整戏剧单元;混入多个动作/信息才拆,完整推理、氛围或情绪链可以保留稍长段。

**写完后对照 小节大纲.md 检查**:每个子事件的现场、因果和下一步是否读得懂?已经写入的感知/反应是否提供不同信息,而非凑格?本节情绪到位?伏笔/物件已植入?新增任务卡点是否卡出了情绪、证据或关系变化(删掉无损则压缩)?节长 <800 字 → 补充更多子事件/对话后再写下一节,不拿感官或身体动作注水。

按以下结构分段写:

#### 第一段:开头(前 300-500 字)

**目标**:3 句话内抓住读者。**必须包含一个开篇钩子**(从 hooks-chapter.md 选择类型)。

**先写导语**:正文开头前先按 `references/submission-craft.md`「导语」写一条 150-220 字导语——四维骨架(起因+核心冲突+人设底色+情绪反转)配黄金三角(具体物件+信息差+留白钩子),一句一段(黑岩/盐言导语形态;番茄导语按 short-format.md 短段叙织)——完整句各自独立成段,不是拆成三字碎句。它就是正文开头的头几段,写好顺势往下接、不重写,所以首句同样守下面的开头零环境和前 100 字事件密度≥3(首句是事件/动作/信息炸弹,不是背景或弧线概括),剧透钩子放导语后半。

**技法指令**:前 100 字事件密度 ≥ 3,不做背景铺垫,直接上事件链。

**开头零环境规则**(默认适用;悬疑、惊悚、灾难、强氛围题材可例外):
- 前 3 句禁止出现无事件承载的环境描写(灯光、天气、气味、温度、装修)
- 前 3 句必须是:事件 / 对话 / 动作 / 信息炸弹,四种之一
- 任务卡点可以作为动作/事件钩子,但必须立刻带出赌注或矛盾,不能先写流程再解释意义
- 环境细节只能揉进角色的动作和感知中自然带出,不能独立成句;例外题材中,环境也必须携带威胁、异常或信息差
- 检查方法:标出前 3 句的主语,如果主语是环境物件(灯光/走廊/房间/天气),重写

开头技巧:

| 技巧 | 说明 | 示例 |
|------|------|------|
| 冲突前置 | 第一句就是矛盾 | 「离婚协议放在桌上,他已经签了。」 |
| 信息差钩 | 给读者一个角色不知道的信息 | 「她不知道,对面那个男人已经在计划第三次了。」 |
| 反常行为 | 用一个不合常理的行为引起好奇 | 「她把订婚戒指冲进了马桶。」 |
| 重生反常 | 重生后做前世绝不会做的事 | 「沈栀心念成灰,支着一口气找到了媒婆:郭家的那个天阉,我来嫁。」 |
| 超自然身份 | 开篇揭示非人类身份 | 「我是世上仅存的红衣厉鬼。我不知自己是怎么死的。」 |
| 灵魂旁观 | 以灵魂视角描述死亡现场 | 「我的尸体躺在透明棺材里,三个哥哥在外面笑着说:她演得真像。」 |
| 悬念句 | 抛出一个需要解释的事实 | 「我死后的第三天,老公发了一条朋友圈。」 |
| 替嫁被弃 | 被迫接受不公正的命运 | 「三个月后,我代替皇后的嫡亲公主坐上了去漠北和亲的轿撵。」 |
| 代入式提问 | 直接让读者产生共鸣 | 「你有没有在深夜接到过一个不该接的电话?」 |

#### 第二段:铺垫(占全文 30-40%)

- 用物件/数字/习惯建立羁绊(详见 emotional-methods.md「羁绊铺设」)
- 埋入至少 3 个反转线索,分散在不同小节
- 每 2-3 个小节埋一个钩子(类型从 hooks-paragraph.md 选择)
- 小节用数字分割,每小节推进一个情节点
- 情绪强度逐节递增,不允许连续 2 节无情绪变化
- **贯穿道具第 1 次出现必须在此段完成**
- **反派作恶按阶梯递增**(小恶→中恶,见 villain-and-reveal.md)

#### 第三段:升级(占全文 20-30%)

- 冲突必须比上一段升级(强度/范围/代价至少一个维度上升)
- 插入倒计时钩子或代价钩子制造紧迫感
- 钩子密度提高到每 2 节一个(按题材分级见 genre-writing-formulas.md)
- 埋入误导信息,让读者猜错反转方向
- **数字/金额递增作为叙事工具**(具体数字替代模糊描述,见各 genre-styles 招式库「数字承重」)
- **一动一静交替**:每节有动有静,不连续暴力也不连续安静

#### 第四段:反转(占全文 10-15%)

- 反转在一节内完成揭示,不拖延
- 揭示后确保前面铺垫的线索可被回溯(读者能找到「原来如此」的伏笔)
- 反转节的情绪冲击强度必须 > 前面所有节的最高值
- **用证物/证人/偷听/剥洋葱揭露真相**(4 种方式见 villain-and-reveal.md)
- **贯穿道具第 2 次出现必须在此段完成**(意义被颠覆)

#### 第五段:结尾(占全文 5-10%)

- 章末必须有钩子(悬念或余韵)
- 用安静细节收尾(一个物件、一个动作、一句短话),不写大段抒情
- 结尾方式见下表,参考 emotional-methods.md「余韵钝痛」
- **贯穿道具第 3 次出现(回扣暴击)**

结尾类型:

| 类型 | 效果 | 适合情绪 |
|------|------|----------|
| 余韵式 | 不说完,让读者自己想 | 意难平 |
| 呼应式 | 首尾呼应,形成闭环 | 治愈、成长 |
| 开放式 | 留下悬念 | 细思极恐 |
| 反转再反转 | 结尾再来一个小反转 | 震惊 |
| 金句式 | 一句话点题 | 共鸣 |

---

### Phase 3 完成门槛(进入 Phase 4 前必须通过)

- [ ] 总字数进入锁定的用户范围;未指定时进入 8000-20000 默认范围
- [ ] 每节 ≥ 800 字(爽文等高信息密度题材 ≥ 500 字,见 genre-writing-formulas.md)
- [ ] 节数 = 小节大纲规划节数(不得合并/省略)
- [ ] 身体部位同一词全文 ≤ 5 次
- [ ] 「像/好像/仿佛/如同」不成片堆叠;超过 10 处需逐处复核功能,不机械全删
- [ ] `node scripts/check-ai-patterns.js --check --fail-on=blocking 正文.md` 无 blocking 命中;其余提示先通读,确属问题再改
- [ ] `node scripts/check-degeneration.js --check 正文.md` 无 blocking 退化命中(复读/截断/工程词泄漏)

**不通过 → 回退补足,不得进入精修。**

---

### Phase 4:精修打磨

加载 `references/writing-workflow.md` 中的精修清单完成检查。
重点:开头钩子、情绪曲线、反转铺垫、每句话价值、格式规范、AI 腔。文件模式依次运行 `node scripts/check-ai-patterns.js --check --fail-on=blocking 正文.md`、`node scripts/check-outline-copy.js --outline 小节大纲.md 正文.md`、`node scripts/normalize-punctuation.js 正文.md`、`node scripts/check-degeneration.js --check 正文.md`。blocking 或确属细纲照搬先改正文再复扫;其他提示仅作读感复核,功能性写法可保留。

上述修改全部落盘后,运行 `node scripts/check-delivery-contract.js --json --min-chars {MIN} --max-chars {MAX} --sections {N} {短篇目录}`。exit 0 才可交付;exit 1 只按 `repair_scope` 最小修复并重跑受影响的质量检查与本命令,最多 2 轮;仍失败则报告检查 ID 并停止。exit 2、脚本缺失或不可执行时不得声称交付契约通过。本 verifier 只验用户字数、节数与排版形状,不替代正文质量判断。

#### Agent 调用:narrative-writer(去AI味)+ consistency-checker

精修阶段,如果项目已部署对应 agent,可 spawn:
- `Agent(subagent_type: "narrative-writer", prompt: "项目目录:{dir}\n任务描述:去AI味+格式检查\n检查范围:{正文文件}\n作者偏好:{query 命中的 prose_style/story_design 项}\n删除优先:每条 AI 味项先判能否删除——删后不丢伏笔/钩子/角色/情节/必要信息的直接删,会丢才润色(删除受比例上限与字数下限约束,跌破下限改降AI重写)\n必须检查:先否定再肯定的翻转句式,发现后直接改成后项或动作细节;检查像/好像/仿佛/如同等比喻是否成片堆叠,确属堆叠时只留最有功能的少数比喻,其余回到具体画面;检查是否连续使用头皮发紧/眼皮一跳/心口一沉/胃里翻涌等精致戏剧反应,能写普通动作/普通感觉就写普通动作/普通感觉;已有手机/聊天记录/公告/账单/病历/证据截图等信息,保留为角色看到或处理的场内载体,不改成叙述者解释;任务卡点只在角色本来有要办的事且能加重情绪/证据/关系/反转时使用,不为自然感补流程")` — 执行去AI味(7 Gate)和格式合规检查
- `Agent(subagent_type: "consistency-checker", prompt: "项目目录:{dir}\n检查范围:{正文文件}\n检查类型:事实冲突+伏笔断线+角色属性不一致")` — 执行一致性检查

如 agent 不可用,由主线程直接执行。

**正文洁净规则**:
- 自检(字数统计、禁用词扫描、格式检查)是过程动作,结果直接在对话里说明,不落盘成文件
- **绝对不能**把自检记录附加到正文文件末尾
- 正文中不得出现任何 `<!-- 自检 -->` 或类似的检查标记注释

不通过 → 回退补足。

---

## 流程衔接

**流水线:** 短篇
**位置:** 写作(第 3/3 步)

| 时机 | 跳转到 | 命令 |
|---|---|---|
| 有参考小说想对标 | story-short-analyze | `/story-short-analyze` → 输出存入 `拆文库/{书名}/` |
| 写完,去 AI 味 | story-deslop | `/story-deslop` |
| 想自检 | 本 skill 质量自检 | 用 Phase 4 自检流程 + `references/short-prose-quality.md` 逐项核对 |
| 需要市场方向 | story-short-scan | `/story-short-scan` |
| 设定太大,适合长篇 | story-long-write | `/story-long-write` |

---

## 参考资料

按需加载以下文件。写作时同时加载 ≤ 3 个:

| 文件 | 何时加载 |
|------|----------|
| [references/short-format.md](references/short-format.md) | 写作前必读(短篇正文格式,两平台模板) |
| [references/submission-craft.md](references/submission-craft.md) | 投稿前必读(平台基调 知乎/小程序/番茄 · 导语门面 · 付费点断点) |
| [references/short-craft.md](references/short-craft.md) | 写作全程参考(短篇通用底座:情绪直接写+后接具体反应、在场叙述、超短章节制) |
| [references/genre-styles/](references/genre-styles/) | **定方向后必读**:按题材加载对应风格包(追妻火葬场 / 世情打脸 / 复仇打脸 / 总裁豪门 / 宅斗宫斗 / 民俗怪谈 / 悬疑 / 甜宠 / 双男主 / 沙雕脑洞),正文风格随之切换 |
| [references/short-deslop.md](references/short-deslop.md) | 去AI味时必读(短篇专属,只杀真·AI腔,不杀情绪烈度) |
| [references/writing-workflow.md](references/writing-workflow.md) | Phase 2 设计任务 + Phase 4 精修 |
| [references/genre-writing-formulas.md](references/genre-writing-formulas.md) | 冷门题材结构骨架补充(核心 10 题材直接用 genre-styles/) |
| [references/genre-writing-techniques.md](references/genre-writing-techniques.md) | 跨题材通用技法(震惊场景/三翻四震/感情线四阶段/喜剧flag) |
| [references/emotional-methods.md](references/emotional-methods.md) | 设计情感时 |
| [references/hooks-chapter.md](references/hooks-chapter.md) | 章节钩子设计 |
| [references/short-suspense.md](references/short-suspense.md) | 短篇悬念设计 |
| [references/hooks-paragraph.md](references/hooks-paragraph.md) | 段落钩子技巧 |
| [references/villain-and-reveal.md](references/villain-and-reveal.md) | Phase 2 设计反派时 |
| [references/short-reversal.md](references/short-reversal.md) | 设计短篇反转时 |
| [references/short-prose-quality.md](references/short-prose-quality.md) | 精修检查时 |
| [references/banned-words.md](references/banned-words.md) | 禁用词表 |
| [scripts/normalize-punctuation.js](scripts/normalize-punctuation.js) | Phase 4 文件模式确定性标点收尾 |
| [scripts/check-ai-patterns.js](scripts/check-ai-patterns.js) | Phase 3 完成门槛与 Phase 4 复扫;报告高危 AI 句式、破折号、碎句号、长段落、微动作复读、套式反应细节、抽象总结、套词/比喻密度、解释链、系统公告腔、提纲感短段、低连接密度 |
| [scripts/check-degeneration.js](scripts/check-degeneration.js) | Phase 3 完成门槛与 Phase 4 复扫;报告模型退化(复读/截断/工程词泄漏),blocking 需重新生成 |
| [scripts/check-phase2-contract.js](scripts/check-phase2-contract.js) | Phase 2 产物确定性验收;返回具名失败与最小 repair_scope |
| [scripts/check-delivery-contract.js](scripts/check-delivery-contract.js) | 最终交付确定性验收;按用户参数检查非空白字符、节数与小节格式 |
| [references/dialogue-mastery.md](references/dialogue-mastery.md) | 写对话时 |
| [references/output-contract.md](references/output-contract.md) | Phase 2 对标上下文加载时(理解 analyze 产出格式与消费规范) |

### 按主题快速定位(横切主题)

有些主题散在多个文件里。下表给每个主题一个**权威文件**(先读它,通常够用),配套文件只在需要那个角度时再加载。括号是该文件里对应的小节。

| 主题 | 权威文件(先读) | 配套文件(按角度补充) |
|------|-----------------|----------------------|
| 情绪外化(怎么写情绪) | **`references/short-craft.md` 第2节**(情绪直接写+后接具体反应、三段对照、改写四步——替代旧机械替换表) | 各 `genre-styles/` 包的「情绪烈度与模式」 |
| 情绪设计(情感结构) | **`references/emotional-methods.md`**(情感三板斧 + 拉扯节奏 + 失败模式) | `references/genre-writing-techniques.md`(情绪操控核心法则 / 情绪三层次) |
| 反转 | **`references/short-reversal.md`**(反转类型 / 铺垫 / 揭示位置 / 有效性自检) | `references/villain-and-reveal.md`(真相揭露机制 / 反转有效性自检) |
| 反派揭露 | **`references/villain-and-reveal.md`**(反派模板 / 揭露机制 / 报应设计) | `references/short-reversal.md` |
| 人物 | **各 `genre-styles/{题材}.md` 的「对话风格」「招式库」**(受害者-复仇者主角声线、白月光软刀、施害者道德绑架人设,corpus-grounded) | `references/villain-and-reveal.md`(反派/揭露)· `references/genre-writing-techniques.md`(三层标签反差 / 人设从缺点开始)· `references/dialogue-mastery.md`(声线差异) |
| 钩子 | **`references/hooks-chapter.md`**(章节/开篇钩子类型) | `references/hooks-paragraph.md`(段落钩子)· `references/short-suspense.md`(悬念设计) |
| 女频写作 | **对应 `genre-styles/{题材}.md`**(追妻火葬场 / 总裁豪门 / 宅斗宫斗 / 甜宠 / 世情打脸的题材声线、虐爽比例、招式) | `references/genre-writing-techniques.md`(女频读者心理与写作技法 / 感情线四阶段推进法)· `references/emotional-methods.md`(情绪拉扯) |
| 题材风格 | **`references/genre-styles/{题材}.md`**(核心 10 题材的腔调/开篇/钩子/情绪烈度/招式/收尾,corpus-grounded) | `references/genre-writing-formulas.md`(冷门题材结构骨架)· `references/genre-writing-techniques.md`(核心梗 / 卖点 / 通用技法) |
| 开头 | **各 `genre-styles/{题材}.md` 的「开篇范式」**(关系锚 + 全弧剧透导语 + 火葬场预告,真实开篇范例)+ `short-craft.md` 第12节(开头事件密度) | `references/hooks-chapter.md`(开篇钩子类型)· `references/hooks-paragraph.md`(段钩密度) |
| 格式与节奏 | **`references/short-format.md`**(短篇正文格式,两平台模板) | `references/short-craft.md`(情绪直接写+后接具体反应/三维度揉进/疏密)· `references/writing-workflow.md`(设计/精修工作流) |
| 对话 | **`references/dialogue-mastery.md`**(对话技法主文件:差异化/潜台词/对话节奏) | `references/short-craft.md`(三类台词与对话权力博弈)· 各 `genre-styles/` 包的真实金句库 |
| 去AI味 | **`references/short-deslop.md`**(短篇专属:只杀真·AI腔,不杀情绪烈度/审判句/火葬场预告) | `references/banned-words.md`(禁用词扫描)· `scripts/check-ai-patterns.js`(AI句式复扫)· `references/short-prose-quality.md`(成稿检查) |

---

## 语言

- 跟随用户的语言回复,用户用什么语言就用什么语言回复
- 中文回复遵循《中文文案排版指北》