# 本子格式与加载器 —— 从「手写 Markdown」到「可校验的数据」 > 2026-10-05。`schema/case.schema.yml` 定稿到 v1.0,`src/case.ts` 是它的加载器, > `jubensha_case` 是它在工具面上的出口。§四 记了两次被实测打回来修正的判断。 ## 一、要解决什么 设计方案 §3.4 第 1 条:「**本子只有我能写**」——三局的本子都是手写 Markdown, 谁想玩都得找作者写一本。 这句话底下有两层,第二层才是真问题: 1. **没有格式** —— 作者知道该写什么(那是三局攒出来的经验),但没写下来 2. **角色本靠手抄** —— DM 从手册里把每个人的角色本**抄**出来再发给玩家。 抄漏一句就是信息隔离破了,而抄的过程**没有任何检查** 第二层更危险:它不是效率问题,是**正确性**问题。 ## 二、schema v1.0:拿真实本子对出来的五处欠账 v0.1 是从第一局反推的,头部写着「还没经过第二局的检验」。三局跑完之后定稿的做法是 **把 case-03 的手册逐段填成 YAML**——填不下去的地方就是缺口。 填出来五处,全是**当年写本时临时加的段,从来没被收进 schema**: | 缺的段 | 它在实战里干什么 | 缺了会怎样 | |---|---|---| | `roles[*].now` | 角色**现在**在做什么活着 | 第三局玩家当场问「我人物背景呢?我不知道自己现在是在做什么啊」(§三) | | `roles[*].play` | 这件事让他**怎么说话** | AI 会照剧情陈述,而不是照一个有难处的人说话 | | `scene.premise` | 这一局的性质("今天不是来查案的") | 开场第一句的调子没地方写 | | `truth.after` | 事发之后又发生了什么(十年) | 情感本的整条主干无处安放 | | `briefing` / `style` / `audit` / `reveal` | 带局脚本、白描素材、监察职责、复盘脚本 | 每局都写,但每局格式都不一样 | **注意这五处都不是"可有可无的装饰"**:三局里每一局都在用,只是写在 Markdown 正文里。 **判据(可复用)**:**格式的缺口不是靠想出来的,是靠拿一份真实内容逐字段填出来的。** 写 schema 时容易只想着"结构应该长什么样",而漏掉"上次是怎么写的"。 ## 三、加载器:只把 `roles` 结构化 ```yaml roles: # 结构化 —— 它要按座位切成各自那一份,切口必须精确 - id: p0 private: … play: … truth: / clues: / briefing: … # 原样留着,按段名取 ``` **为什么不一并结构化**:真相、线索、带局脚本是给 DM 读的**整块文本**,拆开反而丢信息 (`briefing.limits` 里的「不催泪」和「不替玩家决定感受」并列着读才有意义)。 角色本不一样——它要按座位切,所以只有它需要精确的字段边界。 三个动作: | action | 给谁 | 返回什么 | |---|---|---| | `load` | DM | 座位表 + 有哪些段可 + **校验结果** | | `section` | DM | 某一段的原文(真相、线索、复盘脚本…) | | `book` | DM 转交玩家 | 某个角色的角色本文本,**原样** | `book` 的输出直接就是 `jubensha_player` 的 `roleBook` 参数——**不经过 DM 转述**。 工具的说明里写死了这一条:「不要自己转述,更不要把别的座位的发给他」。 ## 四、两次被实测打回来的判断 ### ① 凶手只能有一个出处 第一版按 v0.1 保留了 `roles[*].culprit`(布尔),同时 `truth.culprit` 也写着角色 id。 一个测试用例红了——`roles.find(role => role.culprit)` 找不到人。**红得对**:同一件事有两个 出处,就有不一致的那天。改成 `truth.culprit` 一处说了算,角色的 `culprit` 由加载器派生。 ### ② Team 名册跨进程持久 玩家的 teammate 名一开始直接用座位号,撞 `TEAM_MEMBER_NAME_TAKEN`。我当时的判断是 「名册不随玩家下桌释放,重启会释放」,于是加了进程内单调计数。 **重启后又撞了**。这次才对上:此前几次能成功,是因为那些名字(`p1` / `p2` / `p3`) **从来没被用过**——不是"重启释放了"。名册是**持久**的,计数器是**进程级**的。 改成自带唯一性的后缀(时间戳 + 随机三位)。名字只有机器用,人看的是 `description`。 **判据**:**当一个标识的"已用过"集合会持久,而你的去重手段是内存里的,那它迟早会撞** ——而且只在重启之后撞,看起来像随机故障。 ## 五、实测(2026-10-05,隔离实例) DM 照着本子让一位玩家上桌,四步全部走通: ``` jubensha_case {action:"load", dir:"…/tests/fixtures/case-03.yml"} → caseId "03"、title"没拆的那封信"、seats p0..p3 jubensha_case {action:"book", role:"p1"} → 林小满的完整角色本 jubensha_state {action:"start", caseId:"03", …} jubensha_player {action:"spawn", seat:"p1", name:"林小满", roleBook:"你是「林小满」。…"} → 子会话 5770058b 建立 ``` **关键证据是第三个调用里那段 `roleBook`**:它的开头是 `你是「林小满」。\n\n陈默的高中同学。…\n\n--- 你现在 ---\n…` ——正是 `roleBook()` 生成的排版,**一字不差地传了过去**。手抄环节被拿掉了。 ### 另一半:推理本 `case-03.yml` 是情感本。推理本专属的那几段——`truth.culprit`、`clues[*].supports`、 `truth.timeline`、`truth.misdecisions`——在它里面**全是空的**,也就是没被填过一遍。 2026-10-05 把 case-02《深夜电台》按同样的办法填成 `tests/fixtures/case-02.yml` (对着真实手册 `cases/02-深夜电台/dm-handbook.md` 逐字段填),**一条 issue 都没挑出来**。 再说一遍:**格式的缺口不是想出来的,是填出来的**——两次都填过,才敢说 schema 对这两类 本子都够用。 同一次实验里,这一份 fixture 还给 `jubensha_case action="clue"` 提供了第一份能跑的输入 (此前它只有单元测试)。取用时哪一段给谁、什么时候给,见 [`2026-10-05-真相保险箱.md`](2026-10-05-真相保险箱.md)。 ## 六、仍待办 1. **三个正式本子还没转成 YAML**。`cases/01`、`02`、`03` 仍是 Markdown(那是玩家看得懂、 DM 直接照着带的形式,本身没问题)。转不转是作者的选择——**转换只是为了用上校验**, 而 `tests/fixtures/` 下已经各有一份推理本与情感本证明过格式够用。 2. **校验只覆盖程序答得了的部分**。像「出口有没有修在动机的延长线上」这种要读懂本子才判断的, 一条都没写进检查——把它们写成机械检查只会得到假信号。