W13 旧用例承接补充:作者层路由与中文方法 Skill 的已跟踪文件改动
作者场景路由与指令、完善故事基础设定/合并故事方案/制定作品规划的技能中文化重构及其脚本、对应旧测试修改、SOT 导读、技能目录与实现回顾。此为 W13 阶段对已跟踪文件的改动,随批次按阶段整理补提交。
This commit is contained in:
parent
f7a57f68e6
commit
7f9114eca5
@ -16,5 +16,5 @@
|
||||
## 交付与交接
|
||||
|
||||
- 先给正文候选,再给必要且简短的检查状态和 `采用`、`修改`、`丢弃` 动作。
|
||||
- 候选始终是 Shadow。作者明确 `采用` 或 `丢弃` 后,才交给 [决定正文候选去留](../../../muse/content/work/skills/sovereignty/决定正文候选去留/SKILL.md);`修改` 进入 [改正文](改正文.md)。
|
||||
- 候选始终是 Shadow。作者明确 `采用` 或 `丢弃` 后,才交给 [决定正文候选去留](../../skills/操作/决定正文候选去留/SKILL.md);`修改` 进入 [改正文](改正文.md)。
|
||||
- 作者要求“只给正文”时,只交付正文,不附内部分析、路由或检查清单。
|
||||
|
||||
@ -2,18 +2,16 @@
|
||||
|
||||
## 进入条件
|
||||
|
||||
作者只有点子、素材、题材方向或多份未合并方案,还没有确认能支撑长篇的故事主心骨。先读取现有作品信息和本会话决定;只有缺口会改变候选方向时,才问一个关键问题。
|
||||
作者想从点子、片段、人物、关系、题材方向或已有设计继续构思作品。先读取当前作品、相关草稿与本会话决定;已有明确内容直接沿用,不要求先确定全书中心。
|
||||
|
||||
## 正式技能
|
||||
|
||||
- 点子还只是陈述时,用 [提炼故事概念](../../skills/规划/提炼故事概念/SKILL.md) 找到能持续制造冲突的核心疑问。
|
||||
- 需要形成可比较的完整前期方案时,用 [完善故事基础设定](../../../muse/lifecycle/flow/skills/book/完善故事基础设定/SKILL.md)。
|
||||
- 作者已经有多份完整方案并明确要求综合时,才用 [合并故事方案](../../../muse/lifecycle/flow/skills/book/合并故事方案/SKILL.md)。
|
||||
正式入口是 [完善故事基础设定](../../skills/操作/完善故事基础设定/SKILL.md):私人探索稿的保存、读回、最终格式检查与明确选定都走新版作者入口,不把探索过程写成正式内容。素材、联系、中心、结构可以往返发展;不把概念四问、九章完整度或候选数量作为开始探索的条件。
|
||||
|
||||
一次只设一个主入口。方法技能可以辅助判断,不能代替规划角色产出候选。
|
||||
主会话负责对话与派发,规划角色发展内容。
|
||||
|
||||
## 交付与交接
|
||||
|
||||
- 先给可比较的故事候选和关键差异,再说明哪些内容使用了假设。
|
||||
- 不替作者选故事主心骨,不把候选当作已确认规划。
|
||||
- 作者选择 `采用` 后,带着被选方案进入 [排故事](排故事.md);选择 `修改` 时只改点名部分;选择 `丢弃` 时不保留为当前方向。
|
||||
- 每轮先给本次发展的内容,只追问最影响继续的一项;日常探索不套整书模板、不运行代码校验。
|
||||
- 日常探索稿由新版入口保存;整理交付时才检查最终格式。方法子阶段在方法技能迁移前可参考旧版 [完善故事基础设定](../../../muse/lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) 主文档;方法参考不改变新版入口的存储与选定合同。
|
||||
- 作者采纳一个点子只更新该处。作者明确选择当前方向并要求正式规划时,按主文档的「整理交付」进入 [排故事](排故事.md);具体 setting 仍需另一次确认。
|
||||
|
||||
@ -8,9 +8,10 @@
|
||||
|
||||
- 全书或分卷规划的主入口是 [制定作品规划](../../../muse/lifecycle/flow/skills/book/制定作品规划/SKILL.md)。按实际短板选用 [搭建故事蓝图](../../skills/规划/搭建故事蓝图/SKILL.md)、[搭建故事结构](../../skills/规划/搭建故事结构/SKILL.md)、[增强叙事动力](../../skills/规划/增强叙事动力/SKILL.md)、[铺设并回收伏笔](../../skills/规划/铺设并回收伏笔/SKILL.md) 或 [设计故事结尾](../../skills/规划/设计故事结尾/SKILL.md),不把五种方法一次全塞进任务。
|
||||
- 下一章细纲的主入口是 [规划下一章](../../../muse/lifecycle/flow/skills/chapter/规划下一章/SKILL.md)。书级规划和章级细纲不能由同一次调用混写。
|
||||
- 新版规划候选的读取、编辑、审阅与作者明确决定走 [确认规划候选](../../skills/操作/确认规划候选/SKILL.md);旧入口只维护旧库记录。
|
||||
|
||||
## 交付与交接
|
||||
|
||||
- 交付可执行的规划草稿,明确已确认事实、待作者决定项和必要假设,不用正文段落充当规划。
|
||||
- 规划先保持 Shadow;作者明确确认后,才能成为下层规划或正文写作依据。
|
||||
- 规划先保持 Shadow;作者明确确认后,才能成为下层规划或正文写作依据。确认经 [确认规划候选](../../skills/操作/确认规划候选/SKILL.md) 执行,主会话不代替作者采纳。
|
||||
- 单章细纲齐备并被确认后,交给 [写下一章](写下一章.md)。
|
||||
|
||||
@ -20,5 +20,5 @@
|
||||
## 交付与交接
|
||||
|
||||
- 只处理作者指定范围,范围外正文保持原样;不顺手改规划、设定或其它段落。
|
||||
- 修改结果仍是 Shadow 候选。作者选择 `采用` 或 `丢弃` 后,交给 [决定正文候选去留](../../../muse/content/work/skills/sovereignty/决定正文候选去留/SKILL.md);继续修改时仍留在本场景。
|
||||
- 修改结果仍是 Shadow 候选。作者选择 `采用` 或 `丢弃` 后,交给 [决定正文候选去留](../../skills/操作/决定正文候选去留/SKILL.md);继续修改时仍留在本场景。
|
||||
- 作者要求“只给正文”时,只交付修改后的正文。
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 定故事 | [定故事.md](定故事.md) | 把点子、素材或模糊方向变成可比较的长篇方案 | 作者还没有确认故事主心骨时 | 只交候选,不替作者选定方向 |
|
||||
| 定故事 | [定故事.md](定故事.md) | 通过流程主文档和子阶段提示词逐步发展素材、中心与结构 | 作者想从点子、片段或已有设计继续构思时 | 按需往返探索,整理交付时才校验格式,保留方向与正式设定两次确认 |
|
||||
| 排故事 | [排故事.md](排故事.md) | 处理全书、分卷、结构、节拍、伏笔、结尾和单章细纲 | 故事方向已明确,需要可执行规划时 | 规划保持草稿态,确认后才能供正文使用 |
|
||||
| 塑人物 | [塑人物.md](塑人物.md) | 处理人物动机、关系、声音与正文呈现 | 作者要创建或补强人物时 | 不替作者决定人物命运、主题立场或重大关系转折 |
|
||||
| 写下一章 | [写下一章.md](写下一章.md) | 按正式事实和已确认细纲生成整章正文候选 | 目标章写作输入齐备时 | 同时读取原生中文写作合同;候选不得直接入正典 |
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
## 进入顺序
|
||||
|
||||
1. 先从当前作品、已确认规划和本会话决策中取事实,不让作者重复提供已经能查到的信息。
|
||||
2. 根据作者这轮的主要结果选择一个场景。一个请求横跨多步时,先完成最前面的阻断步骤,再明确交接下一步。
|
||||
2. 根据作者这轮的主要结果选择一个场景。正式产物按已有依赖交接;定故事的探索过程按主文档选用子阶段,可以往返,不按代码状态逐步通关。
|
||||
3. 路由确定后,只读命中的场景文件。生成或修改正文时,同时读 [原生中文写作合同](原生中文写作.md)。
|
||||
4. 在方法目录或编排目录中读取场景指定的正式技能,再按功能链派发合同指定角色。作者层和主会话不代替角色创作、检测或评分;正式技能名只用于内部执行,普通作者回复不列技能清单。
|
||||
5. 先交付作者要的内容,再用一句话说明候选状态或下一项决定。
|
||||
@ -14,7 +14,7 @@
|
||||
|
||||
| 作者常见说法 | 进入场景 | 本轮目标 |
|
||||
|---|---|---|
|
||||
| 我只有一个点子 | [定故事](场景/定故事.md) | 把模糊方向变成可比较的故事候选 |
|
||||
| 我只有一个点子 | [定故事](场景/定故事.md) | 从已有素材逐步发展作品,按需试探中心与结构 |
|
||||
| 帮我排一下整本书 | [排故事](场景/排故事.md) | 做全书、分卷、结构或下一章规划 |
|
||||
| 这个人物还站不住 | [塑人物](场景/塑人物.md) | 补人物动机、关系、声音或呈现方式 |
|
||||
| 接着写下一章 | [写下一章](场景/写下一章.md) | 根据当前正式事实与已确认细纲生成整章候选 |
|
||||
@ -27,11 +27,11 @@
|
||||
|
||||
- 缺少会改变结果的决定时,一轮只问一个关键问题;非关键缺口使用明确假设,并在交付后指出。
|
||||
- 普通回复不展示运行编号、哈希、模型参数、数据库字段、内部英文标识或门禁实现。
|
||||
- 作者没有明确确认时,正文、规划和知识产物保持 Shadow,不进入 Canonical。
|
||||
- 候选内容统一给出 `采用`、`修改`、`丢弃` 三个动作。具体写入由正式主权技能执行,路由层不写库。
|
||||
- 作者没有明确确认时,正式正文、规划和知识候选保持 Shadow,不进入 Canonical;定故事探索稿是本地草稿,尚不属于 Shadow。
|
||||
- 正式候选给出 `采用`、`修改`、`丢弃` 三个动作。探索中的自然反馈只作用于作者点名的内容,不把局部采纳升级为方向或正式规划确认。具体写入由正式主权技能执行,路由层不写库。
|
||||
- 诊断请求默认只查不改;修改请求只处理作者授权的范围。
|
||||
- 作者说“只给正文”时,只给正文,不附分析、路由、检查清单或状态说明。
|
||||
|
||||
## 最小作者旅程
|
||||
|
||||
自然中文点子先进入 [定故事](场景/定故事.md),方向确认后进入 [排故事](场景/排故事.md),细纲确认后进入 [写下一章](场景/写下一章.md)。正文候选可先由 [查作品](场景/查作品.md) 定位问题,再由 [改正文](场景/改正文.md) 处理指定范围。作者最终选择 `采用` 或 `丢弃` 时,交给 [决定正文候选去留](../../muse/content/work/skills/sovereignty/决定正文候选去留/SKILL.md) 执行;选择 `修改` 时仍留在 Shadow。
|
||||
自然中文点子先进入 [定故事](场景/定故事.md),方向确认后进入 [排故事](场景/排故事.md),细纲确认后进入 [写下一章](场景/写下一章.md)。正文候选可先由 [查作品](场景/查作品.md) 定位问题,再由 [改正文](场景/改正文.md) 处理指定范围。作者最终选择 `采用` 或 `丢弃` 时,交给 [决定正文候选去留](../skills/操作/决定正文候选去留/SKILL.md) 执行;选择 `修改` 时仍留在 Shadow。
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 作者路由指令 | [指令.md](指令.md) | 把作者自然中文请求分到一个创作场景,并约束提问、结果说明和决策动作 | 收到作者创作请求时 | 先读本文件;一次只进入一个主场景,不向作者展示内部路由过程 |
|
||||
| 作者路由指令 | [指令.md](指令.md) | 把自然中文请求分到创作场景,区分自由探索、局部采纳与正式确认 | 收到作者创作请求时 | 先读本文件;一次只进入一个主场景,不向作者展示内部路由过程 |
|
||||
| 原生中文写作合同 | [原生中文写作.md](原生中文写作.md) | 生成和修改正文共同遵守的中文语序、声音、事实与范围边界 | 进入写下一章或改正文场景时 | 与命中的场景文件一并读取,不替代具体技能合同 |
|
||||
| 作者场景 | [目录.md](场景/目录.md) | 定故事、排故事、塑人物、写下一章、改正文和查作品六个场景入口 | 路由指令已识别作者主意图后 | 只打开命中的场景;场景只负责分流和交接 |
|
||||
| 作者场景 | [目录.md](场景/目录.md) | 含提示词驱动的定故事,以及排故事、塑人物、续写、修改和诊断入口 | 路由指令已识别作者主意图后 | 只打开命中的场景;场景只负责分流和交接 |
|
||||
|
||||
@ -42,6 +42,8 @@ PostgreSQL `muse-example` 的 `example_skill` 使用单事务原位更新,没
|
||||
|
||||
角色合同仍为 `muse-role-v4 / fixed-opus-v1`:writer、planner、judge 固定 Opus,extractor、detector 只有在 profile 明确登记时可走内容治理链。M3 测试产生的持久化角色策略、默认配置和本机 provider 已撤回;以后测试 M3 只通过单次命令或测试夹具显式传入。仓内五章授权快照也已过期,完整回放仍须先刷新固定 Opus successful probe,再由 `准备正文回放数据` 装配新授权配置。
|
||||
|
||||
撤回后按不放行外部依赖的完整离线清单复验:128 个登记入口中 119 个通过,9 个因 PostgreSQL、模型或凭据依赖明确阻断,实际失败 0;未再次调用 M3 或 Opus。
|
||||
|
||||
## 5. 清理边界
|
||||
|
||||
本回顾、`chinese_only`、永久退役名称门和正常数据库对账已经覆盖一次性迁移能力。英文到中文名称迁移器、机器映射、数据库迁移命令及其专用测试均已删除,仓库内相关程序符号与文件路径引用为 0。
|
||||
|
||||
@ -6,7 +6,7 @@ disable-model-invocation: true
|
||||
|
||||
# 规划(scenario: planning | purpose: planning | 槽位: 规划→planner)
|
||||
|
||||
何时用:立项产全套设定包与大纲;续作期按用户意图修订规划。产出全为草稿(未确认规划不进生成上下文)。
|
||||
何时用:按当前阶段生成指定类型的规划,或按用户意图修订规划。阶段 0 交接先生成 setting,不因此一次产出全部设定包与大纲。产出全为草稿,未确认规划不进生成上下文。
|
||||
|
||||
## 元数据驱动(本功能的核心机制)
|
||||
|
||||
@ -56,7 +56,7 @@ disable-model-invocation: true
|
||||
## 红线
|
||||
|
||||
不写正文;修订场景只动用户点名的规划面,并列出波及(哪些细纲/卡受影响)。
|
||||
从前期设计进入时,须先通过 `完善故事基础设定/scripts/assert_selection_handoff.py`;不得从候选目录偷读未选方案当 setting。
|
||||
从前期设计进入时,先按 [整理交付](../完善故事基础设定/references/整理交付.md) 接收作者明确选择的当前稿、约束与待决问题;最终选定记录须通过 `完善故事基础设定/scripts/assert_selection_handoff.py` 格式检查。该脚本不证明用户授权,也未接入 `persist_planning` 自动拦截,主会话必须核对真实用户回复。不得读未选方案冒充已定事实;不能用“待定”假装补齐必要字段。所生成 setting Shadow 须展示给用户另行确认。
|
||||
|
||||
## 失败关闭
|
||||
|
||||
|
||||
@ -1,60 +1,32 @@
|
||||
---
|
||||
name: 合并故事方案
|
||||
description: 把多份已完成的前期设计候选逐章串行统合成一份新候选,一级章节一个 fresh 代理,权威顺序固定为根设定优先。用户明确要求综合多案时使用;不生成候选、不做机械门校验、不改前期设计 SoT。
|
||||
description: 作者明确要求结合几种已有故事设想时,按指定范围比较兼容性、解释取舍并形成合并建议。可合并局部片段或整稿,产物留在 .agents.local/草稿/;多轮探索归 完善故事基础设定,正式规划落库归 制定作品规划。
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 逐章串行统合前期设计候选
|
||||
# 合并故事方案
|
||||
|
||||
何时用:`完善故事基础设定` 已产出多份通过机械门的候选,用户明确要求"把这几案统合成一份"。用户直接指定主案时不用本 Skill。
|
||||
## 用途与输入
|
||||
|
||||
候选的生成、冻结三级树与机械门校验归 `完善故事基础设定`;本 Skill 只做统合,产物仍是待选候选。
|
||||
作者指定的来源材料、合并范围、已有偏好、当前探索稿与已确认约束。只有明确要求结合不同设想时使用;不要求来源先写成完整九章候选。
|
||||
|
||||
## 依赖边界
|
||||
按 [合并指令](references/合并指令.md) 组织本轮 planner 任务。角色、模型、工具权限和运行留痕沿用既有派发合同;不按章节创建固定代理流水线。
|
||||
|
||||
统合的每章任务包引用 `完善故事基础设定` 的冻结三级树与 [章节内容规范](../完善故事基础设定/references/chapter-content-spec.md);统合完成后的机械门仍由 `完善故事基础设定/scripts/validate_candidates.py` 执行,本 Skill 不复制第二套校验。
|
||||
## 推进与输出
|
||||
|
||||
完整节点合同见 [串行统合合同](references/serial-merge-contract.md)。
|
||||
先比较需要合并的局部,再形成可读的合并建议,并说明保留、舍弃和新增连接的理由。出现影响方向的冲突时交由作者取舍;已经获得的合并授权不重复询问,授权未覆盖的改变明确提出。
|
||||
|
||||
## 输入
|
||||
默认把建议保留在 `.agents.local/草稿/<作品>/`。落盘优先级:用户指定路径 > 仓库各 `目录.md` 中已登记的约定 > 本 Skill 默认路径。确认采用后,按授权范围更新当前探索稿。
|
||||
|
||||
```bash
|
||||
# ① 为当前一级章节构建输入包(最新根设定 + 已统合前文 + 各来源候选的本章)
|
||||
.venv/bin/python muse/lifecycle/flow/skills/book/合并故事方案/scripts/serial_merge.py packet \
|
||||
--heading "<冻结的一级章标题>" --output /private/tmp/merge-packet-01.md <来源候选>...
|
||||
|
||||
# ② 由主代理派发 planner 子代理
|
||||
# 主代理把 planner 身份提示与 muse/sot/角色合同.md 对应章节作为角色 prompt,输入仅包含
|
||||
# serial-merge-contract.md 与当前 merge-packet-01.md;子代理无工具、fresh 会话,
|
||||
# 输出写到 /private/tmp/merge-raw-01.md,并由 记录运行证据 保存派发回执。
|
||||
|
||||
# ③ 校验并接纳本章草案
|
||||
.venv/bin/python muse/lifecycle/flow/skills/book/合并故事方案/scripts/serial_merge.py accept ...
|
||||
```
|
||||
|
||||
每章一个 fresh 高推理代理;不复用上一节点会话,不把来源候选的后续章节提前交给当前代理。
|
||||
|
||||
## 输出
|
||||
|
||||
每章产出 `<decision>`(主轴、主要取舍、发现的前文冲突及处理)与 `<chapter>`(可直接审定的章节草案)两个区块。主会话审定落盘后,该章成为下一节点的"已统合前文"。全部章节完成后得到一份新的统合候选文件。
|
||||
需要形成完整交付稿时,返回 [完善故事基础设定](../完善故事基础设定/SKILL.md) 的「整理交付」,统一使用其 [产出范式](../完善故事基础设定/references/candidate-contract.md) 和最终格式校验。局部合并过程不运行校验脚本。
|
||||
|
||||
## 数据边界
|
||||
|
||||
- 允许读取:最新根设定、已统合前文、来源候选的当前章节。
|
||||
- 允许写入:统合候选文件与 `/private/tmp` 下的任务包、原始输出。
|
||||
- 数据库:不读不写任何表。
|
||||
- Git:不执行 `add`、`commit` 或删除候选。
|
||||
|
||||
## 红线
|
||||
|
||||
- 权威顺序固定:最新根设定 > 已统合前文 > 当前来源章节 > 最小补缝新增。来源候选没有投票权,不按票数取多数。
|
||||
- 后章不得推翻前章已审定的因果、人物关系、数值、专名和披露节点。
|
||||
- 不叠加多案互斥的卖点;故事只能有一条主发动机。
|
||||
- 某章校验或语义复核失败时停在当前章,不得让后续代理基于未审定草案继续。
|
||||
- 统合结果仍是待选候选,不自动进入唯一前期设计 SoT,也不自动交给 `制定作品规划`。
|
||||
- 本 Skill 不提供模型 CLI 子命令。主代理必须按 07 领域派发合同启动 planner 子代理;不得把宿主命令、模型名或凭据固化进脚本。
|
||||
- 用户最新明确决定优先;不能按来源数量投票,也不把所有互斥卖点叠加。
|
||||
- 可以保留尚未解决的部分与多个中心设想。合并范围由作者的实际问题决定,不强制逐章、固定顺序或统一发动机。
|
||||
- 涉及已确认约束时说明影响,不能静默改写。源稿保留,不自动删除。
|
||||
- 不写正式正文、不写数据库、不执行 Git 提交。合并建议与局部采纳都不替代方向选择及正式 setting 确认。
|
||||
|
||||
## 复利合同
|
||||
|
||||
- **消费**:多路候选方案与用户选定约束。
|
||||
- **回写**:统合本身不落 `example_lesson`;选定方案进入 `制定作品规划` / `决定正文候选去留` 后由规划或确认链登记效果信号。
|
||||
消费来源材料和作者取舍;在探索稿保留有效选择的理由。合并阶段不写 `example_lesson`,正式规划或正文确认链负责后续效果登记。
|
||||
|
||||
@ -1,70 +1,63 @@
|
||||
---
|
||||
name: 完善故事基础设定
|
||||
description: 在正式规划前固化作品根设定,按固定三级章节树生成可比较的前期设计候选。用户仍在单文档前期设计阶段时使用;不写正文、不落库、不替用户定稿。不适用于合并多案(归 合并故事方案)或正式规划落库(归 制定作品规划)。
|
||||
description: 作者从点子、片段或已有设计继续构思作品时,用提示词引导多轮探索,在 .agents.local/草稿/ 逐步形成作品探索稿。按需发展素材、联系、中心与结构,整理交付时才校验格式;指定的多案合并归 合并故事方案,正式规划落库归 制定作品规划。
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# 作品设定初始化
|
||||
# 完善故事基础设定
|
||||
|
||||
## 唯一目的
|
||||
## 流程主文档
|
||||
|
||||
把零散对话收束成根设定与若干独立候选,让用户比较后再进 `制定作品规划`。不写正文、不落 PostgreSQL、不替用户定稿。
|
||||
帮助作者把散乱的想法逐步发展成作品。边缘素材与中心设想相互影响,结构从已有内容中长出来;中心可以暂定,也可以在正文写作后调整。
|
||||
|
||||
## 元数据驱动:输入与权威顺序
|
||||
本文件统一管理阶段 0 的探索指令。下表是可往返的子阶段,不是必须顺序执行的流水线。每轮只加载当前需要的一份子文档;九章清单用于找方向和整理产物,不要求一次填完。
|
||||
|
||||
先冻结 `完善故事基础设定/v2` 任务包(作品名、SoT 路径、候选数、根设定、对话记录、节奏要求、参考书证据、候选路径)。冲突顺序:用户最新明确要求 > 根设定已定事实 > 已接受方案 > 待定建议。
|
||||
## 输入与已有事实
|
||||
|
||||
字段与完成判定见 [候选生成合同](references/candidate-contract.md);三级内容见 [章节内容规范](references/chapter-content-spec.md)、[一至五章](references/chapter-content-spec-chapters-1-5.md)、[六至九章](references/chapter-content-spec-chapters-6-9.md)。冻结后派发;新要求先改任务包,旧候选作废。
|
||||
作品身份、作者本轮想推进的内容、已有探索稿和本会话决定。进入阶段 0 前已有作品行;缺少作品身份时由作者入口交给作品管理能力处理,本 Skill 不建表或写库。
|
||||
|
||||
## 根设定与候选章节树
|
||||
先读相关内容再提问。用户最新明确要求优先;涉及已确认正式事实的改变,说明影响并留作修订建议。作者说过的决定不重复问,未作决定的内容保留为设想或待决。
|
||||
|
||||
根设定 = 上游硬约束(不占九章);三级候选树 = 本 Skill 主输出。根设定只写稳定事实,不写过程/工具/方案来源;每点 30–60 字;歧义进「待决问题」。用户确认的稳定事实才回写根设定。
|
||||
## 子阶段调度
|
||||
|
||||
## 固定三级章节树与节奏
|
||||
| 当前需要 | 读取子文档 | 本轮留下什么 |
|
||||
|---|---|---|
|
||||
| 只有零散点子,或只有题材、感受与写法偏好 | [收集素材](references/收集素材.md) | 原始偏好、已有素材与 AI 提出的少量设想 |
|
||||
| 素材之间出现情绪、意象、气氛上的呼应,或产生冲突与因果 | [发展联系](references/发展联系.md) | 素材间的呼应、关系与可能后果,保留尚未说清的感觉 |
|
||||
| 想试探“这部作品在围绕什么生长” | [试探中心](references/试探中心.md) | 能解释部分素材、仍可改动的中心设想 |
|
||||
| 已有内容需要归位,或准备展开某一设计面 | [组织结构](references/组织结构.md) | 九章中的局部结构与清楚的缺口 |
|
||||
| 同一问题有互斥走法,需要比较或合并 | [比较分歧](references/比较分歧.md) | 小范围差异、代价与作者取舍 |
|
||||
| 作者要回看当前成果、选择方向或交给正式规划 | [整理交付](references/整理交付.md) | 按固定范式整理的当前作品探索稿 |
|
||||
|
||||
九章三级树与层级硬规则见章节内容规范。设定从追读节奏反推:前三章立处境与钩子、前十章局部闭环、前五十章阶段高潮、全书逐级扩舞台;区分世界事实 / 角色知情 / 读者披露。
|
||||
作者指定要谈什么,就从那里进入。没有指定时,围绕已有材料提出一个具体推进点;不因为清单里有空栏就改问卷。
|
||||
|
||||
## 参考书与冻结合同
|
||||
## 每轮协作
|
||||
|
||||
参考书先读窗级大纲再校验前五十章正文;只借方法不照搬专名骨架。派发前冻结三级标题与号段(见候选生成合同);同构只为比较。
|
||||
1. 主会话确认本轮范围,将相关原文、用户决定、本主文档与当前子文档交给 planner;角色、模型与运行留痕沿用既有派发合同。
|
||||
2. 规划角色先发展内容,再指出其中最影响下一步的疑问。需要作者决定时,一轮只问一个关键问题,并说明它会改变什么。
|
||||
3. 按作者反馈补写当前探索稿,只整理本轮涉及部分。用户原话、作者接受的内容、模型设想与暂缓问题用自然文字分清。
|
||||
4. 简短说明这轮留下了什么;下一轮可以继续、转向、退回或暂停。暂缓、矛盾和推翻旧设想都不触发程序失败。
|
||||
|
||||
## 独立生成
|
||||
“这个点子保留”只采纳该点子;“按这个方向继续”只选择当前方向;确认正式 setting 针对之后展示的具体规划产物。三者不能互相代替。
|
||||
|
||||
每候选一个独立 planner 子代理,只读共同输入与自己的文件;会话和工具授权隔离。调用走 `执行角色任务`,回执由 `记录运行证据` 保存。超时从最后一个完整单元续写,禁止空话补篇幅。
|
||||
## 元数据驱动:设计导航与产出范式
|
||||
|
||||
## 复核与机械门
|
||||
- 需要查某一设计面的内容时,读 [章节内容规范](references/chapter-content-spec.md),再按需读 [一至五章](references/chapter-content-spec-chapters-1-5.md) 或 [六至九章](references/chapter-content-spec-chapters-6-9.md)。
|
||||
- 整理交付时才读 [作品探索稿产出范式](references/candidate-contract.md)。日常素材和局部方案不套完整模板、不运行格式校验。
|
||||
- 人物动作、关系与声音使用 [呈现人物](../../../../../../.agent/skills/写作/呈现人物/SKILL.md);试探冲突使用 [提炼故事概念](../../../../../../.agent/skills/规划/提炼故事概念/SKILL.md);已有材料需要因果安排时使用 [搭建故事蓝图](../../../../../../.agent/skills/规划/搭建故事蓝图/SKILL.md)。方法按问题选用,其完整工作表不作为本阶段前置要求。
|
||||
|
||||
表达层复核(可用 `humanizer-zh`)后重跑机械门与语义复核清单。
|
||||
## 输出与落盘
|
||||
|
||||
```bash
|
||||
.venv/bin/python muse/lifecycle/flow/skills/book/完善故事基础设定/scripts/validate_candidates.py \
|
||||
--root-doc docs/design/<作品>-前期设计.md --min-candidates 5 docs/design/candidates/*.md
|
||||
```
|
||||
默认持续维护 `.agents.local/草稿/<作品>/作品探索.md`;局部备选只有需要单独比较时才另存。首次有内容时再建文件,不预建九章空表,不自动生成多份整书候选。
|
||||
|
||||
机械门查标题同构、号段、占位符、根设定句长;内容完整靠语义复核。失败码:`SETTING_INIT_VALIDATION_FAILED`。
|
||||
落盘优先级:用户指定路径 > 仓库各 `目录.md` 中已登记的约定 > 本 Skill 默认路径。探索稿属于私有草稿;项目规则与设计的修改遵守三树及父子索引契约。
|
||||
|
||||
## 用户选择与交接
|
||||
## 边界与最终交接
|
||||
|
||||
比较报告不自动合并。统合交给 `合并故事方案`;用户确认方向后整理回 SoT。
|
||||
本阶段不写正式正文、不写数据库、不自动提交 Git。探索过程由指令和作者反馈推进;代码只在最终交付时核对产物格式与选定记录格式,不决定题材、中心、质量或下一子阶段。
|
||||
|
||||
进入 `制定作品规划` 前必须有用户选定记录,并用机械门校验:
|
||||
|
||||
```bash
|
||||
.venv/bin/python muse/lifecycle/flow/skills/book/完善故事基础设定/scripts/assert_selection_handoff.py \
|
||||
docs/design/<作品>-selection.json
|
||||
```
|
||||
|
||||
选定 JSON 必含 `selectedCandidate` / `confirmedByUser` / `confirmedAt`。未通过交接闸不得把未选候选当正式设定写入规划。
|
||||
|
||||
## 数据与失败边界
|
||||
|
||||
只读写用户点名的前期设计 SoT 与临时候选目录;不碰正文/正式规划/知识卡/任何表;无授权不 Git 提交。输入不全、合同漂移、候选污染或机械门失败时稳定失败关闭。
|
||||
|
||||
## 输出
|
||||
|
||||
可比较的候选 Markdown + 比较报告;不写 Canonical。
|
||||
作者明确选择当前方向后,按「整理交付」记录所选稿,再交给 [制定作品规划](../制定作品规划/SKILL.md) 生成 setting Shadow。作者确认这份具体 setting 后才转 confirmed;本地探索稿与方向选择都不产生 Canonical。
|
||||
|
||||
## 复利合同
|
||||
|
||||
- **消费**:用户对话与参考书方法证据,指导后续 `制定作品规划` 正式规划。
|
||||
- **回写**:本阶段不落 `example_lesson`;选定方案进入规划链后由 `制定作品规划` / `决定正文候选去留` 登记效果信号。
|
||||
消费已有写作方法与参考材料,只借观察、人物、因果等方法,不照搬参考作品设定。本阶段保留有效取舍及其原因;进入正式规划或正文确认链后,由对应 Skill 登记效果信号,本阶段不写 `example_lesson`。
|
||||
|
||||
@ -1,149 +1,49 @@
|
||||
# 候选生成合同(完善故事基础设定/v2)
|
||||
# 作品探索稿产出范式
|
||||
|
||||
本合同在派发作品设定候选前读取。它定义共同输入、候选结构、层级编号和完成判定,不保存任何具体作品设定。各章节“应该写什么”的内容合同见 [章节内容规范](chapter-content-spec.md)。
|
||||
本文件只定义阶段 0 整理交付时的 Markdown 形状。交互推进由 [流程主文档](../SKILL.md) 及其子文档负责;日常片段、局部比较和未整理草稿不受本格式约束。
|
||||
|
||||
## 一、冻结任务包
|
||||
## 固定目录,按需展开
|
||||
|
||||
主会话先组装任务包,再复制给全部候选子代理。只有 `candidateId` 和 `outputPath` 可以因候选不同而变化。
|
||||
|
||||
| 字段 | 内容 |
|
||||
|---|---|
|
||||
| `contractVersion` | 固定为 `完善故事基础设定/v2` |
|
||||
| `workTitle` | 当前作品名 |
|
||||
| `sotPath` | 唯一前期设计文档路径 |
|
||||
| `rootSetting` | 根设定全文,不用摘要替代 |
|
||||
| `conversationRecord` | 与作品有关的用户原话,按时间排序 |
|
||||
| `decisionLedger` | 每项标为已接受、已否决、待定或被新要求覆盖 |
|
||||
| `unresolvedQuestions` | 会实质改变故事方向、仍需用户拍板的问题 |
|
||||
| `retentionMilestones` | 前三章、前十章、前五十章及后续阶段落点 |
|
||||
| `disclosureRules` | 世界事实、角色知情、读者披露三线分界 |
|
||||
| `phaseRequirements` | 用户指定的阶段模型;为空时由候选自行划分并写明边界依据 |
|
||||
| `referenceEvidence` | 大纲直证、正文归纳、可借鉴、不可照搬四栏 |
|
||||
| `sectionContract` | 固定九章三级树(引用 chapter-content-spec),含各一级章节预留号段 |
|
||||
| `thresholdOverrides` | 用户显式指定的数量或字数门槛;机械门不查数量,为空时不设任何数量要求 |
|
||||
| `candidateId` | 当前子代理唯一编号 |
|
||||
| `outputPath` | 当前子代理唯一输出文件 |
|
||||
|
||||
对话不能只给总结。原话用于防止整理时改义,`decisionLedger` 用来阻断已经否决的旧方案复活。出现矛盾时,子代理不得自行选择;它只按任务包里已写明的优先级执行,并把真正未决项留给用户。
|
||||
|
||||
## 二、统一文档形状
|
||||
|
||||
每份候选只允许一个一级标题。`## 目录`可选,不参与号段和判定。正文使用固定的二级、三级标题,顺序逐字一致:
|
||||
一份稿只用一个 `# 作品名|作品探索` 标题。正文依次包含 `## 根设定`、下列九章、`## 待决与取舍`;可在根设定之前增加 `## 目录`。
|
||||
|
||||
```markdown
|
||||
# 候选标题
|
||||
# 作品名|作品探索
|
||||
|
||||
## 根设定
|
||||
作者已明确的约束。尚无约束时如实说明。
|
||||
|
||||
## 一、作品定位与核心命题
|
||||
## 二、故事世界与社会系统
|
||||
## 三、角色与关系
|
||||
## 四、故事机制与关键实体
|
||||
## 五、阶段与状态变化
|
||||
## 六、情节架构与全书大纲
|
||||
## 七、主题与读者体验弧线
|
||||
## 八、信息、悬念与伏笔管理
|
||||
## 九、叙事执行与一致性
|
||||
|
||||
<!-- S001-S050 -->
|
||||
|
||||
### 1.1 作品形态与定位
|
||||
|
||||
> 回答:这是什么样的作品、写给谁、以什么形态连载、与同类有何差别。
|
||||
> 应写:题材标签、载体与读者、篇幅与连载结构、差异化定位。
|
||||
> 不写:故事内部设定、情节、角色。
|
||||
> 关联:1.2、1.4、6.2。
|
||||
|
||||
### 1.1.1 题材、载体与目标读者
|
||||
|
||||
> 回答:题材元素、发布载体与目标读者分别是什么。
|
||||
> 应写:题材标签、载体与阅读场景、读者群体与其期待。
|
||||
> 不写:篇幅结构归 1.1.2,差异比较归 1.1.3。
|
||||
> 关联:1.1.2、1.1.3。
|
||||
|
||||
- **S001|设定名称:** 设定正文。
|
||||
|
||||
### 1.1.2 篇幅、连载方式与结构规模
|
||||
|
||||
> 不适用:本作品尚未确定连载形态,待用户确认后补充。
|
||||
## 待决与取舍
|
||||
保留的零散素材、未决分歧、已有取舍及下一步关注点;没有时明确说明。
|
||||
```
|
||||
|
||||
规则:
|
||||
以上仅列目录;实际交付各节必须有正文或明确说明,例如:
|
||||
|
||||
1. 四字段说明中“回答、应写、关联”必填,“不写”用于标出最常见的越界写法;内容以 chapter-content-spec 为准,子代理不得自行改写合同语义。
|
||||
2. 每个三级章节要么至少一条 `S` 条目,要么一条“不适用及原因”标注;两者不得同时缺省。
|
||||
3. 一项 `S` 可以写多段并包含表格,但必须围绕同一内容单元;阶段状态矩阵、伏笔登记、时间线核对允许以表格为主体。
|
||||
> 未探索:目前只有兄妹关系,尚未讨论社会制度。
|
||||
|
||||
## 三、层级与编号规则
|
||||
> 待定:暂不决定是否设置超自然力量,后续先发展人物处境。
|
||||
|
||||
```text
|
||||
一级标题:一个创作设计领域
|
||||
二级标题:这个领域要回答的一组问题
|
||||
三级标题:可以独立填写、审查和引用的内容单元
|
||||
S 条目:三级标题下的具体设定、规则、实体、关系、阶段状态或事件
|
||||
```
|
||||
章节内可以直接写概述。需要细分时,使用 [九章导航](chapter-content-spec.md) 中相关组或内容单元的标题,均写为 `###`,按导航顺序排列。允许只展开其中一部分;不要求全稿包含 36 组、161 个单元,也不要求各方案展开相同的部分。不使用四级及更深标题。
|
||||
|
||||
1. 号段按一级章节预留,写在章标题之后;三级章节不单独分号段,避免号段过碎。
|
||||
2. `S` 编号全文连续、唯一、递增,必须落在本章号段内;空号允许,越界、倒序和重复不允许。
|
||||
3. 禁止四级标题;禁止在二级标题下直接放 `S` 条目。
|
||||
4. 阶段名称(初期、中期、后期或卷、篇、幕)只能出现在卡片正文、表格列和第五章中,不得成为二、三级标题。
|
||||
5. 所有候选标题逐字同构;候选序号只出现在一级标题,不进入共同目录。
|
||||
## 内容表达
|
||||
|
||||
## 四、节奏与披露
|
||||
- 根设定只记录作者明确要求或已确认的约束;提出新的设想不自动成为根设定。
|
||||
- 已形成的内容写具体,建议与疑问明确标注。中心、终局和远期阶段可保留待定,不因模板要求而虚构完整答案。
|
||||
- 需要引用具体条目时可用 `- **S001|名称:** 内容`;编号可选,使用时全稿唯一。没有预分号段、数量下限或句长要求。
|
||||
- 同一事实一个主归属,其他章节写引用或本章视角的增量。未归位素材留在最后一节。
|
||||
- 不留下九章空节或裸露的 `TODO`、`TBD`、`[待填]`;把尚未解决的事说清,不以“未探索”冒充已经设计完成。
|
||||
|
||||
候选先完成追读链,再扩写设定。三个早期里程碑都要同时具备“当期问题、行动目标、实际兑现、付出代价、章末新问题”。
|
||||
## 最终代码校验的范围
|
||||
|
||||
- 前三章让读者看见异常和价值,不解释最终来源,不把终局阵营拉到台前。
|
||||
- 前十章让优势经过对手或任务验证,同时暴露限制,完成第一个局部闭环。
|
||||
- 前五十章完成初期主冲突和一次身份或能力抬升,再打开更大舞台。
|
||||
- 后续阶段继续扩大问题尺度;早期劳动、训练、资源或关系线要换规模延续,不能用完即丢。
|
||||
[最终格式校验器](../scripts/validate_candidates.py) 使用 [标题树](../scripts/candidate_tree.json) 核对固定目录和已展开标题,检查空节、裸占位符及可选 S 编号重复。一次可交付一份稿,也可分别校验作者要求的多份稿;不比较方案数量、篇幅、完整度或优劣。
|
||||
|
||||
信息分三线记录:世界实际状态、角色知道的状态、读者已看到的状态。一个谜底可以在作者侧确定,但它的征兆、误判、局部解释和正式揭示必须分开放置,归口见 chapter-content-spec 第七章。
|
||||
|
||||
## 五、独立性
|
||||
|
||||
候选子代理只能读取任务包、共同参考证据和自己的文件。不得搜索候选目录,不得读取其他候选,不得询问主会话“前一份怎么写”。主会话也不能把某份候选的优点转述给尚未完成的代理。
|
||||
|
||||
长文需要多轮时,在同一候选内部维护简短设计账本:已经确定的因果、数值、人物关系、阶段披露点和未完成章节。账本只属于该候选,续跑时与任务包一起提供。
|
||||
|
||||
## 六、表达复核
|
||||
|
||||
完成内容复核后再处理语言。表达层重点清理:
|
||||
|
||||
- 空泛评价代替具体事件,例如只说“极具张力”“层层递进”。
|
||||
- 每段都用“不是……而是……”或三项排比制造伪力度。
|
||||
- 所有条目使用相同句式、相同结尾或固定总结句。
|
||||
- 频繁使用“同时、此外、值得注意的是、总而言之”等连接词。
|
||||
- 用旁白宣布人物多强、世界多危险,却没有任务、损失和对比支撑。
|
||||
- 为了显得完整,提前解释终局真相或最终敌人的全貌。
|
||||
|
||||
复核可以改句子长短、用词和段落节奏,不能改事实、编号、目录、量级、代价或披露节点。
|
||||
|
||||
## 七、完成判定
|
||||
|
||||
一份候选只有同时满足以下条件才算完成。机械可查项由校验器把关,其余由语义复核逐项核对:
|
||||
|
||||
1. 九个一级章节齐全,全部二级、三级标题齐全且顺序一致。
|
||||
2. 每个三级章节有内容说明,并有 `S` 条目或显式不适用标注。
|
||||
3. 每个核心实体都有稳定定义和阶段状态。
|
||||
4. 每个阶段都有进入条件、核心问题、状态矩阵和退出条件。
|
||||
5. 每个阶段转变都有事件或明确原因。
|
||||
6. 主要关系都有初始状态、变化触发和后续状态。
|
||||
7. 主要规则都有触发、边界、代价和可观察结果。
|
||||
8. 主要事件都改变至少一项状态或推进一项承诺。
|
||||
9. 世界事实、角色知情和读者披露三条线没有混写。
|
||||
10. 前三章、前十章、前五十章分别有目标、兑现、代价和新问题。
|
||||
11. 没有把终局答案提前泄露给不该知道的角色或读者。
|
||||
12. 没有重复定义同一事实,也没有同名异义。
|
||||
13. 数字、单位、术语和时间线一致。
|
||||
14. 候选之间结构相同,但故事方案可以不同。
|
||||
15. 任务包指定了数量或字数门槛时同时满足;未指定时不以篇幅论完成。
|
||||
|
||||
## 八、交付报告
|
||||
|
||||
所有候选过门后才生成比较报告。报告逐案回答:
|
||||
|
||||
1. 前三章靠什么让读者继续。
|
||||
2. 前十章兑现了什么,暴露了什么限制。
|
||||
3. 前五十章在哪个事件形成初期高潮。
|
||||
4. 初期机制如何换规模进入中期和后期。
|
||||
5. 阶段状态矩阵是否闭合,转段事件是否成立。
|
||||
6. 这套方案最大的收益、代价和失速风险是什么。
|
||||
|
||||
报告只供用户选择,不宣告胜者。用户没有拍板前,不合并、不落库、不写正文。
|
||||
|
||||
## 九、用户授权后的逐章统合
|
||||
|
||||
用户明确要求综合多份候选时,独立生成阶段结束,进入串行统合阶段。统合以一级章节为节点,代理必须读取该章完整的三级结构。统合不再要求来源隔离,但每个代理仍只处理一个当前章节,不能提前读取后续来源章节。详细输入、权威顺序、输出格式与失败边界见 [`合并故事方案` 的串行统合合同](../../合并故事方案/references/serial-merge-contract.md)。
|
||||
|
||||
串行链的每个节点都要生成 fresh 会话。当前章只有经主会话审定并写入统合候选后,才可作为下一节点的权威前文。未审定的代理输出、分析摘要和舍弃方案不能进入下一节点上下文。
|
||||
格式通过只表示可读、可交接。根设定是否确为作者决定、内容是否自洽、何时选方向,由指令和作者判断。所选稿仍是本地探索材料,正式 setting 的字段与确认合同归 [制定作品规划](../../制定作品规划/SKILL.md)。
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
> 六至九章见 [chapter-content-spec-chapters-6-9.md](chapter-content-spec-chapters-6-9.md)。
|
||||
|
||||
按当前问题查阅其中一节即可。下列字段和判断用于深化已展开的设计,不要求从头填完;未讨论的内容可以留待后续。使用方式见 [章节内容规范](chapter-content-spec.md)。
|
||||
|
||||
|
||||
本章回答:这部作品是什么、写给谁、承诺什么、边界在哪。不写世界内部设定、情节和角色细节。
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
> 一至五章见 [chapter-content-spec-chapters-1-5.md](chapter-content-spec-chapters-1-5.md)。
|
||||
|
||||
按当前问题查阅其中一节即可。下列字段和判断用于深化已展开的设计,不要求预先完成全书阶段、结局与披露计划。使用方式见 [章节内容规范](chapter-content-spec.md)。
|
||||
|
||||
|
||||
本章回答:故事如何一步步发生。调用第三、四章设定,不得在事件中偷偷新增未定义的核心规则。
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 章节内容规范(完善故事基础设定/chapter-spec-v2)
|
||||
# 章节内容规范
|
||||
|
||||
本规范是 `完善故事基础设定/v2` 的内容事实源:定义固定三级章节树、每级标题职责、内容单元模板、阶段状态规范和跨章归属规则。候选子代理、统合代理和语义复核都以本文件为准;任务包只引用本文件,不重复保存章节合同。
|
||||
本规范保留九章设计知识,供探索和深化时按需查阅。每个内容单元说明可以继续想什么,不要求在某一轮全部回答。探索怎么推进见 [流程主文档](../SKILL.md),最终交付的 Markdown 形状见 [产出范式](candidate-contract.md)。
|
||||
|
||||
## 一、层级规则
|
||||
|
||||
@ -11,12 +11,12 @@
|
||||
S 条目:三级标题下的具体设定、规则、实体、关系、阶段状态或事件
|
||||
```
|
||||
|
||||
1. 九章、全部二级与三级标题固定,顺序不得调整,所有候选逐字同构。
|
||||
2. 不允许四级标题;不允许在二级标题下直接放 `S` 条目。
|
||||
3. 三级标题不按阶段复制。阶段变化由第五章状态矩阵和实体卡阶段字段表达。
|
||||
4. 每个三级章节开头写四字段说明(回答/应写/不写/关联),正文内容以本规范为准。
|
||||
5. 三级章节无适用内容时写“不适用及原因”,不得留空。
|
||||
6. `## 目录`可选,不属于内容章节。
|
||||
1. 九章是最终稿的固定主目录;平时只展开当前需要的部分,不比较各稿是否逐字同构。
|
||||
2. 章内可直接写概述或具体设想。需要细分时,从下方标题树选用相关组与单元,按原顺序排列;没有探索的单元不必建空标题。
|
||||
3. 已涉及阶段变化时,由第五章状态矩阵和实体卡阶段字段表达;不为填表强行划分全书阶段。
|
||||
4. 分章参考中的“回答/应写/不写/关联”是给规划角色的提示,不要求复制进作者每轮产物。
|
||||
5. 未探索、暂定、不适用都应按实际情况说明。只有“尚未探索”时,不把它改写为“不适用”。
|
||||
6. 上述一、二、三级是设计知识层级;最终 Markdown 使用一个作品 H1、章 H2、组和单元 H3,具体格式只由产出范式规定。
|
||||
|
||||
## 二、固定章节树总览
|
||||
|
||||
@ -86,11 +86,11 @@ S 条目:三级标题下的具体设定、规则、实体、关系、阶段状
|
||||
|
||||
## 四、内容单元模板
|
||||
|
||||
不同三级章节使用不同的内容单元。`S` 条目正文围绕同一单元自然展开,不使用整齐划一的表单腔,但字段必须覆盖。
|
||||
以下字段用于深化已选中的内容单元,提醒规划角色发现联系与缺口。围绕本轮问题自然展开;未讨论的字段可以留待后续,不把整张卡的字段覆盖当成探索通行条件。`S` 编号仅在需要引用时使用。
|
||||
|
||||
### 1. 作品命题单元(第一章)
|
||||
|
||||
必须覆盖:作品定位、目标读者、一句话故事、主角起点、核心目标、外部期限或主要压力、核心卖点、主题问题、读者体验、题材边界。不写代理过程、候选比较、工具模型、尚未确认的作品事实。
|
||||
可逐步考虑:作品定位、目标读者、一句话故事、主角起点、核心目标、外部期限或主要压力、核心卖点、主题问题、读者体验、题材边界。尚未确认的内容标明是设想;不因存在该字段就强定答案。工具与比较过程留在取舍说明中。
|
||||
|
||||
### 2. 稳定规则单元(第二、四章)
|
||||
|
||||
@ -126,7 +126,7 @@ S 条目:三级标题下的具体设定、规则、实体、关系、阶段状
|
||||
|
||||
## 五、阶段状态规范
|
||||
|
||||
阶段是全书的横向维度,不是某几个章节的专属分类。
|
||||
当作品已经需要设计阶段变化时,阶段是全书的横向维度,不是某几个章节的专属分类。以下规则只约束已经展开的阶段;未探索的远期变化明确保留未知,不要求先设计全书再继续人物或场景。
|
||||
|
||||
1. 动态实体至少记录:不变量;初始状态;各阶段状态;状态变化原因;终局状态或未知状态。
|
||||
2. 同一实体在某阶段没有变化时,必须显式写“本阶段保持不变”,不得跳过,否则无法区分“保持不变”与“设计遗漏”。
|
||||
|
||||
@ -30,17 +30,15 @@ def assert_selection_handoff(selection: dict) -> dict:
|
||||
+ ",".join(missing)
|
||||
+ "。用户未选定候选时不得进入 制定作品规划。"
|
||||
)
|
||||
if selection.get("confirmedByUser") is not True and str(selection.get("confirmedByUser")).lower() not in (
|
||||
"true", "yes", "1", "user",
|
||||
):
|
||||
# 允许字符串用户标识,但禁止空/false
|
||||
if selection.get("confirmedByUser") in (False, None, "", 0):
|
||||
raise HandoffError("SELECTION_HANDOFF_UNCONFIRMED:confirmedByUser 未确认")
|
||||
candidate = str(selection["selectedCandidate"]).strip()
|
||||
if not candidate:
|
||||
if selection["confirmedByUser"] is not True:
|
||||
raise HandoffError("SELECTION_HANDOFF_UNCONFIRMED:confirmedByUser 必须为布尔值 true")
|
||||
candidate = selection["selectedCandidate"]
|
||||
if not isinstance(candidate, str) or not candidate.strip():
|
||||
raise HandoffError("SELECTION_HANDOFF_EMPTY:selectedCandidate 为空")
|
||||
if not isinstance(selection["confirmedAt"], str) or not selection["confirmedAt"].strip():
|
||||
raise HandoffError("SELECTION_HANDOFF_INVALID:confirmedAt 必须为非空时间文本")
|
||||
return {
|
||||
"selectedCandidate": candidate,
|
||||
"selectedCandidate": candidate.strip(),
|
||||
"confirmedByUser": selection["confirmedByUser"],
|
||||
"confirmedAt": selection["confirmedAt"],
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"contractVersion": "完善故事基础设定/v2",
|
||||
"contractVersion": "完善故事基础设定/output-v3",
|
||||
"chapters": [
|
||||
{
|
||||
"heading": "一、作品定位与核心命题",
|
||||
|
||||
@ -1,41 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""校验作品设定初始化的根设定与同构候选。
|
||||
|
||||
只检查机械易错且人眼难以抽查的项:三级标题树同构、S 编号唯一递增不越号段、
|
||||
占位符残留、根设定格式与句长。内容是否完整由语义复核按完成判定核对,
|
||||
本脚本不做数量、字数或内容质量门槛。
|
||||
"""
|
||||
"""只校验阶段 0 最终交付稿的格式,不参与探索调度或创作判断。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from dataclasses import asdict, dataclass
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Iterable
|
||||
|
||||
TREE_PATH = Path(__file__).with_name("candidate_tree.json")
|
||||
HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.MULTILINE)
|
||||
RANGE_RE = re.compile(r"<!--\s*S(\d+)\s*[-—–]\s*S(\d+)\s*-->")
|
||||
SETTING_RE = re.compile(r"^\s*[-*]\s+(?:\*\*)?(S\d{3,})(?=[^\d])", re.MULTILINE)
|
||||
HEADING_RE = re.compile(r"^(#{1,6})[ \t]+(.+?)[ \t]*$", re.MULTILINE)
|
||||
SETTING_RE = re.compile(r"^\s*[-*]\s+(?:\*\*)?S(\d{3,})(?=[^\d]|$)", re.MULTILINE)
|
||||
PLACEHOLDER_RE = re.compile(
|
||||
r"(?<![A-Za-z0-9])(?:TODO|TBD|PLACEHOLDER)(?![A-Za-z0-9])|\[待填\]|待补充|待生成|在此填写",
|
||||
re.IGNORECASE,
|
||||
r"^\s*(?:[-*>]\s*)?(?:TODO|TBD|PLACEHOLDER|\[待填\])\s*$",
|
||||
re.MULTILINE | re.IGNORECASE,
|
||||
)
|
||||
ROOT_ITEM_RE = re.compile(r"^\s*-\s+\*\*([^*]+)\*\*(.+)$")
|
||||
ROOT_H2_RE = re.compile(r"^##\s+(.+?)\s*$", re.MULTILINE)
|
||||
ROOT_NESTED_RE = re.compile(r"^#{3,6}\s+", re.MULTILINE)
|
||||
PROCESS_TERMS = ("Claude", "claude", "子代理", "候选文档", "提示词", "工具调用", "设计过程", "方案来源")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidationConfig:
|
||||
min_candidates: int = 1
|
||||
min_item_chars: int = 20
|
||||
root_min_chars: int = 30
|
||||
root_max_chars: int = 60
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -45,315 +27,115 @@ class Problem:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SectionReport:
|
||||
heading: str
|
||||
range_start: int
|
||||
range_end: int
|
||||
setting_count: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CandidateReport:
|
||||
file: str
|
||||
setting_count: int
|
||||
headings: tuple[str, ...]
|
||||
sections: tuple[SectionReport, ...]
|
||||
|
||||
|
||||
def visible_chars(text: str) -> int:
|
||||
"""去掉常见 Markdown 标记后统计非空白可见字符。"""
|
||||
cleaned = re.sub(r"[*_`#>\[\]()]", "", text)
|
||||
return len(re.sub(r"\s+", "", cleaned))
|
||||
|
||||
|
||||
def _problem(code: str, path: Path, message: str) -> Problem:
|
||||
return Problem(code=code, file=str(path), message=message)
|
||||
|
||||
|
||||
def load_tree(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def expected_heading_sequence(tree: dict) -> list[str]:
|
||||
"""按冻结树返回 章→节→单元 的扁平标题序列。"""
|
||||
sequence: list[str] = []
|
||||
for chapter in tree["chapters"]:
|
||||
sequence.append(chapter["heading"])
|
||||
for section in chapter["sections"]:
|
||||
sequence.append(section["heading"])
|
||||
sequence.extend(section["units"])
|
||||
return sequence
|
||||
|
||||
|
||||
def validate_root(path: Path, config: ValidationConfig) -> list[Problem]:
|
||||
problems: list[Problem] = []
|
||||
text = path.read_text(encoding="utf-8")
|
||||
headings = list(ROOT_H2_RE.finditer(text))
|
||||
root_index = next((index for index, match in enumerate(headings) if "根设定" in match.group(1)), None)
|
||||
if root_index is None:
|
||||
return [_problem("ROOT_SECTION_MISSING", path, "未找到包含“根设定”的二级标题")]
|
||||
|
||||
start = headings[root_index].end()
|
||||
end = headings[root_index + 1].start() if root_index + 1 < len(headings) else len(text)
|
||||
body = text[start:end]
|
||||
|
||||
if ROOT_NESTED_RE.search(body):
|
||||
problems.append(_problem("ROOT_STRUCTURE", path, "根设定内部出现三级或更深标题"))
|
||||
|
||||
for term in PROCESS_TERMS:
|
||||
if term in body:
|
||||
problems.append(_problem("ROOT_PROCESS_LEAK", path, f"根设定包含设计过程词:{term}"))
|
||||
|
||||
labels: set[str] = set()
|
||||
items = 0
|
||||
for line_number, raw_line in enumerate(body.splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
if not line or line == "---":
|
||||
continue
|
||||
match = ROOT_ITEM_RE.match(line)
|
||||
if not match:
|
||||
problems.append(
|
||||
_problem("ROOT_ITEM_FORMAT", path, f"根设定第 {line_number} 个相对行不是单行加粗标签条目")
|
||||
)
|
||||
continue
|
||||
items += 1
|
||||
label = match.group(1).strip()
|
||||
if label in labels:
|
||||
problems.append(_problem("ROOT_LABEL_DUPLICATE", path, f"根设定标签重复:{label}"))
|
||||
labels.add(label)
|
||||
length = visible_chars(line.lstrip("- "))
|
||||
if not config.root_min_chars <= length <= config.root_max_chars:
|
||||
problems.append(
|
||||
_problem(
|
||||
"ROOT_ITEM_LENGTH",
|
||||
path,
|
||||
f"根设定“{label}”为 {length} 个可见字符,要求 {config.root_min_chars}—{config.root_max_chars}",
|
||||
)
|
||||
)
|
||||
if items == 0:
|
||||
problems.append(_problem("ROOT_EMPTY", path, "根设定没有可校验条目"))
|
||||
return problems
|
||||
|
||||
|
||||
def _parse_candidate(path: Path, tree: dict, config: ValidationConfig) -> tuple[CandidateReport, list[Problem]]:
|
||||
def _validate(path: Path, tree: dict) -> tuple[CandidateReport, list[Problem]]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
headings = list(HEADING_RE.finditer(text))
|
||||
problems: list[Problem] = []
|
||||
|
||||
def fail(code: str, message: str) -> None:
|
||||
problems.append(Problem(code, str(path), message))
|
||||
|
||||
if sum(len(h.group(1)) == 1 for h in headings) != 1:
|
||||
fail("CANDIDATE_STRUCTURE", "交付稿须有且只有一个一级标题")
|
||||
if headings and len(headings[0].group(1)) != 1:
|
||||
fail("CANDIDATE_STRUCTURE", "首个标题须为作品一级标题")
|
||||
if any(len(h.group(1)) >= 4 for h in headings):
|
||||
fail("CANDIDATE_STRUCTURE", "交付稿不使用四级及更深标题")
|
||||
if PLACEHOLDER_RE.search(text):
|
||||
problems.append(_problem("CANDIDATE_PLACEHOLDER", path, "候选仍含待填占位文字"))
|
||||
fail("CANDIDATE_PLACEHOLDER", "请把裸占位符改为具体的待定或未探索说明")
|
||||
|
||||
headings = [(len(match.group(1)), match.group(2).strip(), match.start(), match.end()) for match in HEADING_RE.finditer(text)]
|
||||
h1_count = sum(1 for level, _, _, _ in headings if level == 1)
|
||||
if h1_count != 1:
|
||||
problems.append(_problem("CANDIDATE_STRUCTURE", path, f"候选应有且只有一个一级标题,实际 {h1_count} 个"))
|
||||
deep = [heading for level, heading, _, _ in headings if level >= 4]
|
||||
if deep:
|
||||
problems.append(_problem("CANDIDATE_STRUCTURE", path, f"候选出现四级或更深标题:{deep[0]}"))
|
||||
chapters = {chapter["heading"]: chapter for chapter in tree["chapters"]}
|
||||
expected = ["根设定", *chapters, "待决与取舍"]
|
||||
h2 = [h for h in headings if len(h.group(1)) == 2]
|
||||
titles = [h.group(2) for h in h2]
|
||||
if titles and titles[0] == "目录":
|
||||
titles = titles[1:]
|
||||
if titles != expected:
|
||||
fail("TREE_MISMATCH", "二级标题须按顺序包含根设定、九章和待决与取舍;目录仅可置于最前")
|
||||
|
||||
h2_list = [(heading, start, end) for level, heading, start, end in headings if level == 2]
|
||||
toc_seen = False
|
||||
chapter_index = 0
|
||||
all_ids: list[int] = []
|
||||
previous_range_end = 0
|
||||
sections: list[SectionReport] = []
|
||||
chapter_titles = [chapter["heading"] for chapter in tree["chapters"]]
|
||||
if any(
|
||||
len(h.group(1)) == 3 and (not h2 or h.start() < h2[0].start())
|
||||
for h in headings
|
||||
):
|
||||
fail("CANDIDATE_STRUCTURE", "三级标题须位于对应章节内")
|
||||
|
||||
for position, (heading, start, end) in enumerate(h2_list):
|
||||
body_end = h2_list[position + 1][1] if position + 1 < len(h2_list) else len(text)
|
||||
body = text[end:body_end]
|
||||
for index, heading in enumerate(h2):
|
||||
title = heading.group(2)
|
||||
end = h2[index + 1].start() if index + 1 < len(h2) else len(text)
|
||||
body = text[heading.end():end]
|
||||
# 标题和注释不能代替说明;未知内容可以用一句明确说明表达。
|
||||
prose = HEADING_RE.sub("", body)
|
||||
prose = re.sub(r"<!--.*?-->", "", prose, flags=re.DOTALL)
|
||||
if not re.sub(r"[\s>*_\x60-]", "", prose):
|
||||
fail("SECTION_EMPTY", f"“{title}”为空,请写已有内容或说明尚未探索的部分")
|
||||
|
||||
if heading == "目录":
|
||||
toc_seen = True
|
||||
if re.search(r"^###\s+", body, re.MULTILINE):
|
||||
problems.append(_problem("CANDIDATE_STRUCTURE", path, "目录下不得出现三级标题"))
|
||||
continue
|
||||
actual = [
|
||||
h.group(2) for h in headings
|
||||
if len(h.group(1)) == 3 and heading.end() <= h.start() < end
|
||||
]
|
||||
allowed: list[str] = []
|
||||
if title in chapters:
|
||||
for section in chapters[title]["sections"]:
|
||||
allowed.append(section["heading"])
|
||||
allowed.extend(section["units"])
|
||||
positions = {name: position for position, name in enumerate(allowed)}
|
||||
if any(name not in positions for name in actual):
|
||||
fail("TREE_MISMATCH", f"“{title}”含导航之外的三级标题")
|
||||
elif any(positions[right] <= positions[left] for left, right in zip(actual, actual[1:])):
|
||||
fail("TREE_MISMATCH", f"“{title}”的已展开标题重复或顺序错误")
|
||||
|
||||
if chapter_index >= len(chapter_titles):
|
||||
problems.append(_problem("TREE_MISMATCH", path, f"多出章节标题:{heading}"))
|
||||
continue
|
||||
expected_title = chapter_titles[chapter_index]
|
||||
if heading != expected_title:
|
||||
problems.append(
|
||||
_problem("TREE_MISMATCH", path, f"第 {chapter_index + 1} 章应为“{expected_title}”,实际为“{heading}”")
|
||||
)
|
||||
chapter_index += 1
|
||||
continue
|
||||
chapter_index += 1
|
||||
|
||||
expected_subs: list[str] = []
|
||||
for section in tree["chapters"][chapter_index - 1]["sections"]:
|
||||
expected_subs.append(section["heading"])
|
||||
expected_subs.extend(section["units"])
|
||||
actual_subs = [sub_heading for level, sub_heading, sub_start, _ in headings if level == 3 and start < sub_start < body_end]
|
||||
if actual_subs != expected_subs:
|
||||
expected_set = set(expected_subs)
|
||||
missing = [item for item in expected_subs if item not in set(actual_subs)]
|
||||
extra = [item for item in actual_subs if item not in expected_set]
|
||||
detail = []
|
||||
if missing:
|
||||
detail.append(f"缺 {len(missing)} 个(首个:{missing[0]})")
|
||||
if extra:
|
||||
detail.append(f"多 {len(extra)} 个(首个:{extra[0]})")
|
||||
if not detail:
|
||||
detail.append("顺序与冻结树不一致")
|
||||
problems.append(_problem("TREE_MISMATCH", path, f"章节“{heading}”标题树漂移:{';'.join(detail)}"))
|
||||
|
||||
ranges = RANGE_RE.findall(body)
|
||||
if len(ranges) != 1:
|
||||
problems.append(_problem("SECTION_RANGE", path, f"章节“{heading}”必须且只能有一个号段标记"))
|
||||
continue
|
||||
range_start, range_end = (int(value) for value in ranges[0])
|
||||
if range_start > range_end:
|
||||
problems.append(_problem("SECTION_RANGE", path, f"章节号段倒置:S{range_start:03d}-S{range_end:03d}"))
|
||||
if range_start <= previous_range_end:
|
||||
problems.append(_problem("SECTION_RANGE", path, "章节号段未按顺序递增或发生重叠"))
|
||||
previous_range_end = max(previous_range_end, range_end)
|
||||
|
||||
setting_matches = list(SETTING_RE.finditer(body))
|
||||
ids = [int(match.group(1)[1:]) for match in setting_matches]
|
||||
all_ids.extend(ids)
|
||||
for setting_id in ids:
|
||||
if not range_start <= setting_id <= range_end:
|
||||
problems.append(
|
||||
_problem(
|
||||
"SETTING_ID_OUT_OF_RANGE",
|
||||
path,
|
||||
f"S{setting_id:03d} 不在章节号段 S{range_start:03d}-S{range_end:03d} 内",
|
||||
)
|
||||
)
|
||||
for item_index, setting_match in enumerate(setting_matches):
|
||||
item_end = setting_matches[item_index + 1].start() if item_index + 1 < len(setting_matches) else len(body)
|
||||
item_text = body[setting_match.start():item_end]
|
||||
if visible_chars(item_text) < config.min_item_chars:
|
||||
problems.append(
|
||||
_problem("SETTING_ITEM_TOO_SHORT", path, f"{setting_match.group(1)} 内容过短,疑似只有标题或占位句")
|
||||
)
|
||||
sections.append(
|
||||
SectionReport(heading=heading, range_start=range_start, range_end=range_end, setting_count=len(ids))
|
||||
)
|
||||
|
||||
if chapter_index < len(chapter_titles):
|
||||
problems.append(
|
||||
_problem(
|
||||
"TREE_MISMATCH",
|
||||
path,
|
||||
f"缺少章节:{ '、'.join(chapter_titles[chapter_index:]) }",
|
||||
)
|
||||
)
|
||||
if toc_seen and chapter_index == 0:
|
||||
problems.append(_problem("CANDIDATE_STRUCTURE", path, "候选只有目录,没有内容章节"))
|
||||
|
||||
duplicates = sorted({setting_id for setting_id in all_ids if all_ids.count(setting_id) > 1})
|
||||
ids = [int(value) for value in SETTING_RE.findall(text)]
|
||||
duplicates = [value for value, count in Counter(ids).items() if count > 1]
|
||||
if duplicates:
|
||||
display = ", ".join(f"S{setting_id:03d}" for setting_id in duplicates[:10])
|
||||
problems.append(_problem("SETTING_ID_DUPLICATE", path, f"全文编号重复:{display}"))
|
||||
if any(later <= earlier for earlier, later in zip(all_ids, all_ids[1:])):
|
||||
problems.append(_problem("SETTING_ORDER", path, "全文 S 编号必须严格递增"))
|
||||
|
||||
report = CandidateReport(
|
||||
file=str(path),
|
||||
setting_count=len(all_ids),
|
||||
headings=tuple(heading for heading, _, _ in h2_list),
|
||||
sections=tuple(sections),
|
||||
)
|
||||
return report, problems
|
||||
fail("SETTING_ID_DUPLICATE", "编号重复:" + "、".join(f"S{value:03d}" for value in duplicates))
|
||||
return CandidateReport(str(path), len(ids)), problems
|
||||
|
||||
|
||||
def validate_candidates(
|
||||
paths: Iterable[Path],
|
||||
config: ValidationConfig,
|
||||
root_doc: Path | None = None,
|
||||
tree_path: Path = TREE_PATH,
|
||||
) -> tuple[list[CandidateReport], list[Problem]]:
|
||||
candidate_paths = sorted((Path(path) for path in paths), key=lambda item: str(item))
|
||||
problems: list[Problem] = []
|
||||
def validate_candidates(paths: Iterable[Path]) -> tuple[list[CandidateReport], list[Problem]]:
|
||||
"""各稿独立校验;不要求候选间同构,也不裁决是否已经完成创作。"""
|
||||
tree = json.loads(TREE_PATH.read_text(encoding="utf-8"))
|
||||
reports: list[CandidateReport] = []
|
||||
|
||||
if len(candidate_paths) < config.min_candidates:
|
||||
problems.append(
|
||||
Problem(
|
||||
code="CANDIDATE_COUNT",
|
||||
file="<group>",
|
||||
message=f"只有 {len(candidate_paths)} 份候选,要求至少 {config.min_candidates} 份",
|
||||
)
|
||||
)
|
||||
if root_doc is not None:
|
||||
if not root_doc.is_file():
|
||||
problems.append(_problem("ROOT_FILE_MISSING", root_doc, "根设定文档不存在"))
|
||||
else:
|
||||
problems.extend(validate_root(root_doc, config))
|
||||
|
||||
tree = load_tree(tree_path)
|
||||
for path in candidate_paths:
|
||||
if not path.is_file():
|
||||
problems.append(_problem("CANDIDATE_FILE_MISSING", path, "候选文件不存在"))
|
||||
problems: list[Problem] = []
|
||||
for item in paths:
|
||||
path = Path(item)
|
||||
try:
|
||||
report, found = _validate(path, tree)
|
||||
except (OSError, UnicodeError) as exc:
|
||||
problems.append(Problem("CANDIDATE_FILE_UNREADABLE", str(path), str(exc)))
|
||||
continue
|
||||
report, file_problems = _parse_candidate(path, tree, config)
|
||||
reports.append(report)
|
||||
problems.extend(file_problems)
|
||||
|
||||
if len(reports) > 1:
|
||||
baseline = reports[0]
|
||||
baseline_shape = tuple((section.heading, section.range_start, section.range_end) for section in baseline.sections)
|
||||
for report in reports[1:]:
|
||||
shape = tuple((section.heading, section.range_start, section.range_end) for section in report.sections)
|
||||
if report.headings != baseline.headings or shape != baseline_shape:
|
||||
problems.append(
|
||||
Problem(
|
||||
code="CANDIDATE_STRUCTURE_MISMATCH",
|
||||
file=report.file,
|
||||
message=f"目录或号段与基准候选 {baseline.file} 不一致",
|
||||
)
|
||||
)
|
||||
problems.extend(found)
|
||||
if not reports and not problems:
|
||||
problems.append(Problem("CANDIDATE_FILE_MISSING", "<input>", "请提供一份最终交付稿"))
|
||||
return reports, problems
|
||||
|
||||
|
||||
def _build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="校验作品设定初始化候选")
|
||||
parser.add_argument("files", nargs="+", type=Path, help="候选 Markdown 文件")
|
||||
parser.add_argument("--root-doc", type=Path, help="含根设定的唯一前期设计文档")
|
||||
parser.add_argument("--min-candidates", type=int, default=1)
|
||||
parser.add_argument("--min-item-chars", type=int, default=20)
|
||||
parser.add_argument("--root-min-chars", type=int, default=30)
|
||||
parser.add_argument("--root-max-chars", type=int, default=60)
|
||||
parser.add_argument("--tree", type=Path, default=TREE_PATH, help="冻结三级树 JSON")
|
||||
parser.add_argument("--json", action="store_true", help="输出稳定 JSON")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = _build_parser()
|
||||
parser = argparse.ArgumentParser(description="校验阶段 0 最终交付稿格式")
|
||||
parser.add_argument("files", nargs="+", type=Path, help="已整理的作品探索稿")
|
||||
parser.add_argument("--json", action="store_true", help="输出稳定 JSON")
|
||||
args = parser.parse_args(argv)
|
||||
if args.root_min_chars > args.root_max_chars:
|
||||
parser.error("--root-min-chars 不能大于 --root-max-chars")
|
||||
|
||||
config = ValidationConfig(
|
||||
min_candidates=args.min_candidates,
|
||||
min_item_chars=args.min_item_chars,
|
||||
root_min_chars=args.root_min_chars,
|
||||
root_max_chars=args.root_max_chars,
|
||||
)
|
||||
reports, problems = validate_candidates(args.files, config, args.root_doc, args.tree)
|
||||
status = "ok" if not problems else "invalid"
|
||||
code = "SETTING_INIT_OK" if not problems else "SETTING_INIT_VALIDATION_FAILED"
|
||||
payload = {
|
||||
"status": status,
|
||||
"code": code,
|
||||
"reports": [asdict(report) for report in reports],
|
||||
"problems": [asdict(problem) for problem in problems],
|
||||
}
|
||||
|
||||
reports, problems = validate_candidates(args.files)
|
||||
code = "SETTING_INIT_VALIDATION_FAILED" if problems else "SETTING_INIT_OK"
|
||||
if args.json:
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
print(json.dumps({
|
||||
"status": "invalid" if problems else "ok",
|
||||
"code": code,
|
||||
"reports": [asdict(report) for report in reports],
|
||||
"problems": [asdict(problem) for problem in problems],
|
||||
}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"{code}: {len(reports)} 份候选,{len(problems)} 个问题")
|
||||
for report in reports:
|
||||
print(f"- {report.file}: {report.setting_count} 项")
|
||||
print(f"{code}: {len(reports)} 份交付稿,{len(problems)} 个格式问题")
|
||||
for problem in problems:
|
||||
print(f"[{problem.code}] {problem.file}: {problem.message}")
|
||||
return 0 if not problems else 1
|
||||
return 1 if problems else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
raise SystemExit(main())
|
||||
|
||||
@ -17,20 +17,20 @@
|
||||
贯穿五阶段的硬合同(均为既有合同,本节只引用):
|
||||
|
||||
- **横切总闸**:一切正式规划候选先落 Shadow,经用户确认翻 Confirmed 后才进入生成上下文(Shadow/Canonical 双轨,见 [架构-02](../../../../design-docs/架构-02-核心数据结构与双轨模型.md))。
|
||||
- **阶段 0 探索例外**:`完善故事基础设定` 产出的多案比较稿属于正式规划之前的仓内探索材料,只留在用户指定的设计文档与候选目录,不进 PostgreSQL,也不得进入生成上下文。用户选定方向并通过 `assert_selection_handoff.py` 后,由 `制定作品规划` 把选定内容重写为 `setting` Shadow;该行经用户确认成为 Confirmed,才算阶段 0 正式退出。候选被选中不等于规划已确认。
|
||||
- **阶段 0 探索例外**:`完善故事基础设定` 通过主文档及子阶段提示词管理多轮探索,材料默认留在 `.agents.local/草稿/`,不进 PostgreSQL,也不得进入正文生成上下文。代码只在整理交付时核对最终稿及选定记录的格式,不管理创作步骤。用户明确选定当前方向后,由 `制定作品规划` 整理为 `setting` Shadow;用户再确认这份具体 setting,才算阶段 0 正式退出。局部点子被采纳、方向被选中和正式规划确认是不同决定。
|
||||
- **上层约束下层**:大纲派生卷纲、卷纲派生细纲;下层规划不得与已确认的上层规划相悖,冲突时以下层退回为准。
|
||||
|
||||
### 阶段定义
|
||||
|
||||
阶段 0 的多案比较稿按上面的探索例外处理;选定方向的正式 `setting` 与阶段 1–4 的规划产出都经 `persist_planning` 落 `example_planning_section`(默认 `state='shadow'`),用户确认后翻 `confirmed`。
|
||||
阶段 0 的作品探索稿与局部比较稿按上面的探索例外处理;选定方向的正式 `setting` 与阶段 1–4 的规划产出都经 `persist_planning` 落 `example_planning_section`(默认 `state='shadow'`),用户确认后翻 `confirmed`。
|
||||
|
||||
**阶段 0 · 定盘**
|
||||
|
||||
- 进入条件:作品已创建(作品行存在,见 [01-作品领域](01-作品领域.md))。
|
||||
- 探索产出:冻结根设定后生成可比较候选;候选只供用户选方向,不是 Shadow,也不是正式内容。统合多案仍只得到一份待选候选。
|
||||
- 正式产出:通过选定交接门后,`制定作品规划` 按 `work_core`(题材定位 / 核心卖点 / 主题立意 / 基调 / 禁区 / 核心悬念 / 谜底与真相 / 结局方向 / 目标读者,字段权威见 `muse/content/meta/schemas/work_core.yaml`)和 `novel_work` 的篇幅目标(目标章数 × 单章字数区间 × 分卷数,见 `muse/content/meta/schemas/novel_work.yaml`)生成 `section_type='setting'` Shadow。
|
||||
- 机械门禁:**篇幅目标三要素齐全**——当前为文档纪律,待升级为落库校验。
|
||||
- 退出条件:用户选定记录通过交接门,且 `work_core` 九字段与篇幅目标三要素齐备的 `setting` 已由用户确认(`confirmed`)。
|
||||
- 探索产出:持续充实的作品探索稿,按需附局部方案。素材、联系、中心与结构可以往返发展;中心和远期内容可以暂定。九章清单用于导航,整理交付时才按固定目录归位,已展开内容具体写明,未探索部分保留说明。完整指令唯一入口为 [完善故事基础设定](../../lifecycle/flow/skills/book/完善故事基础设定/SKILL.md)。
|
||||
- 正式产出:用户选定当前方向后,`制定作品规划` 按 `work_core`(题材定位 / 核心卖点 / 主题立意 / 基调 / 禁区 / 核心悬念 / 谜底与真相 / 结局方向 / 目标读者,字段权威见 `muse/content/meta/schemas/work_core.yaml`)和 `novel_work` 的篇幅目标(目标章数 × 单章字数区间 × 分卷数,见 `muse/content/meta/schemas/novel_work.yaml`)生成 `section_type='setting'` Shadow。探索中的未知不伪装为已确认事实;必要缺口在这次正式交接时向用户明确。
|
||||
- 最终产出校验:`validate_candidates.py` 只查探索交付稿格式;`assert_selection_handoff.py` 只查选定记录形状,不能证明真实用户授权,也未接入 `persist_planning` 自动拦截。创作意义、完成程度与方向选择由提示词和用户判断。正式 setting 的九字段与篇幅三要素当前仍靠规划指令及人审核对,字段覆盖的落库校验待建;不将其变成逐轮探索门槛。
|
||||
- 退出条件:真实用户方向选择及选定记录已核对,且 `work_core` 九字段与篇幅目标三要素齐备的 `setting` 已由用户确认(`confirmed`)。确认当前版本不承诺永不修订;正文带来的新发现按正式规划修订合同处理。
|
||||
|
||||
**阶段 1 · 大纲 / 卷纲**
|
||||
|
||||
@ -95,7 +95,7 @@
|
||||
1. 设定全书闭环校验 + 全书设定台账(把「演变历程」从只追加日志升级为闭环义务,新增设定 × 章消费矩阵视图;见 [02-实体领域 §8](02-实体领域.md))。
|
||||
2. 卷数合同:`novel_work` 篇幅目标增「分卷数」,`outline` 分卷粗纲卷数须与之一致并机械校验(见 [01-作品领域 §6](01-作品领域.md))。
|
||||
3. 范式规划期绑定:消费侧已通;作品行 `pattern_bindings` 承载列为主仓生产方向(实验仓暂以已确认 assembly 行承载,见 [03-范式领域 §6](03-范式领域.md))。
|
||||
- **待建(其余门禁升级与取数补全)**:阶段 0–1 的字数 / 比例 / 四要素校验、阶段 2 字段覆盖门禁扩到其余型(中文键 schema 与英文 payload 对齐后纳入)、阶段 4 细纲硬约束覆盖率门禁、结构化 `style` 八字段画像的书级抽取(当前注入设定行一句话文风)、写作期范式须可回指 confirmed 绑定的机械门禁。字段层面的回填(`novel_work` 分卷数、`pattern_bindings` 库列)属各 owner 文档与 `meta/schemas`,不在本节。
|
||||
- **待建(其余门禁升级与取数补全)**:阶段 0 正式 setting 字段覆盖校验、阶段 1 的字数 / 比例 / 四要素校验、阶段 2 字段覆盖门禁扩到其余型(中文键 schema 与英文 payload 对齐后纳入)、阶段 4 细纲硬约束覆盖率门禁、结构化 `style` 八字段画像的书级抽取(当前注入设定行一句话文风)、写作期范式须可回指 confirmed 绑定的机械门禁。阶段 0 探索过程不建设字数、条目数量或全书完成度门禁。字段层面的回填(`novel_work` 分卷数、`pattern_bindings` 库列)属各 owner 文档与 `meta/schemas`,不在本节。
|
||||
|
||||
### 本节验收条件
|
||||
|
||||
|
||||
@ -118,7 +118,7 @@ Skill 还有两个实现形态:
|
||||
0 1 2 3 4
|
||||
```
|
||||
|
||||
正式规划产物先落 Shadow,你确认后才变 Confirmed,才允许进入下一步的生成上下文。阶段 0 的多案比较稿是唯一例外:它只供选方向,不入库;选定方向必须经 `制定作品规划` 写成 `setting` Shadow 并确认,才算定盘完成。下层不得跟已确认的上层打架。
|
||||
正式规划产物先落 Shadow,你确认后才变 Confirmed,才允许进入下一步的生成上下文。阶段 0 的作品探索稿与局部比较稿是唯一例外:它们由提示词引导逐步充实,保留在本地草稿,不入库;选定方向必须经 `制定作品规划` 写成 `setting` Shadow 并确认,才算定盘完成。下层不得跟已确认的上层打架。
|
||||
|
||||
### 章级:三个节点
|
||||
|
||||
@ -183,15 +183,17 @@ flowchart TD
|
||||
|
||||
### 阶段 0 · 定盘(节点 P)
|
||||
|
||||
**要完成**:先从多案中选定方向,再把这本书是什么、写给谁、禁区在哪、篇幅目标(目标章数 × 单章字数 × 分卷数)写成正式 `setting`。九个核心字段齐且正式规划已确认,才能往下。
|
||||
**怎么推进**:从作者已有材料进入,由 [流程主文档](../lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) 调度六份子阶段提示词,允许素材、联系、中心与结构往返发展。九章清单按需展开;只在实际分歧处比较局部方案,整理交付时才核对最终格式。
|
||||
|
||||
**如何交接**:作者明确选择当前方向后,再把已有约束和待决问题交给规划角色,按正式字段合同整理 `setting`。九个核心字段与篇幅目标(目标章数 × 单章字数区间 × 分卷数)齐备、具体 setting 再经用户确认,才进入下一阶段。局部采纳不自动触发交接,中心也不因一次选择而永久冻结。
|
||||
|
||||
功能链:`setting_init` → [完善故事基础设定](../lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) → 规划师。
|
||||
|
||||
| Skill | 意图 | 规则 | 工具 / 方法 |
|
||||
|---|---|---|---|
|
||||
| [完善故事基础设定](../lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) | 固化根设定,按固定三级章节树出可比较候选 | 候选不落库、不进生成上下文;选中只完成方向交接,不等于规划确认 | `validate_candidates.py`、`assert_selection_handoff.py`;合同在 `references/candidate-contract.md` |
|
||||
| [合并故事方案](../lifecycle/flow/skills/book/合并故事方案/SKILL.md) | 多案逐章合成一案 | 一级章节一个新代理;根设定优先 | `serial_merge.py` |
|
||||
| [提炼故事概念](../../.agent/skills/规划/提炼故事概念/SKILL.md) | 把「我想写 X」升级成能撑长篇的疑问 | idea 是陈述,concept 是疑问;四问(新鲜 / 角度 / 冲突 / 可撑长),可撑长必须过 | 无脚本。见 [方法:概念设计](#提炼故事概念-概念设计) |
|
||||
| [完善故事基础设定](../lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) | 一份主文档与六份子阶段指令,逐步形成作品探索稿 | 过程可往返,草稿不入库;方向与正式 setting 两次确认 | 仅整理交付时用 `validate_candidates.py`、`assert_selection_handoff.py` 校验最终格式 |
|
||||
| [合并故事方案](../lifecycle/flow/skills/book/合并故事方案/SKILL.md) | 按作者指定范围结合设想并解释取舍 | 不要求全书候选、逐章流水或一次收束中心 | 提示词;完整稿交回「整理交付」统一校验 |
|
||||
| [提炼故事概念](../../.agent/skills/规划/提炼故事概念/SKILL.md) | 需要时从已有材料试探持续冲突 | 四问作为深化概念的方法,不作为开始探索的前置门槛 | 无脚本。见 [方法:概念设计](#提炼故事概念-概念设计) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -46,8 +46,8 @@
|
||||
|
||||
| 技能名 | 技能文件 | 技能说明 |
|
||||
|---|---|---|
|
||||
| 合并故事方案 | [SKILL.md](lifecycle/flow/skills/book/合并故事方案/SKILL.md) | 把多份已完成的前期设计候选逐章串行统合成一份新候选,一级章节一个 fresh 代理,权威顺序固定为根设定优先。用户明确要求综合多案时使用;不生成候选、不做机械门校验、不改前期设计 SoT。 |
|
||||
| 完善故事基础设定 | [SKILL.md](lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) | 在正式规划前固化作品根设定,按固定三级章节树生成可比较的前期设计候选。用户仍在单文档前期设计阶段时使用;不写正文、不落库、不替用户定稿。不适用于合并多案(归 合并故事方案)或正式规划落库(归 制定作品规划)。 |
|
||||
| 合并故事方案 | [SKILL.md](lifecycle/flow/skills/book/合并故事方案/SKILL.md) | 作者明确要求结合几种已有故事设想时,按指定范围比较兼容性、解释取舍并形成合并建议。可合并局部片段或整稿,产物留在 .agents.local/草稿/;多轮探索归 完善故事基础设定,正式规划落库归 制定作品规划。 |
|
||||
| 完善故事基础设定 | [SKILL.md](lifecycle/flow/skills/book/完善故事基础设定/SKILL.md) | 作者从点子、片段或已有设计继续构思作品时,用提示词引导多轮探索,在 .agents.local/草稿/ 逐步形成作品探索稿。按需发展素材、联系、中心与结构,整理交付时才校验格式;指定的多案合并归 合并故事方案,正式规划落库归 制定作品规划。 |
|
||||
|
||||
## 4 结构与规划
|
||||
|
||||
|
||||
@ -22,14 +22,18 @@ def expect_fail(payload) -> None:
|
||||
|
||||
def main() -> None:
|
||||
ok = assert_selection_handoff({
|
||||
"selectedCandidate": "docs/design/candidates/A.md",
|
||||
"selectedCandidate": ".agents.local/草稿/作品/作品探索.md",
|
||||
"confirmedByUser": True,
|
||||
"confirmedAt": "2026-08-20T12:00:00+08:00",
|
||||
})
|
||||
assert ok["selectedCandidate"].endswith("A.md")
|
||||
assert ok["selectedCandidate"].endswith("作品探索.md")
|
||||
expect_fail({})
|
||||
expect_fail({"selectedCandidate": "x", "confirmedByUser": False, "confirmedAt": "t"})
|
||||
for unconfirmed in ("false", "true", "user", 1, ["true"]):
|
||||
expect_fail({"selectedCandidate": "x", "confirmedByUser": unconfirmed, "confirmedAt": "t"})
|
||||
expect_fail({"selectedCandidate": "", "confirmedByUser": True, "confirmedAt": "t"})
|
||||
expect_fail({"selectedCandidate": ["x"], "confirmedByUser": True, "confirmedAt": "t"})
|
||||
expect_fail({"selectedCandidate": "x", "confirmedByUser": True, "confirmedAt": " "})
|
||||
try:
|
||||
raise HandoffError("x")
|
||||
except click.ClickException:
|
||||
|
||||
@ -1,123 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""前期设计候选校验器的离线测试:只覆盖机械门禁自身的正确性。"""
|
||||
"""最终交付格式校验器的回归检查;不模拟或约束创作对话。"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "flow" / "skills" / "book" / "完善故事基础设定" / "scripts"
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse/lifecycle/flow/skills/book/完善故事基础设定/scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from validate_candidates import ValidationConfig, validate_candidates, validate_root # noqa: E402
|
||||
from validate_candidates import validate_candidates # noqa: E402
|
||||
|
||||
TREE = json.loads((SCRIPT_DIR / "candidate_tree.json").read_text(encoding="utf-8"))
|
||||
BLOCK = "> 回答:本节回答什么。\n> 应写:本节写什么。\n> 不写:本节不写什么。\n> 关联:其他节。\n"
|
||||
ITEM = "- **S{sid:03d}|条目:** 这是带条件、代价和剧情用途的完整设定内容。"
|
||||
DRAFT = """# 雨伞|作品探索
|
||||
|
||||
## 根设定
|
||||
作者希望保留雨天的兄妹关系。
|
||||
|
||||
def chapter_bands() -> list[tuple[int, int]]:
|
||||
"""按每章实际条目数切分号段,保证全局连续编号都落在本章号段内。"""
|
||||
bands = []
|
||||
cursor = 1
|
||||
for chapter in TREE["chapters"]:
|
||||
count = sum(len(section["units"]) for section in chapter["sections"])
|
||||
bands.append((cursor, cursor + count - 1))
|
||||
cursor += count
|
||||
return bands
|
||||
## 一、作品定位与核心命题
|
||||
待定:先从人物行动中寻找中心。
|
||||
|
||||
## 二、故事世界与社会系统
|
||||
未探索:尚未讨论社会制度。
|
||||
|
||||
BANDS = chapter_bands()
|
||||
## 三、角色与关系
|
||||
哥哥每天擦一把从不用的伞,妹妹故意把它拿走。
|
||||
|
||||
## 四、故事机制与关键实体
|
||||
未探索:尚未决定是否有超自然力量。
|
||||
|
||||
def make_candidate() -> str:
|
||||
parts = ["# 候选\n\n## 目录\n\n- 章节列表\n"]
|
||||
counter = 1
|
||||
for index, chapter in enumerate(TREE["chapters"]):
|
||||
start, end = BANDS[index]
|
||||
parts.append(f"## {chapter['heading']}\n\n<!-- S{start:03d}-S{end:03d} -->\n")
|
||||
for section in chapter["sections"]:
|
||||
parts.append(f"### {section['heading']}\n\n{BLOCK}")
|
||||
for unit in section["units"]:
|
||||
parts.append(f"### {unit}\n\n{BLOCK}")
|
||||
parts.append(ITEM.format(sid=counter))
|
||||
counter += 1
|
||||
return "\n".join(parts)
|
||||
## 五、阶段与状态变化
|
||||
未探索:只有当前这一幕。
|
||||
|
||||
## 六、情节架构与全书大纲
|
||||
未探索:兄妹争执后的事情还没想好。
|
||||
|
||||
## 七、主题与读者体验弧线
|
||||
暂想保留亲近中带着疑问的感觉。
|
||||
|
||||
## 八、信息、悬念与伏笔管理
|
||||
待定:哥哥为何留着伞。
|
||||
|
||||
## 九、叙事执行与一致性
|
||||
先从动作里表现关系。
|
||||
|
||||
## 待决与取舍
|
||||
保留争执这一幕,暂缓决定雨伞的来历。
|
||||
"""
|
||||
|
||||
|
||||
class CandidateValidatorTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp_dir.cleanup)
|
||||
self.root = Path(self.temp_dir.name)
|
||||
self.config = ValidationConfig(min_candidates=2, min_item_chars=10)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def write(self, name: str, content: str) -> Path:
|
||||
path = self.root / name
|
||||
path.write_text(content, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def codes(self, *contents: str) -> set[str]:
|
||||
paths = [self.write(f"c{index}.md", content) for index, content in enumerate(contents)]
|
||||
_, problems = validate_candidates(paths, self.config)
|
||||
paths = []
|
||||
for index, content in enumerate(contents):
|
||||
path = self.root / f"draft-{index}.md"
|
||||
path.write_text(content, encoding="utf-8")
|
||||
paths.append(path)
|
||||
_, problems = validate_candidates(paths)
|
||||
return {problem.code for problem in problems}
|
||||
|
||||
def test_two_same_shape_candidates_pass(self) -> None:
|
||||
self.assertEqual(set(), self.codes(make_candidate(), make_candidate()))
|
||||
def test_one_partial_draft_without_ids_or_length_limits_passes(self) -> None:
|
||||
self.assertEqual(set(), self.codes(DRAFT))
|
||||
|
||||
def test_missing_unit_heading_fails(self) -> None:
|
||||
broken = make_candidate().replace("### 1.1.3 同类作品中的差异\n", "")
|
||||
self.assertIn("TREE_MISMATCH", self.codes(make_candidate(), broken))
|
||||
|
||||
def test_deep_heading_fails(self) -> None:
|
||||
broken = make_candidate().replace(
|
||||
"### 1.1.1 题材、载体与目标读者\n", "### 1.1.1 题材、载体与目标读者\n\n#### 细分\n"
|
||||
def test_drafts_can_expand_different_subsets(self) -> None:
|
||||
expanded = DRAFT.replace(
|
||||
"待定:先从人物行动中寻找中心。",
|
||||
"### 1.1 作品形态与定位\n\n### 1.1.1 题材、载体与目标读者\n\n暂定现实题材。",
|
||||
)
|
||||
self.assertIn("CANDIDATE_STRUCTURE", self.codes(make_candidate(), broken))
|
||||
self.assertEqual(set(), self.codes(DRAFT, expanded))
|
||||
|
||||
def test_duplicate_and_out_of_range_ids_fail(self) -> None:
|
||||
broken = make_candidate().replace("S002|条目:", "S001|条目:").replace("S003|条目:", "S002|条目:")
|
||||
codes = self.codes(make_candidate(), broken)
|
||||
self.assertIn("SETTING_ID_DUPLICATE", codes)
|
||||
self.assertIn("SETTING_ORDER", codes)
|
||||
def test_unknown_missing_or_reordered_headings_fail(self) -> None:
|
||||
for content in (
|
||||
DRAFT.replace("## 三、角色与关系", "## 三、人物"),
|
||||
DRAFT.replace("## 三、角色与关系\n", ""),
|
||||
DRAFT.replace("待定:先从人物行动中寻找中心。",
|
||||
"### 1.2 故事承诺与一句话梗概\n\n设想。\n\n### 1.1 作品形态与定位\n\n设想。"),
|
||||
):
|
||||
with self.subTest(content=content):
|
||||
self.assertIn("TREE_MISMATCH", self.codes(content))
|
||||
|
||||
def test_placeholder_fails(self) -> None:
|
||||
broken = make_candidate().replace("完整设定内容", "TODO")
|
||||
self.assertIn("CANDIDATE_PLACEHOLDER", self.codes(make_candidate(), broken))
|
||||
def test_empty_chapter_and_bare_placeholder_fail_but_explained_unknown_passes(self) -> None:
|
||||
self.assertIn("SECTION_EMPTY", self.codes(DRAFT.replace("未探索:尚未讨论社会制度。", "<!-- 留空 -->")))
|
||||
self.assertIn("CANDIDATE_PLACEHOLDER", self.codes(DRAFT.replace("待定:哥哥为何留着伞。", "TODO")))
|
||||
self.assertEqual(set(), self.codes(DRAFT))
|
||||
|
||||
def test_root_contract_accepts_clean_items(self) -> None:
|
||||
root_doc = self.write(
|
||||
"root.md",
|
||||
"""# 前期设计
|
||||
|
||||
## 一、根设定
|
||||
|
||||
- **作品前提:**主角刚结束高考,随后进入拥有星际机甲的陌生时代并寻找立足之地。
|
||||
- **披露要求:**前三章只呈现当前危机和能力征兆,终局答案留到后续阶段逐步揭开。
|
||||
|
||||
## 二、定盘
|
||||
""",
|
||||
def test_optional_ids_are_unique_but_have_no_frozen_bands(self) -> None:
|
||||
numbered = DRAFT.replace("哥哥每天", "- **S090|哥哥:** 哥哥每天").replace(
|
||||
"先从动作里表现关系。", "- **S003|表达:** 先从动作里表现关系。"
|
||||
)
|
||||
self.assertEqual([], validate_root(root_doc, self.config))
|
||||
self.assertEqual(set(), self.codes(numbered))
|
||||
self.assertIn("SETTING_ID_DUPLICATE", self.codes(numbered.replace("S003", "S090")))
|
||||
|
||||
def test_root_rejects_process_language(self) -> None:
|
||||
root_doc = self.write(
|
||||
"root.md",
|
||||
"""# 前期设计
|
||||
def test_missing_title_or_deep_heading_fails(self) -> None:
|
||||
self.assertIn("CANDIDATE_STRUCTURE", self.codes(DRAFT.replace("# 雨伞|作品探索\n", "")))
|
||||
self.assertIn("CANDIDATE_STRUCTURE", self.codes(DRAFT + "\n#### 额外层级\n说明。"))
|
||||
|
||||
## 一、根设定
|
||||
|
||||
- **代理要求:**Claude 子代理分别生成候选文档,再由主会话比较和选择最终方向。
|
||||
|
||||
## 二、定盘
|
||||
""",
|
||||
)
|
||||
self.assertIn("ROOT_PROCESS_LEAK", {problem.code for problem in validate_root(root_doc, self.config)})
|
||||
def test_missing_file_is_reported(self) -> None:
|
||||
_, problems = validate_candidates([self.root / "missing.md"])
|
||||
self.assertEqual(["CANDIDATE_FILE_UNREADABLE"], [problem.code for problem in problems])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user