merge: 提示词问题修复四批次合入主干预理(A1-D3)

批次1 治理归一:硬编码系统提示收归发布资源包+架构守卫
批次2 身份与术语统一:身份句唯一归角色层、正典名规划员、JF-03销项
批次3 写手阻塞出口:输出合同oneOf二选一与生产/评测消费点分流
批次4 小模型鲁棒性:负向约束瘦身+governed池引文few-shot

终验:离线全量1155、全仓lint/type、模块边界、索引、全量数据库分片
(751通过;4项失败经main基线对照确认为存量环境问题)。
剩余:真实模型A/B实证待MUSE_REAL_MODEL_URL环境(方案附录已标记)。
This commit is contained in:
zizi 2026-09-19 23:04:55 +08:00
commit 9879e5e181
32 changed files with 554 additions and 79 deletions

View File

@ -16,10 +16,20 @@ description: 检查拆书台账、歧义与失败窗口,按窗口重算或以
## 操作步骤 ## 操作步骤
1. `python -m muse 研究 进度 <来源ID>` 或「拆书工作区」台账查看状态。 1. `python -m muse 研究 进度 <来源ID>` 或「拆书工作区」台账查看状态。
2. `python -m muse 研究 检查 <任务ID>` 查看完成窗、失败窗与歧义清单;歧义条目待「核对实体歧义」澄清,同名异型默认不合并。 2. `python -m muse 研究 检查 <任务ID>` 查看完成窗、失败窗与歧义清单;歧义条目只登记、由作者人工判定(核对步骤见下),同名异型默认不合并。
3. 失败窗重算:经恢复入口带 `retry_windows` 指定窗口;已完成窗不重跑,成本包络含重试余量。 3. 失败窗重算:经恢复入口带 `retry_windows` 指定窗口;已完成窗不重跑,成本包络含重试余量。
4. 来源新版本重建:直接重新发起拆解;旧版本分析保留可回查,版本身份不冲突。 4. 来源新版本重建:直接重新发起拆解;旧版本分析保留可回查,版本身份不冲突。
## 歧义人工核对
对歧义条目(同名不同类型的实体候选)逐项人工核对:
1. 列出各候选的出现窗与证据引文。
2. 依据上下文判断各候选是否同一实体;判断必须给出引文级依据。
3. 结论只能是三态之一:同一实体(合并并说明)、不同实体(分别命名区分)、证据不足(维持歧义)。
不因为同名就直接合并;类型冲突的同名默认不同实体;只处理参考作品内部身份判定,不跨作品合并、不改写本书事实;证据不足时维持歧义,不做倾向性猜测。
## 边界 ## 边界
- 只撤销本任务可证明归属的中间产物;不触碰外部确认与来源版本。 - 只撤销本任务可证明归属的中间产物;不触碰外部确认与来源版本。

View File

@ -9,7 +9,7 @@ roles:
explicitModelRequired: true explicitModelRequired: true
toolPolicy: readonly_tools toolPolicy: readonly_tools
planner: planner:
displayName: 规划师 displayName: 规划员
promptFile: .agent/角色/规划员.md promptFile: .agent/角色/规划员.md
modelPolicy: fixed modelPolicy: fixed
modelPolicyVersion: role-policy-r2-v2 modelPolicyVersion: role-policy-r2-v2
@ -116,7 +116,7 @@ roles:
<!-- /role-contract:judge --> <!-- /role-contract:judge -->
<!-- role-contract:planner --> <!-- role-contract:planner -->
## planner:规划师 ## planner:规划员
**责任**:承接设定初始化、作品规划和单章细纲等规划任务;一次调用只执行任务包指定的一个功能合同,产出可比较或可校验的 Shadow 草稿。 **责任**:承接设定初始化、作品规划和单章细纲等规划任务;一次调用只执行任务包指定的一个功能合同,产出可比较或可校验的 Shadow 草稿。

View File

@ -14,19 +14,14 @@
| 2 | C1 blocked 载荷改为**仅 reason**,不要求写手逐字引文举证;依据"写手纯写作负荷"原则(引文举证属检测/审阅职能) | 用户决策 2026-09-19 | | 2 | C1 blocked 载荷改为**仅 reason**,不要求写手逐字引文举证;依据"写手纯写作负荷"原则(引文举证属检测/审阅职能) | 用户决策 2026-09-19 |
| 3 | C1 评测侧消费点补齐:`逐例执行.py:935`、`语义检测.py:14` 直读 paragraphs,blocked 需在读取前分流;补 blocked 统计口径与合同哈希/回放影响评估项 | 复核 P1-2、P1-3 | | 3 | C1 评测侧消费点补齐:`逐例执行.py:935`、`语义检测.py:14` 直读 paragraphs,blocked 需在读取前分流;补 blocked 统计口径与合同哈希/回放影响评估项 | 复核 P1-2、P1-3 |
| 4 | A3(JF-03)引用面扩充:补 `检查与维护拆书结果/SKILL.md:19`、`文件设计/后端-B03-资料研究.md:25,179-186`;话术定位以 grep 为准,不按行号 | 复核 P2-1;用户决策=删除+话术改写 | | 4 | A3(JF-03)引用面扩充:补 `检查与维护拆书结果/SKILL.md:19`、`文件设计/后端-B03-资料研究.md:25,179-186`;话术定位以 grep 为准,不按行号 | 复核 P2-1;用户决策=删除+话术改写 |
| 5 | 新增范围外声明:其余孤儿模板不在本方案;`分析参考作品.md` 例外随批次 1 处置 | 复核 P2-2 | | 5 | 范围外声明修正:经开工前实测,报告所列其余孤儿模板已在早前批次删除,现存唯一孤儿为 `核对实体歧义.md`(批次 2 处置);台账/报告的孤儿清单属过时信息 | 复核 P2-2 + 开工实测 2026-09-19 |
| 6 | B3 身份句实为 **4 处**(补 `创作质量审阅.md:3`) | 复核 P2-3 | | 6 | B3 身份句实为 **4 处**(补 `创作质量审阅.md:3`) | 复核 P2-3 |
| 7 | 架构守卫正则须覆盖 `系统提示=(...)` 括号多行字面量形式 | 复核 P2-3 | | 7 | 架构守卫正则须覆盖 `系统提示=(...)` 括号多行字面量形式 | 复核 P2-3 |
| 8 | D2 判据下沉受写手纯度原则约束:判据优先落审阅/评委侧模板 | 决策 2 连带 | | 8 | D2 判据下沉受写手纯度原则约束:判据优先落审阅/评委侧模板 | 决策 2 连带 |
## 范围与范围外 ## 范围与范围外
**范围外(显式声明,避免误认提示词问题已全处置)**:以下孤儿模板归属各自专项批次/台账组,本方案不动—— **事实修正(2026-09-19 开工前实测)**:专项检查报告所列其余孤儿模板(`提取作品声音.md`、`诊断机器味.md`、`分析参考作品.md`、`交付/全书审阅.md`、`整理交付材料.md`)**已在早前批次删除**,仓内不复存在;台账与报告中的孤儿清单属过时信息。现存唯一孤儿为 `核对实体歧义.md`(批次 2 处置)。批次 1 无需"底稿复用"动作,`拆书分析.md` 直接新建。
- `审校修订/提示词/提取作品声音.md`、`诊断机器味.md`(审校修订+效果评测组台账)
- `交付/全书审阅.md`、`整理交付材料.md`(交付连载组台账,已登记但无代码消费者)
**例外**:`资料研究/提示词/分析参考作品.md` 与批次 1 新建的 `拆书分析.md` 同域同类,随批次 1 一并处置(见批次 1),避免同类双份残留。
## 事实基线(已机械核实) ## 事实基线(已机械核实)
@ -62,11 +57,6 @@
- 只保留行为指令(逐字引文、不补猜窗口外内容、不新增正文没有的设定等),原文语义不增不减;受限探索模板保留"探索完成后输出空 JSON 对象"等现有行为指令。 - 只保留行为指令(逐字引文、不补猜窗口外内容、不新增正文没有的设定等),原文语义不增不减;受限探索模板保留"探索完成后输出空 JSON 对象"等现有行为指令。
- 加载函数仿照 `写作任务.生成正文模板()` 返回 `(文本, sha256)`,供运行证据留痕;请求级冻结已由现有 `请求字节哈希 + 批准保留` 覆盖,不新增机制。 - 加载函数仿照 `写作任务.生成正文模板()` 返回 `(文本, sha256)`,供运行证据留痕;请求级冻结已由现有 `请求字节哈希 + 批准保留` 覆盖,不新增机制。
### 分析参考作品.md 处置(范围外声明的例外)
- 新建 `拆书分析.md` 时以 `分析参考作品.md` 为底稿改写(内容可对齐则复用其表达),改写完成后**删除原孤儿文件**;内容不可对齐则直接删除并在台账注明。
- 对应台账项(资料研究组孤儿模板)随本批次销项。
### D3 配置探针 ### D3 配置探针
- 保留代码内提示:探针与 `探针合同` 代码同构、64 token、无迭代需求;加豁免注释说明理由。 - 保留代码内提示:探针与 `探针合同` 代码同构、64 token、无迭代需求;加豁免注释说明理由。
@ -182,3 +172,25 @@ blocked: { reason: string(minLength 1), additionalProperties: false }
- **角色层冻结红线豁免范围(仅批次 2)**:角色合同.md 两处文字(displayName、章节标题);其余角色文件一律不动。 - **角色层冻结红线豁免范围(仅批次 2)**:角色合同.md 两处文字(displayName、章节标题);其余角色文件一律不动。
- **执行顺序刚性**:1 → 2 → 3 → 4(批次 2/3 依赖批次 1 的资源化落点;批次 4 依赖模板定稿)。 - **执行顺序刚性**:1 → 2 → 3 → 4(批次 2/3 依赖批次 1 的资源化落点;批次 4 依赖模板定稿)。
- **提交纪律**:每批次至少一次本地 commit 检查点;中文提交 `{类型}({模块}): {描述}`;合并回 main 前统一 Review。 - **提交纪律**:每批次至少一次本地 commit 检查点;中文提交 `{类型}({模块}): {描述}`;合并回 main 前统一 Review。
---
## 附录:负向约束审计表(批次4 C2/C3 执行记录,2026-09-19)
处置判据:**删**=已被机械保证(无工具请求 / additionalProperties:false / 解析层拒绝);**留**=行为性约束(schema 管不住)或预防性提示(降低小模型拒绝率,与 C3 同向)。
| 模板 | 约束 | 处置 | 依据 |
|---|---|---|---|
| 细化章节计划 | 不调用工具、不读取未授权正文 | 删 | 规划请求不带工具(机械不可为) |
| 生成作品规划 | 不执行工具、不读取其他作品、不输出正文 | 删 | 同上;输出合同无正文字段 |
| 生成作品规划 | 不添加…模型来源或用户确认字段 | 删 | additionalProperties:false 机械拒绝 |
| 创作质量审阅 | 不使用工具 | 删 | judge 请求无工具(机械保证) |
| 创作质量审阅 | 未登记字段 | 删 | additionalProperties:false |
| 全部模板 | 只返回约定 JSON 对象(含不添加解释/围栏变体) | 留 | 解析层机械拒绝,但提示预防拒绝成本更低 |
| 生成正文/修改选段/生成最小修订 | 不虚构冲突事实、不扩大圈选边界、retain不夹带改写等 | 留 | 行为性 |
| 连续性审阅/独立评审/文学评分 | 提示注入防护、盲化、引文保真、不提交位置哈希(带理由) | 留 | 行为性+预防性 |
| 受限探索 | 不要输出正文 | 留 | 探索合同 type:object 不限字段,非机械 |
统计:12 份任务模板逐条审计,删 5 处机械保证重复表述(写手模板零删除,其负向约束均为行为性);C3 为拆书分析/章后抽取/连续性审阅各加 4 行引文正反例(逐字合法 vs 截断/转述/自造 id 非法),writer/judge 模板不加示例。
**实证 A/B(needs_verification)**:本环境未配置 `MUSE_REAL_MODEL_URL`,governed 池小模型改动前后 schema 拒绝率与引文对齐通过率的 A/B 比较未执行;待真实模型环境补验,无改善则回滚对应模板文本(重新登记旧文本即可)。

View File

@ -25,6 +25,7 @@
- **位置时效**:条目位置为 2026-09-16 检查时快照。优化执行重组了提示词、模块与测试文件结构,**路径一律以当前仓库为准**;本台账不逐条追改路径。 - **位置时效**:条目位置为 2026-09-16 检查时快照。优化执行重组了提示词、模块与测试文件结构,**路径一律以当前仓库为准**;本台账不逐条追改路径。
- **对象退场**:`数据库/旧库迁移/` 工具链及其用例已于 2026-09-18 整体退场(提交 `9712589`),`LM-01`~`LM-07` 与位置引用 `tests/迁移/test_PG作品正文迁移.py`、`test_作品资产迁移.py`、`test_混合知识分流.py`、`test_隔离目标台账.py` 等已删文件的条目(`TS-20`、`TS-50`、`TS-55`、`TS-61`、`TS-62`)失去核查对象,保留为历史发现。 - **对象退场**:`数据库/旧库迁移/` 工具链及其用例已于 2026-09-18 整体退场(提交 `9712589`),`LM-01`~`LM-07` 与位置引用 `tests/迁移/test_PG作品正文迁移.py`、`test_作品资产迁移.py`、`test_混合知识分流.py`、`test_隔离目标台账.py` 等已删文件的条目(`TS-20`、`TS-50`、`TS-55`、`TS-61`、`TS-62`)失去核查对象,保留为历史发现。
- **过程材料留存口径**:14 份模块报告(`专项检查-报告-*.md`)与 `/tmp` 探针日志均为过程材料,受 `.gitignore` 忽略、不入版本库;台账中指向它们的路径仅在本地可读时有效。 - **过程材料留存口径**:14 份模块报告(`专项检查-报告-*.md`)与 `/tmp` 探针日志均为过程材料,受 `.gitignore` 忽略、不入版本库;台账中指向它们的路径仅在本地可读时有效。
- **JF-03 处置(2026-09-19)**:经用户授权采纳删除方案——`src/muse/资料研究/提示词/核对实体歧义.md` 已删除(未登记、运行时不加载、承诺不存在的任务);研究命令与「检查与维护拆书结果」技能话术改为“歧义只登记,由作者人工判定”,人工核对步骤承接进该技能;`实体归并.py` 话术检查时已合规。原行保留为历史快照。
## 0. 汇总 ## 0. 汇总

View File

@ -42,13 +42,13 @@
| 分章切窗.py | 分章、窗口划分及输入漂移 | | 分章切窗.py | 分章、窗口划分及输入漂移 |
| 榜单快照.py | 来源、时间、质量、排名及观察差异 | | 榜单快照.py | 来源、时间、质量、排名及观察差异 |
| 样本选题.py | 样本比较、选题依据及选书交接 | | 样本选题.py | 样本比较、选题依据及选书交接 |
| 拆书分析.py | 当前逐窗分析的内联系统提示、输出合同与执行;提取章节、阶段、实体及写法 | | 拆书分析.py | 逐窗分析的任务模板(发布包登记)、输出合同与执行;提取章节、阶段、实体及写法 |
| 提示词/拆书分析.md | 逐窗拆书分析任务模板;随发布包登记加载,不含身份句 |
| 实体归并.py | 参考作品内专名、别名及歧义 | | 实体归并.py | 参考作品内专名、别名及歧义 |
| 分析版本.py | 完整模型成果登记、旧检查点承接及运行摘要 | | 分析版本.py | 完整模型成果登记、旧检查点承接及运行摘要 |
| 分析保存.py | 分析不可变存储合同、S04校验与S01未决旧卡写入 | | 分析保存.py | 分析不可变存储合同、S04校验与S01未决旧卡写入 |
| 提示词/核对实体歧义.md | 作者人工核对歧义的参考说明;未接入自动核对任务 |
当前文件以[资料研究源码](../../../../src/muse/资料研究/)为准;原[目标文件清单](../项目目录与文件职责.md)及[文件合同](../文件设计/后端-B03-资料研究.md)保留历史设计目标,未消费的分析模板已退出。 当前文件以[资料研究源码](../../../../src/muse/资料研究/)为准;原[目标文件清单](../项目目录与文件职责.md)及[文件合同](../文件设计/后端-B03-资料研究.md)保留历史设计目标,未消费的分析模板与人工歧义核对说明(核对实体歧义.md)已退出,歧义只登记、由作者人工判定。
## 实现约束 ## 实现约束

View File

@ -35,6 +35,8 @@ def 探针请求(任务ID: str, 角色: str, 内容: 运行配置内容, 策略
provider=声明["provider"], provider=声明["provider"],
model=声明["model"], model=声明["model"],
thinking=声明["thinking"], thinking=声明["thinking"],
# 提示词治理豁免:探针提示与探针合同同构、64 token 固定、无迭代需求,
# 保留代码内字面量;治理白名单见 tests/架构/test_提示词治理.py。
系统提示="你是运行配置探针;只输出结构要求的 JSON,不解释。", 系统提示="你是运行配置探针;只输出结构要求的 JSON,不解释。",
用户输入='请输出 JSON:{"ok": true}', 用户输入='请输出 JSON:{"ok": true}',
输出合同=探针合同, 输出合同=探针合同,

View File

@ -1,8 +1,8 @@
# 生成作品规划 # 生成作品规划
你是规划师。本次只生成输入指定的一个规划对象,输出必须满足任务给定的 JSON Schema。 本次只生成输入指定的一个规划对象,输出必须满足任务给定的 JSON Schema。
- 使用冻结材料中的目录、作者要求和允许模型读取的规划基线。方法材料是写作参考,不是本书事实。 - 使用冻结材料中的目录、作者要求和允许模型读取的规划基线。方法材料是写作参考,不是本书事实。
- 保留作者已经确定的方向;引用只能使用材料中可核对的本书身份与版本。材料不足时不得虚构实体、来源身份或确认决定。 - 保留作者已经确定的方向;引用只能使用材料中可核对的本书身份与版本。材料不足时不得虚构实体、来源身份或确认决定。
- 不执行工具、不读取其他作品、不输出正文,不把候选宣称为已确认规划。 - 不把候选宣称为已确认规划。
- 只输出约定 JSON 对象,不添加解释、Markdown 围栏、模型来源或用户确认字段。 - 只输出约定 JSON 对象,不添加解释或 Markdown 围栏。

View File

@ -1,8 +1,8 @@
# 章级细纲 # 章级细纲
你是规划师。本次只细化输入指定的一章的章级细纲,输出满足任务给定的 JSON Schema。 本次只细化输入指定的一章的章级细纲,输出满足任务给定的 JSON Schema。
- 目标章号以冻结目录为准,不改章节身份。已确认的上层方向和作者要求优先。 - 目标章号以冻结目录为准,不改章节身份。已确认的上层方向和作者要求优先。
- 将冲突、出场、事件方向和章末承接组织成可执行的章级细纲,不输出正文。 - 将冲突、出场、事件方向和章末承接组织成可执行的章级细纲,不输出正文。
- 方法卡仅用于选择组织方式,不得将参考作品的实体和情节写成本书事实;不能虚构引用身份或来源版本。 - 方法卡仅用于选择组织方式,不得将参考作品的实体和情节写成本书事实;不能虚构引用身份或来源版本。
- 不调用工具、不读取未授权正文,不代作者确认。只返回约定 JSON 对象。 - 不代作者确认,只返回约定 JSON 对象。

View File

@ -1,14 +1,14 @@
# 本书文学片审阅 # 本书文学片审阅
你是受控审阅角色。输入包含固定背景、已确认声音(可能没有)、完整片段语境与核心区间;不使用工具,不改写正文。 输入包含固定背景、已确认声音(可能没有)、完整片段语境与核心区间;不改写正文。
- 对continuity(连续性)、craft(创作质量)、voice(声音)分别给出判断,不合并成一个文学通过分。 - 对continuity(连续性)、craft(创作质量)、voice(声音)分别给出判断,不合并成一个文学通过分。
- continuity依据本次允许的时点事实核对因果、视角和已知条件。没有确切外部依据时不能断言事实冲突,返回insufficient_evidence或ask。 - continuity依据本次允许的时点事实核对因果、视角和已知条件。没有确切外部依据时不能断言事实冲突,返回insufficient_evidence(依据不足)或ask(追问)。
- craft关注人物行动、冲突、节奏、情绪与场景作用,解释具体阅读效果。不要仅凭禁词、比喻或句型命中要求修改。 - craft关注人物行动、冲突、节奏、情绪与场景作用,解释具体阅读效果。不要仅凭禁词、比喻或句型命中要求修改。
- voice只比较已确认指导;没有已确认声音则状态为uncalibrated,不给reviewed或repair。 - voice只比较已确认指导;没有已确认声音则状态为uncalibrated(未标定),不给reviewed(已审阅)或repair(修订)。
- 每个问题只从本片核心区起点定位,可延伸到本片语境结尾。start/end使用全文Unicode码点左闭右开区间,quote必须逐字来自该位置。重复文字不默认选择第一次出现。 - 每个问题只从本片核心区起点定位,可延伸到本片语境结尾。start/end使用全文Unicode码点左闭右开区间,quote必须逐字来自该位置。重复文字不默认选择第一次出现。
- judgment为repair、retain或ask。有意表达可retain,未知信号可ask,零问题是合法结果。缺依据或未标定不得输出repair。 - judgment(判定)为repair、retain(保留)或ask。有意表达可retain,未知信号可ask,零问题是合法结果。缺依据或未标定不得输出repair。
- basis_ids只能引用输入允许的依据ID。不要创造参考资料、人物事实或来源身份。 - basis_ids只能引用输入允许的依据ID。不要创造参考资料、人物事实或来源身份。
- reviewed表示本维给出了一次判断,不代表文学收益经过实验验证。禁止输出作者身份概率、检测器规避分数、自动采纳或伪造运行完成声明。 - reviewed表示本维给出了一次判断,不代表文学收益经过实验验证。禁止输出作者身份概率、检测器规避分数、自动采纳或伪造运行完成声明。
只输出固定JSON合同,不另附正文改写、说明前后缀或未登记字段。 只输出固定JSON合同,不另附正文改写或说明前后缀。

View File

@ -7,3 +7,8 @@
逐项覆盖required_assertions与required_constraints;每项引用对应assertion或constraint依据及候选原字,并说明pass、fail或unknown。缺证据或无法判断时使用unknown,不为凑通过推断。候选内部的视角、因果或引文问题可引用candidate;对外部事实的判断仍须有相应资料。 逐项覆盖required_assertions与required_constraints;每项引用对应assertion或constraint依据及候选原字,并说明pass、fail或unknown。缺证据或无法判断时使用unknown,不为凑通过推断。候选内部的视角、因果或引文问题可引用candidate;对外部事实的判断仍须有相应资料。
发现说明事实、视角、因果、引文、硬约束或连续性问题及严重度;各局部身份唯一。新设定建议只关联本次declared_new或unknown陈述,保持未决。不要提交原文位置、哈希、模型名、任务号或回执;代码会从实际引文派生位置和身份。 发现说明事实、视角、因果、引文、硬约束或连续性问题及严重度;各局部身份唯一。新设定建议只关联本次declared_new或unknown陈述,保持未决。不要提交原文位置、哈希、模型名、任务号或回执;代码会从实际引文派生位置和身份。
## 引文示例
- 合法:candidate_quote 逐字来自候选全文;evidence_refs 只用输入已给的依据id(如 "candidate"、"e1")。
- 非法:candidate_quote 截断或转述、evidence_refs 自造id(如 "guess-1")——逐字定位与依据白名单校验都会拒绝。

View File

@ -150,7 +150,7 @@ def 运行研究命令(装配: 应用装配, 动作: str, 目标: str | None) ->
if 结果 if 结果
else ("history_not_imported" if 快照.状态.value == "completed" else "in_progress") else ("history_not_imported" if 快照.状态.value == "completed" else "in_progress")
) )
状态["说明"] = "歧义条目待核对实体歧义任务澄清;失败窗可经重跑窗恢复。" 状态["说明"] = "歧义条目只登记,由作者人工判定;失败窗可经重跑窗恢复。"
return 状态 return 状态
raise 配置错误(f"未知研究命令动作:{动作}") raise 配置错误(f"未知研究命令动作:{动作}")

View File

@ -11,6 +11,8 @@ from muse.正式变更.接口 import 固定哈希
def 读取检测材料(conn, spec, writer): def 读取检测材料(conn, spec, writer):
if writer is None or len(spec["dependencies"]) != 1: if writer is None or len(spec["dependencies"]) != 1:
raise 评测错误("语义检测缺少单份实际生成交付") raise 评测错误("语义检测缺少单份实际生成交付")
if "blocked" in (writer["output"] or {}):
raise 评测错误("语义检测依赖的写手声明阻塞,无正文可检")
text = "\n".join(p["text"] for p in writer["output"]["paragraphs"]) text = "\n".join(p["text"] for p in writer["output"]["paragraphs"])
return 组装语义检测材料(text, 读取文学依据(conn, spec)) return 组装语义检测材料(text, 读取文学依据(conn, spec))

View File

@ -930,6 +930,8 @@ def _比较材料(conn, spec, writers):
return 组装成对材料(left, right, tuple(spec["dimensions"])) return 组装成对材料(left, right, tuple(spec["dimensions"]))
if len(writers) != 2 or any(w is None for w in writers): if len(writers) != 2 or any(w is None for w in writers):
raise 评测错误("成对比较缺少两侧真实生成交付") raise 评测错误("成对比较缺少两侧真实生成交付")
if any("blocked" in (w["output"] or {}) for w in writers):
raise 评测错误("成对比较的写手声明阻塞,本单元无正文可比")
if spec.get("comparison_contract") not in {None, "quote-only-v2", "writer-rubric-v1"}: if spec.get("comparison_contract") not in {None, "quote-only-v2", "writer-rubric-v1"}:
raise 评测错误("成对比较的冻结合同版本未知") raise 评测错误("成对比较的冻结合同版本未知")
texts = ["\n".join(p["text"] for p in w["output"]["paragraphs"]) for w in writers] texts = ["\n".join(p["text"] for p in w["output"]["paragraphs"]) for w in writers]

View File

@ -143,6 +143,10 @@ _写作输出合同 = {
"type": "object", "type": "object",
"properties": { "properties": {
"paragraphs": { "paragraphs": {
"description": (
"正文段落数组;每项 text 是完整段落文本,不能包含 LF 或 CR 换行符,"
"需要另起一段时另建一个段落对象;系统不改写或规范化段内换行。"
),
"type": "array", "type": "array",
"minItems": 1, "minItems": 1,
"items": { "items": {
@ -153,9 +157,19 @@ _写作输出合同 = {
"required": ["text"], "required": ["text"],
"additionalProperties": False, "additionalProperties": False,
}, },
}
}, },
"required": ["paragraphs"], "blocked": {
"description": (
"写作死结出口:仅当细纲与前置事实存在不可化解冲突"
"(任何写法都必然虚构冲突事实)时输出,reason 一句话说明。"
),
"type": "object",
"properties": {"reason": {"type": "string", "minLength": 1}},
"required": ["reason"],
"additionalProperties": False,
},
},
"oneOf": [{"required": ["paragraphs"]}, {"required": ["blocked"]}],
"additionalProperties": False, "additionalProperties": False,
} }

View File

@ -1,6 +1,6 @@
# 生成正文(正文写作 · 无工具写作模板) # 生成正文(正文写作 · 无工具写作模板)
你是本作品的写手。你收到的是一次冻结装配的写作材料: 你收到的是一次冻结装配的写作材料:
- 目标章细纲投影(目标、硬约束、关键事件、必出场对象、章末钩子); - 目标章细纲投影(目标、硬约束、关键事件、必出场对象、章末钩子);
- 截止时点的历史正文投影与事实投影(如装配入选); - 截止时点的历史正文投影与事实投影(如装配入选);
@ -11,15 +11,13 @@
1. 只使用装配材料中的设定与事实;不得虚构与材料冲突的事实。 1. 只使用装配材料中的设定与事实;不得虚构与材料冲突的事实。
2. 严格覆盖细纲关键事件的顺序与结果方向;必出场对象必须实际出场。 2. 严格覆盖细纲关键事件的顺序与结果方向;必出场对象必须实际出场。
3. 用戏剧化叙述展开,禁止把细纲措辞原样搬进正文。 3. 用戏剧化叙述展开(用动作、对白与细节呈现事件,不复述或概括事件),禁止把细纲措辞原样搬进正文。
4. 按当前场景和人物选择适用的已确认声音指导,保持作者认可的表达;口癖只在合适语境使用,不强塞到每句。 4. 按当前场景和人物选择适用的已确认声音指导,保持作者认可的表达;口癖只在合适语境使用,不强塞到每句。
5. 依据当前文本判断声音偏移;保护表达用于保真,不要求新造或照抄剧情。人物事实与细纲仍约束声音指导的使用。 5. 依据当前文本判断声音偏移;保护表达用于保真,不要求新造或照抄剧情。人物事实与细纲仍约束声音指导的使用。
6. 词表命中只作观察线索,判断是否偏移须给出文本依据,不能直接判为缺陷。 6. 词表命中只作观察线索,判断是否偏移须给出文本依据,不能直接判为缺陷。
7. 输出只包含正文段落,不含大纲、说明、注释或元信息。 7. 输出只包含正文段落,不含大纲、说明、注释或元信息。
## 输出 ## 输出与阻塞
输出一个 JSON 对象:`{"paragraphs": [{"text": "段落文本"}, ...]}`。 - 正常输出一个 JSON 对象:`{"paragraphs": [{"text": "段落文本"}, ...]}`;段落顺序即正文顺序,不输出段落标题。
- 仅当细纲与前置事实材料存在不可化解的死结(任何写法都必然虚构冲突事实)时,不写正文,输出 `{"blocked": {"reason": "一句话说明"}}`;不用它回避普通难度。
- 每项 `text` 是完整段落文本,不能包含 LF 或 CR 换行符;需要另起一段时另建一个段落对象。系统不会改写或规范化段内换行。
- 段落顺序即正文顺序;不输出段落标题。

View File

@ -0,0 +1,3 @@
# 受限探索(编排 · 写手探索阶段模板)
当前处于受限探索阶段:使用提供的只读工具了解目标章细纲、截止时点事实与历史正文,为写作收集必要材料;不要输出正文,探索完成后输出一个空 JSON 对象。

View File

@ -0,0 +1,8 @@
# 章后抽取(编排 · 定稿章知识抽取模板)
对刚定稿的一章做章后抽取:给出续写所需的本章摘要与候选事实;每条事实必须附正文原句引文,不新增正文没有的设定;输出一个 JSON 对象。
## 引文示例
- 合法:quote 逐字复制正文原句,如正文含「灯芯在风里矮下去」,quote 写「灯芯在风里矮下去」。
- 非法:quote 多字(「灯芯在风里矮了下去」)、截断(「灯芯在风里…」)或转述(「灯芯被风吹矮」)——后端逐字定位失败即拒绝。

View File

@ -78,6 +78,17 @@ _写作输出合同 = 写作输出合同()
_探索输出合同 = {"type": "object"} _探索输出合同 = {"type": "object"}
def 受限探索模板() -> tuple[str, str]:
"""读取发布包登记的受限探索模板及实际字节哈希。
角色身份与输出合同由调用方装配;模板只含探索行为指令,不重复自我介绍。
"""
from muse.资源加载 import 打开资源
data = 打开资源("正文/受限探索.md")
return data.decode("utf-8"), hashlib.sha256(data).hexdigest()
class 生成正文错误(Muse错误): class 生成正文错误(Muse错误):
"""旅程编排内的明确失败;错误说明面向作者呈现。""" """旅程编排内的明确失败;错误说明面向作者呈现。"""
@ -201,11 +212,7 @@ def 登记生成正文(登记: 流程登记, 装配, 计价) -> None:
{"name": d.名称, "description": d.说明, "parameters": d.参数合同} {"name": d.名称, "description": d.说明, "parameters": d.参数合同}
for d in 工具.工具.values() for d in 工具.工具.values()
) )
探索提示 = ( 探索提示, 探索模板哈希 = 受限探索模板()
"你是本作品的写手,当前处于受限探索阶段。"
"使用提供的只读工具了解目标章细纲、截止时点事实与历史正文,"
"为写作收集必要材料;不要输出正文,探索完成后输出一个空 JSON 对象。"
)
for _ in range(轮次上限): for _ in range(轮次上限):
请求 = _写手请求( 请求 = _写手请求(
装配, 装配,
@ -251,6 +258,7 @@ def 登记生成正文(登记: 流程登记, 装配, 计价) -> None:
检查点={ 检查点={
"清单": [asdict(项) for 项 in 清单], "清单": [asdict(项) for 项 in 清单],
"绑定": [asdict(绑) for 绑 in 绑定集.values()], "绑定": [asdict(绑) for 绑 in 绑定集.values()],
"模板哈希": 探索模板哈希,
}, },
) )
@ -377,6 +385,10 @@ def 登记生成正文(登记: 流程登记, 装配, 计价) -> None:
), ),
) )
) )
# 写作死结出口:blocked 载荷已随模型结构化交付留痕(含 reason);
# 此处不登记方法消费、不产出候选,以编排内明确失败把理由呈现给作者。
if "blocked" in 交付.内容:
raise 生成正文错误(f"写手声明写作死结:{交付.内容['blocked']['reason']}")
# 只有真实写作交付后才登记消费,并关联模型调用;冻结不自动产生消费。 # 只有真实写作交付后才登记消费,并关联模型调用;冻结不自动产生消费。
from muse.知识方法.接口 import 消费输入, 记录消费 from muse.知识方法.接口 import 消费输入, 记录消费
@ -631,6 +643,7 @@ __all__ = [
"流程定义_生成正文", "流程定义_生成正文",
"生成正文步骤", "生成正文步骤",
"生成正文模板", "生成正文模板",
"受限探索模板",
"流程身份", "流程身份",
"流程版本", "流程版本",
"探索工具集", "探索工具集",

View File

@ -316,5 +316,5 @@ def 发起生成规划(装配, 身份, 命令ID: str, 请求: 规划保存, 要
任务ID = 装配.任务运行.创建任务于(连, 任务, 流程身份, 流程版本) 任务ID = 装配.任务运行.创建任务于(连, 任务, 流程身份, 流程版本)
配置 = 配置版本管理(库).冻结到任务于(连, 任务ID, 配置ID) 配置 = 配置版本管理(库).冻结到任务于(连, 任务ID, 配置ID)
if "planner" not in 配置.内容.角色配置: if "planner" not in 配置.内容.角色配置:
raise 作品错误("CONFIG_INVALID", "选定配置没有规划师角色") raise 作品错误("CONFIG_INVALID", "选定配置没有规划员角色")
return {"task_id": 任务ID} return {"task_id": 任务ID}

View File

@ -55,6 +55,17 @@ class 章后处理错误(Muse错误):
错误码 = "POST_FLOW_INVALID" 错误码 = "POST_FLOW_INVALID"
def 章后抽取模板() -> tuple[str, str]:
"""读取发布包登记的章后抽取模板及实际字节哈希。
角色身份与输出合同由调用方装配;模板只含抽取行为指令,不重复自我介绍。
"""
from muse.资源加载 import 打开资源
data = 打开资源("抽取/章后抽取.md")
return data.decode("utf-8"), hashlib.sha256(data).hexdigest()
_抽取输出合同 = { _抽取输出合同 = {
"type": "object", "type": "object",
"properties": { "properties": {
@ -159,16 +170,13 @@ def 登记章后处理(登记: 流程登记, 装配, 计价, *, 抽取配置ID:
from muse.任务运行.接口 import 配置版本管理 from muse.任务运行.接口 import 配置版本管理
声明 = 配置版本管理(装配.要求数据库()).读取任务绑定(任务.任务ID).内容.角色配置["extractor"] 声明 = 配置版本管理(装配.要求数据库()).读取任务绑定(任务.任务ID).内容.角色配置["extractor"]
模板, 模板哈希 = 章后抽取模板()
请求 = 模型请求( 请求 = 模型请求(
调用ID=str(uuid4()), 调用ID=str(uuid4()),
provider=声明["provider"], provider=声明["provider"],
model=声明["model"], model=声明["model"],
thinking=声明.get("thinking"), thinking=声明.get("thinking"),
系统提示=( 系统提示=模板,
"你是本作品的抽取员,对刚定稿的一章做章后抽取:"
"给出续写所需的本章摘要与候选事实;每条事实必须附正文原句引文,"
"不新增正文没有的设定;输出一个 JSON 对象。"
),
用户输入=json.dumps( 用户输入=json.dumps(
{ {
"chapter_id": 来源["chapter_id"], "chapter_id": 来源["chapter_id"],
@ -199,7 +207,12 @@ def 登记章后处理(登记: 流程登记, 装配, 计价, *, 抽取配置ID:
交付 = asyncio.run(执行器.执行(上下文, 请求, 阶段="抽取", 原文授权ID=授权)) 交付 = asyncio.run(执行器.执行(上下文, 请求, 阶段="抽取", 原文授权ID=授权))
return 步骤结果( return 步骤结果(
输出={"摘要字数": len(交付.内容["summary"]), "事实数": len(交付.内容["facts"])}, 输出={"摘要字数": len(交付.内容["summary"]), "事实数": len(交付.内容["facts"])},
检查点={"抽取": 交付.内容, "调用ID": 交付.调用ID, "证据回执": 交付.证据回执}, 检查点={
"抽取": 交付.内容,
"调用ID": 交付.调用ID,
"证据回执": 交付.证据回执,
"模板哈希": 模板哈希,
},
) )
def 登记事实提案(上下文: 执行上下文) -> 步骤结果: def 登记事实提案(上下文: 执行上下文) -> 步骤结果:
@ -438,6 +451,7 @@ __all__ = [
"查询章后状态", "查询章后状态",
"事实批内去重", "事实批内去重",
"章后处理错误", "章后处理错误",
"章后抽取模板",
"流程身份", "流程身份",
"流程版本", "流程版本",
"处理器版本集", "处理器版本集",

View File

@ -64,11 +64,17 @@ from muse.资料研究.模型 import 资料错误
"additionalProperties": False, "additionalProperties": False,
} }
系统提示 = (
"你是参考作品分析员,对给定窗口做拆书分析:" def 拆书分析模板() -> tuple[str, str]:
"列出章节与情节节拍、叙事阶段、出现的人物/地点/事物实体(附原文引文证据)与可借鉴的写法。" """读取发布包登记的拆书分析模板及实际字节哈希。
"只依据窗口原文,不补猜窗口外内容;输出一个 JSON 对象。"
) 角色身份与输出合同由调用方装配;模板只含分析行为指令,不重复自我介绍。
模板字节已冻结进请求哈希(批准保留留痕),不再单独落库。
"""
from muse.资源加载 import 打开资源
data = 打开资源("抽取/拆书分析.md")
return data.decode("utf-8"), hashlib.sha256(data).hexdigest()
def 执行窗口提取( def 执行窗口提取(
@ -88,12 +94,13 @@ def 执行窗口提取(
from muse.任务运行.接口 import 配置版本管理 from muse.任务运行.接口 import 配置版本管理
声明 = 配置版本管理(工厂).读取任务绑定(上下文.任务.任务ID).内容.角色配置["extractor"] 声明 = 配置版本管理(工厂).读取任务绑定(上下文.任务.任务ID).内容.角色配置["extractor"]
模板, _模板哈希 = 拆书分析模板()
请求 = 模型请求( 请求 = 模型请求(
调用ID=str(uuid4()), 调用ID=str(uuid4()),
provider=声明["provider"], provider=声明["provider"],
model=声明["model"], model=声明["model"],
thinking=声明.get("thinking"), thinking=声明.get("thinking"),
系统提示=系统提示, 系统提示=模板,
用户输入=json.dumps( 用户输入=json.dumps(
{ {
"source_id": 拆书输入.get("source_id"), "source_id": 拆书输入.get("source_id"),
@ -132,4 +139,4 @@ def 执行窗口提取(
return 内容 return 内容
__all__ = ["窗口输出合同", "系统提示", "执行窗口提取", "资料错误"] __all__ = ["窗口输出合同", "拆书分析模板", "执行窗口提取", "资料错误"]

View File

@ -0,0 +1,8 @@
# 拆书分析(资料研究 · 参考作品逐窗分析模板)
对给定窗口做拆书分析:列出章节与情节节拍、叙事阶段、出现的人物/地点/事物实体(附原文引文证据)与可借鉴的写法。只依据窗口原文,不补猜窗口外内容;输出一个 JSON 对象。
## 引文示例
- 合法:实体 evidence 逐字摘自窗口原文,如窗口含「旧码头的雾灯亮了」,evidence 写「旧码头的雾灯亮了」。
- 非法:evidence 截断(「旧码头的雾…」)或转述(「雾灯亮起」)——不是原文逐字,属无效证据。

View File

@ -1,17 +0,0 @@
# 核对实体歧义 · 人工核对说明
歧义只登记,由作者人工判定。以下是人工查看证据时的参考步骤,尚无自动语义核对任务或自动合并入口。
## 核对步骤
对拆书归并中登记的歧义条目(同名不同类型的实体候选)逐项核对:
1. 列出各候选的出现窗与证据引文。
2. 依据上下文判断各候选是否同一实体;判断必须给出引文级依据。
3. 结论只能是三态之一:同一实体(合并并说明)、不同实体(分别命名区分)、证据不足(维持歧义)。
## 合同
- 不因为同名就直接合并;类型冲突的同名默认不同实体。
- 只处理参考作品内部身份判定;不跨作品合并,不改写本书事实。
- 证据不足时维持歧义,不得输出倾向性猜测。

View File

@ -0,0 +1,46 @@
"""三条收归链路装配后的系统提示:角色层唯一身份 + 资源模板 + 动态合同。
保护:批次1治理归一的目标语义——探索/章后抽取/拆书分析的系统提示
全部来自登记资源模板,装配后身份句只出现在角色层(模板不重复自我介绍)。
"""
from __future__ import annotations
import json
import pytest
from muse.任务运行.角色会话 import 组装系统提示
from muse.任务运行.角色策略 import 角色策略目录
from muse.编排.生成正文 import _探索输出合同, 受限探索模板
from muse.编排.章后处理 import _抽取输出合同, 章后抽取模板
from muse.资料研究.拆书分析 import 拆书分析模板, 窗口输出合同
from muse.资源加载 import 使用资源提供器
链路集 = (
("writer", 受限探索模板, _探索输出合同),
("extractor", 章后抽取模板, _抽取输出合同),
("extractor", 拆书分析模板, 窗口输出合同),
)
@pytest.mark.case_id("NC-prompt-assembly-single-identity")
@pytest.mark.parametrize(
"角色,读取,合同", 链路集, ids=("explore", "chapter-extract", "book-analysis")
)
def test_装配后身份唯一且模板无身份句(源码资源, 角色, 读取, 合同):
with 使用资源提供器(源码资源):
目录 = 角色策略目录.从发布包()
角色提示 = 目录.读取角色提示(角色)
模板, 模板哈希 = 读取()
组装 = 组装系统提示(目录, 角色, 模板, 合同)
assert 组装 == (
角色提示
+ "\n\n本次任务:\n"
+ 模板
+ "\n\n最终输出结构:\n"
+ json.dumps(合同, ensure_ascii=False, sort_keys=True)
)
assert "你是" not in 模板, "任务模板不得重复角色层自我介绍"
assert 组装.count("你是") == 角色提示.count("你是")
assert len(模板哈希) == 64

View File

@ -1,4 +1,4 @@
"""规划与文学审校仅消费发布资源;发现技能不等于全部运行能力。""" """规划、审校与编排/资料研究链路仅消费发布资源;发现技能不等于全部运行能力。"""
import hashlib import hashlib
import json import json
@ -10,13 +10,19 @@ import pytest
from muse import 资源加载 from muse import 资源加载
from muse.共享.错误 import 资源缺失错误 from muse.共享.错误 import 资源缺失错误
from muse.审校修订.完整审校 import 文学模板 from muse.审校修订.完整审校 import 文学模板
from muse.编排.生成正文 import 受限探索模板
from muse.编排.生成规划 import 规划模板 from muse.编排.生成规划 import 规划模板
from muse.编排.章后处理 import 章后抽取模板
from muse.资料研究.拆书分析 import 拆书分析模板
from muse.资源加载 import 使用资源提供器, 安装包资源提供器, 读取能力 from muse.资源加载 import 使用资源提供器, 安装包资源提供器, 读取能力
模板集 = ( 模板集 = (
(lambda: 规划模板("fine_outline"), "规划/细化章节计划.md"), (lambda: 规划模板("fine_outline"), "规划/细化章节计划.md"),
(lambda: 规划模板("outline"), "规划/生成作品规划.md"), (lambda: 规划模板("outline"), "规划/生成作品规划.md"),
(文学模板, "审校/创作质量审阅.md"), (文学模板, "审校/创作质量审阅.md"),
(受限探索模板, "正文/受限探索.md"),
(章后抽取模板, "抽取/章后抽取.md"),
(拆书分析模板, "抽取/拆书分析.md"),
) )
@ -38,7 +44,11 @@ def _安装资源(tmp_path, monkeypatch, 名称, 内容, *, 缺文件=False, 错
@pytest.mark.case_id("NC-prompt-package-content-and-hash") @pytest.mark.case_id("NC-prompt-package-content-and-hash")
@pytest.mark.parametrize("读取,名称", 模板集, ids=("fine-plan", "work-plan", "literary")) @pytest.mark.parametrize(
"读取,名称",
模板集,
ids=("fine-plan", "work-plan", "literary", "explore", "chapter-extract", "book-analysis"),
)
def test_规划审校无源树从发布包读相同内容与哈希(tmp_path, monkeypatch, 读取, 名称): def test_规划审校无源树从发布包读相同内容与哈希(tmp_path, monkeypatch, 读取, 名称):
内容 = "仅存在于当前发布包的测试提示词。".encode() 内容 = "仅存在于当前发布包的测试提示词。".encode()
_安装资源(tmp_path, monkeypatch, 名称, 内容) _安装资源(tmp_path, monkeypatch, 名称, 内容)
@ -56,7 +66,11 @@ def test_规划审校无源树从发布包读相同内容与哈希(tmp_path, mon
@pytest.mark.case_id("NC-prompt-package-missing-or-corrupt-rejected") @pytest.mark.case_id("NC-prompt-package-missing-or-corrupt-rejected")
@pytest.mark.parametrize("读取,名称", 模板集, ids=("fine-plan", "work-plan", "literary")) @pytest.mark.parametrize(
"读取,名称",
模板集,
ids=("fine-plan", "work-plan", "literary", "explore", "chapter-extract", "book-analysis"),
)
@pytest.mark.parametrize("故障", ("missing", "hash")) @pytest.mark.parametrize("故障", ("missing", "hash"))
def test_发布包模板缺失或篡改不从源码补齐(tmp_path, monkeypatch, 读取, 名称, 故障): def test_发布包模板缺失或篡改不从源码补齐(tmp_path, monkeypatch, 读取, 名称, 故障):
_安装资源( _安装资源(

View File

@ -0,0 +1,17 @@
"""写手阻塞(blocked)交付在评测消费点的分流:不进成对比较、不做语义检测。"""
import pytest
from muse.效果评测.模型 import 评测错误
from muse.效果评测.语义检测 import 读取检测材料
from muse.效果评测.逐例执行 import _比较材料
@pytest.mark.case_id("NC-eval-blocked-writer-no-material")
def test_写手阻塞交付在比较与检测材料处明确失败():
阻塞写手 = {"output": {"blocked": {"reason": "死结"}}}
正常写手 = {"output": {"paragraphs": [{"text": "雨。"}]}}
with pytest.raises(评测错误, match="语义检测依赖的写手声明阻塞"):
读取检测材料(None, {"dependencies": ["u1"]}, 阻塞写手)
with pytest.raises(评测错误, match="成对比较的写手声明阻塞"):
_比较材料(None, {"dependencies": ["a", "b"]}, [正常写手, 阻塞写手])

View File

@ -225,3 +225,34 @@ def test_领域写作粒度兼容而新生成入口仅接受新章__e13005():
assert 写作任务合同(**领域, 粒度="expand_scene").选区 == () assert 写作任务合同(**领域, 粒度="expand_scene").选区 == ()
with pytest.raises(写作任务错误, match="选段修改必须携带明确选区"): with pytest.raises(写作任务错误, match="选段修改必须携带明确选区"):
写作任务合同(**领域, 粒度="edit_selection") 写作任务合同(**领域, 粒度="edit_selection")
@pytest.mark.case_id(
"NC-b05-writer-output-blocked-or-paragraphs",
when="写手输出只接受正文或阻塞二选一",
given="输出合同含 paragraphs/blocked oneOf 出口",
)
def test_写手输出接受阻塞出口且拒绝双现全缺__e13007():
from dataclasses import replace
from jsonschema import Draft202012Validator
from muse.任务运行.执行合同 import 模型协议错误, 模型结果, 模型请求
from muse.任务运行.模型调用 import 校验模型输出
from muse.正文写作.接口 import 写作输出合同
合同 = 写作输出合同()
请求 = 模型请求("call", "synthetic", "model", "写作", "材料", 合同, 200, 10)
阻塞 = {"blocked": {"reason": "细纲第2事件与已确认事实冲突,任何写法都必然虚构。"}}
结果 = 模型结果("completed", json.dumps(阻塞), "model", None)
assert 校验模型输出(请求, 结果) == 阻塞
for 非法 in (
{"paragraphs": [{"text": "雨。"}], "blocked": {"reason": "双现"}},
{},
{"blocked": {}},
{"blocked": {"reason": ""}},
{"blocked": {"reason": "越权字段", "conflicts": []}},
):
assert list(Draft202012Validator(合同).iter_errors(非法)), 非法
with pytest.raises(模型协议错误, match="模型输出不符合固定结构合同"):
校验模型输出(请求, replace(结果, 文本=json.dumps(非法)))

View File

@ -0,0 +1,63 @@
"""提示词治理守卫:模型系统提示必须来自登记资源,代码内字面量仅白名单豁免。
保护:A1 类回归——任务提示词散落在代码字面量、绕过资源登记/哈希冻结/发布包治理。
白名单唯一成员是配置探针:探针提示与探针合同同构、64 token 固定、无迭代需求,
豁免理由在 配置探针.py 调用处注释留痕。
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
仓库根 = Path(__file__).resolve().parents[2]
源码根 = 仓库根 / "src" / "muse"
白名单 = {"任务运行/配置探针.py"}
# 系统提示= 后接字符串字面量(含括号包裹的隐式拼接与 f/r/b 前缀)即硬编码;
# 变量或函数返回值传递(系统提示=模板 / 系统提示=拆书分析模板()[0])不匹配。
_硬编码 = re.compile(r"""系统提示\s*=\s*\(?\s*[fF]?[rRbBuU]{0,2}["']""")
def _源码文件():
for 文件 in sorted(源码根.rglob("*.py")):
if "__pycache__" in 文件.parts:
continue
yield 文件
@pytest.mark.case_id("NC-prompt-governance-no-hardcoded-system-prompt")
def test_系统提示字面量仅白名单豁免():
违规 = [
str(文件.relative_to(源码根))
for 文件 in _源码文件()
if str(文件.relative_to(源码根)) not in 白名单
and _硬编码.search(文件.read_text(encoding="utf-8"))
]
assert 违规 == [], f"系统提示不得硬编码字面量,应登记为资源模板:{违规}"
@pytest.mark.case_id("NC-prompt-governance-guard-self-check")
def test_守卫正则检出各字面量形式且不误报变量传递():
检出样例 = (
'系统提示="你是XX。"',
"系统提示='你是XX。'",
'系统提示=(\n "你是XX。"\n "做YY。"\n )',
'系统提示 = (\n "你是XX。"\n)',
'系统提示=f"你是{名}。"',
)
for 样例 in 检出样例:
assert _硬编码.search(样例), f"守卫漏检字面量形式:{样例}"
放行样例 = (
"系统提示=模板",
"系统提示=模板,",
"系统提示=探索提示,",
"系统提示=拆书分析模板()[0]",
"系统提示: str,",
"系统提示: str = 模板",
)
for 样例 in 放行样例:
assert not _硬编码.search(样例), f"守卫误报变量传递:{样例}"

View File

@ -3091,6 +3091,24 @@
], ],
"markers": [] "markers": []
}, },
{
"case_id": "NC-b05-writer-output-blocked-or-paragraphs",
"when": "写手输出只接受正文或阻塞二选一",
"given": "输出合同含 paragraphs/blocked oneOf 出口",
"file": "tests/契约/test_正文与模型输出.py",
"symbol": "test_写手输出接受阻塞出口且拒绝双现全缺__e13007",
"parameter_ids": [],
"node_ids": [
"tests/契约/test_正文与模型输出.py::test_写手输出接受阻塞出口且拒绝双现全缺__e13007"
],
"fixtures": [
"request",
"测试资源接缝",
"源码资源",
"离线防护"
],
"markers": []
},
{ {
"case_id": "NC-b05-writer-reject-inline-newline", "case_id": "NC-b05-writer-reject-inline-newline",
"when": "写手输出在模型合同阶段拒绝段内换行", "when": "写手输出在模型合同阶段拒绝段内换行",
@ -6627,6 +6645,22 @@
"file": "web/tests/编辑/保存冲突.test.tsx", "file": "web/tests/编辑/保存冲突.test.tsx",
"symbol": "NC-editor-save-retry:未知结果重试原命令,后续输入不改变在途载荷" "symbol": "NC-editor-save-retry:未知结果重试原命令,后续输入不改变在途载荷"
}, },
{
"case_id": "NC-eval-blocked-writer-no-material",
"file": "tests/单元/test_评测阻塞分流.py",
"symbol": "test_写手阻塞交付在比较与检测材料处明确失败",
"parameter_ids": [],
"node_ids": [
"tests/单元/test_评测阻塞分流.py::test_写手阻塞交付在比较与检测材料处明确失败"
],
"fixtures": [
"request",
"测试资源接缝",
"源码资源",
"离线防护"
],
"markers": []
},
{ {
"case_id": "NC-evidence-outcome-immutable", "case_id": "NC-evidence-outcome-immutable",
"environment": "隔离 PostgreSQL 与合成原文", "environment": "隔离 PostgreSQL 与合成原文",
@ -8941,16 +8975,81 @@
], ],
"markers": [] "markers": []
}, },
{
"case_id": "NC-prompt-assembly-single-identity",
"file": "tests/单元/test_提示词装配.py",
"symbol": "test_装配后身份唯一且模板无身份句",
"parameter_ids": [
"book-analysis",
"chapter-extract",
"explore"
],
"node_ids": [
"tests/单元/test_提示词装配.py::test_装配后身份唯一且模板无身份句[book-analysis]",
"tests/单元/test_提示词装配.py::test_装配后身份唯一且模板无身份句[chapter-extract]",
"tests/单元/test_提示词装配.py::test_装配后身份唯一且模板无身份句[explore]"
],
"fixtures": [
"request",
"合同",
"测试资源接缝",
"源码资源",
"离线防护",
"角色",
"读取"
],
"markers": [
"parametrize"
]
},
{
"case_id": "NC-prompt-governance-guard-self-check",
"file": "tests/架构/test_提示词治理.py",
"symbol": "test_守卫正则检出各字面量形式且不误报变量传递",
"parameter_ids": [],
"node_ids": [
"tests/架构/test_提示词治理.py::test_守卫正则检出各字面量形式且不误报变量传递"
],
"fixtures": [
"request",
"测试资源接缝",
"源码资源",
"离线防护"
],
"markers": []
},
{
"case_id": "NC-prompt-governance-no-hardcoded-system-prompt",
"file": "tests/架构/test_提示词治理.py",
"symbol": "test_系统提示字面量仅白名单豁免",
"parameter_ids": [],
"node_ids": [
"tests/架构/test_提示词治理.py::test_系统提示字面量仅白名单豁免"
],
"fixtures": [
"request",
"测试资源接缝",
"源码资源",
"离线防护"
],
"markers": []
},
{ {
"case_id": "NC-prompt-package-content-and-hash", "case_id": "NC-prompt-package-content-and-hash",
"file": "tests/单元/test_规划审校发布模板.py", "file": "tests/单元/test_规划审校发布模板.py",
"symbol": "test_规划审校无源树从发布包读相同内容与哈希", "symbol": "test_规划审校无源树从发布包读相同内容与哈希",
"parameter_ids": [ "parameter_ids": [
"book-analysis",
"chapter-extract",
"explore",
"fine-plan", "fine-plan",
"literary", "literary",
"work-plan" "work-plan"
], ],
"node_ids": [ "node_ids": [
"tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[book-analysis]",
"tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[chapter-extract]",
"tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[explore]",
"tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[fine-plan]", "tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[fine-plan]",
"tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[literary]", "tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[literary]",
"tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[work-plan]" "tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[work-plan]"
@ -8975,17 +9074,29 @@
"file": "tests/单元/test_规划审校发布模板.py", "file": "tests/单元/test_规划审校发布模板.py",
"symbol": "test_发布包模板缺失或篡改不从源码补齐", "symbol": "test_发布包模板缺失或篡改不从源码补齐",
"parameter_ids": [ "parameter_ids": [
"hash-book-analysis",
"hash-chapter-extract",
"hash-explore",
"hash-fine-plan", "hash-fine-plan",
"hash-literary", "hash-literary",
"hash-work-plan", "hash-work-plan",
"missing-book-analysis",
"missing-chapter-extract",
"missing-explore",
"missing-fine-plan", "missing-fine-plan",
"missing-literary", "missing-literary",
"missing-work-plan" "missing-work-plan"
], ],
"node_ids": [ "node_ids": [
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-book-analysis]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-chapter-extract]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-explore]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-fine-plan]", "tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-fine-plan]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-literary]", "tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-literary]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-work-plan]", "tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[hash-work-plan]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-book-analysis]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-chapter-extract]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-explore]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-fine-plan]", "tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-fine-plan]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-literary]", "tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-literary]",
"tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-work-plan]" "tests/单元/test_规划审校发布模板.py::test_发布包模板缺失或篡改不从源码补齐[missing-work-plan]"
@ -12205,6 +12316,40 @@
"数据库" "数据库"
] ]
}, },
{
"case_id": "NC-w15-writer-blocked-exit",
"environment": "真实隔离PostgreSQL",
"given": "细纲与已确认事实存在不可化解死结的写作任务(合成剧本)",
"when": "写作回合输出 blocked 声明而非正文",
"then": [
"blocked 载荷通过输出合同校验并随结构化交付留痕",
"任务失败于无工具写作步骤,错误说明含写手理由",
"不进入检查与候选步骤,不登记方法消费"
],
"contract": "docs/系统架构/新版设计/模块设计/B05-正文写作.md",
"file": "tests/集成/test_两阶段写手.py",
"symbol": "test_写手声明写作死结时任务明确失败且不产候选__e72a10",
"parameter_ids": [],
"node_ids": [
"tests/集成/test_两阶段写手.py::test_写手声明写作死结时任务明确失败且不产候选__e72a10"
],
"fixtures": [
"request",
"tmp_path",
"tmp_path_factory",
"内置种子方案",
"内置结构测试库",
"数据库底座",
"测试资源接缝",
"源码资源",
"生成环境",
"离线防护",
"隔离数据库URL"
],
"markers": [
"数据库"
]
},
{ {
"case_id": "NC-w16-w16a01", "case_id": "NC-w16-w16a01",
"environment": "真实隔离 PG", "environment": "真实隔离 PG",

View File

@ -885,3 +885,68 @@ def test_模型自填无效输出被输出合同拒绝__93c5b7(生成环境):
运行.执行一步(领取) 运行.执行一步(领取)
终态 = 运行.读取任务(任务ID) 终态 = 运行.读取任务(任务ID)
assert any(s["state"] == "failed" for s in 终态.步骤) assert any(s["state"] == "failed" for s in 终态.步骤)
@pytest.mark.case_id(
"NC-w15-writer-blocked-exit",
environment="真实隔离PostgreSQL",
given="细纲与已确认事实存在不可化解死结的写作任务(合成剧本)",
when="写作回合输出 blocked 声明而非正文",
then=[
"blocked 载荷通过输出合同校验并随结构化交付留痕",
"任务失败于无工具写作步骤,错误说明含写手理由",
"不进入检查与候选步骤,不登记方法消费",
],
contract="docs/系统架构/新版设计/模块设计/B05-正文写作.md",
)
def test_写手声明写作死结时任务明确失败且不产候选__e72a10(生成环境):
剧本 = [
{"类型": "工具调用", "调用ID": "exp-plan", "名称": "query_work_plan", "参数": {}},
{"类型": "空对象"},
{
"类型": "文本",
"文本": {"blocked": {"reason": "细纲要求角色在已确认死亡后出场,任何写法都必然虚构。"}},
},
]
环境 = 生成环境
回执 = 发起生成正文(
环境["装配"],
环境["作者"],
f"gen-{uuid4()}",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)
任务ID = 回执["task_id"]
预算 = 预算管理(环境["库"], "synthetic")
预算.登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
预算.登记任务预算(
任务ID,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"approval",
datetime.now(UTC) + timedelta(minutes=10),
),
)
环境["剧本"].extend(剧本)
运行 = 环境["装配"].任务运行
with pytest.raises(生成正文错误, match="写手声明写作死结"):
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
break
领取 = 运行.领取步骤("worker", 处理器能力)
assert 领取 is not None
运行.执行一步(领取)
终态 = 运行.读取任务(任务ID)
assert 终态.状态 is 任务状态.已失败
失败步 = [s for s in 终态.步骤 if s["state"] == "failed"]
assert 失败步 and 失败步[0]["step_id"] == "无工具写作"
assert not any(s["step_id"] == "检查与候选" and s["state"] == "completed" for s in 终态.步骤)
with 环境["库"].连接(只读=True) as 连:
消费数 = 连.execute(
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (任务ID,)
).fetchone()
assert 消费数[0] == 0, "blocked 写作死结不得登记方法消费"

View File

@ -112,7 +112,7 @@ def test_规划真实方法消费且只产出候选__20c01a(规划环境):
task_id, 终态 = _运行(环境) task_id, 终态 = _运行(环境)
assert 终态.状态 == 任务状态.已完成 assert 终态.状态 == 任务状态.已完成
assert len(环境["收到"]) == 1 and not 环境["收到"][0].get("tools") assert len(环境["收到"]) == 1 and not 环境["收到"][0].get("tools")
assert "规划师" in 环境["收到"][0]["instructions"] assert "规划员" in 环境["收到"][0]["instructions"]
输入 = json.loads(环境["收到"][0]["input"]) 输入 = json.loads(环境["收到"][0]["input"])
assert "用动作表现角色的迟疑" in 输入["装配材料"] assert "用动作表现角色的迟疑" in 输入["装配材料"]
assert "方法字段仅供规划定位,不应发给写手。" in 输入["装配材料"] assert "方法字段仅供规划定位,不应发给写手。" in 输入["装配材料"]
@ -190,7 +190,7 @@ def test_错误角色配置不留孤儿规划任务__20c02b(规划环境):
环境 = 规划环境 环境 = 规划环境
with 环境["库"].连接(只读=True) as 连: with 环境["库"].连接(只读=True) as 连:
前 = 连.execute("SELECT COUNT(*) FROM muse_task").fetchone()[0] 前 = 连.execute("SELECT COUNT(*) FROM muse_task").fetchone()[0]
with pytest.raises(作品错误, match="规划师角色"): with pytest.raises(作品错误, match="规划员角色"):
发起生成规划( 发起生成规划(
环境["装配"], 环境["装配"],
环境["作者"], 环境["作者"],

View File

@ -143,6 +143,18 @@
"源路径": "src/muse/正文写作/提示词/修改选段.md", "源路径": "src/muse/正文写作/提示词/修改选段.md",
"打包路径": "正文/修改选段.md" "打包路径": "正文/修改选段.md"
}, },
{
"源路径": "src/muse/编排/提示词/受限探索.md",
"打包路径": "正文/受限探索.md"
},
{
"源路径": "src/muse/编排/提示词/章后抽取.md",
"打包路径": "抽取/章后抽取.md"
},
{
"源路径": "src/muse/资料研究/提示词/拆书分析.md",
"打包路径": "抽取/拆书分析.md"
},
{ {
"源路径": ".agent/skills/操作/记录机器味案例/references/案例合同.md", "源路径": ".agent/skills/操作/记录机器味案例/references/案例合同.md",
"打包路径": "审校/案例合同.md" "打包路径": "审校/案例合同.md"