diff --git a/.agent/skills/操作/检查与维护拆书结果/SKILL.md b/.agent/skills/操作/检查与维护拆书结果/SKILL.md index 02b0271..4738755 100644 --- a/.agent/skills/操作/检查与维护拆书结果/SKILL.md +++ b/.agent/skills/操作/检查与维护拆书结果/SKILL.md @@ -16,10 +16,20 @@ description: 检查拆书台账、歧义与失败窗口,按窗口重算或以 ## 操作步骤 1. `python -m muse 研究 进度 <来源ID>` 或「拆书工作区」台账查看状态。 -2. `python -m muse 研究 检查 <任务ID>` 查看完成窗、失败窗与歧义清单;歧义条目待「核对实体歧义」澄清,同名异型默认不合并。 +2. `python -m muse 研究 检查 <任务ID>` 查看完成窗、失败窗与歧义清单;歧义条目只登记、由作者人工判定(核对步骤见下),同名异型默认不合并。 3. 失败窗重算:经恢复入口带 `retry_windows` 指定窗口;已完成窗不重跑,成本包络含重试余量。 4. 来源新版本重建:直接重新发起拆解;旧版本分析保留可回查,版本身份不冲突。 +## 歧义人工核对 + +对歧义条目(同名不同类型的实体候选)逐项人工核对: + +1. 列出各候选的出现窗与证据引文。 +2. 依据上下文判断各候选是否同一实体;判断必须给出引文级依据。 +3. 结论只能是三态之一:同一实体(合并并说明)、不同实体(分别命名区分)、证据不足(维持歧义)。 + +不因为同名就直接合并;类型冲突的同名默认不同实体;只处理参考作品内部身份判定,不跨作品合并、不改写本书事实;证据不足时维持歧义,不做倾向性猜测。 + ## 边界 - 只撤销本任务可证明归属的中间产物;不触碰外部确认与来源版本。 diff --git a/.agent/角色/角色合同.md b/.agent/角色/角色合同.md index e8295f9..fa9c39b 100644 --- a/.agent/角色/角色合同.md +++ b/.agent/角色/角色合同.md @@ -9,7 +9,7 @@ roles: explicitModelRequired: true toolPolicy: readonly_tools planner: - displayName: 规划师 + displayName: 规划员 promptFile: .agent/角色/规划员.md modelPolicy: fixed modelPolicyVersion: role-policy-r2-v2 @@ -116,7 +116,7 @@ roles: -## planner:规划师 +## planner:规划员 **责任**:承接设定初始化、作品规划和单章细纲等规划任务;一次调用只执行任务包指定的一个功能合同,产出可比较或可校验的 Shadow 草稿。 diff --git a/docs/2026-09-19-提示词问题修复方案.md b/docs/2026-09-19-提示词问题修复方案.md index 67c60d1..9aa2346 100644 --- a/docs/2026-09-19-提示词问题修复方案.md +++ b/docs/2026-09-19-提示词问题修复方案.md @@ -14,19 +14,14 @@ | 2 | C1 blocked 载荷改为**仅 reason**,不要求写手逐字引文举证;依据"写手纯写作负荷"原则(引文举证属检测/审阅职能) | 用户决策 2026-09-19 | | 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;用户决策=删除+话术改写 | -| 5 | 新增范围外声明:其余孤儿模板不在本方案;`分析参考作品.md` 例外随批次 1 处置 | 复核 P2-2 | +| 5 | 范围外声明修正:经开工前实测,报告所列其余孤儿模板已在早前批次删除,现存唯一孤儿为 `核对实体歧义.md`(批次 2 处置);台账/报告的孤儿清单属过时信息 | 复核 P2-2 + 开工实测 2026-09-19 | | 6 | B3 身份句实为 **4 处**(补 `创作质量审阅.md:3`) | 复核 P2-3 | | 7 | 架构守卫正则须覆盖 `系统提示=(...)` 括号多行字面量形式 | 复核 P2-3 | | 8 | D2 判据下沉受写手纯度原则约束:判据优先落审阅/评委侧模板 | 决策 2 连带 | ## 范围与范围外 -**范围外(显式声明,避免误认提示词问题已全处置)**:以下孤儿模板归属各自专项批次/台账组,本方案不动—— - -- `审校修订/提示词/提取作品声音.md`、`诊断机器味.md`(审校修订+效果评测组台账) -- `交付/全书审阅.md`、`整理交付材料.md`(交付连载组台账,已登记但无代码消费者) - -**例外**:`资料研究/提示词/分析参考作品.md` 与批次 1 新建的 `拆书分析.md` 同域同类,随批次 1 一并处置(见批次 1),避免同类双份残留。 +**事实修正(2026-09-19 开工前实测)**:专项检查报告所列其余孤儿模板(`提取作品声音.md`、`诊断机器味.md`、`分析参考作品.md`、`交付/全书审阅.md`、`整理交付材料.md`)**已在早前批次删除**,仓内不复存在;台账与报告中的孤儿清单属过时信息。现存唯一孤儿为 `核对实体歧义.md`(批次 2 处置)。批次 1 无需"底稿复用"动作,`拆书分析.md` 直接新建。 ## 事实基线(已机械核实) @@ -62,11 +57,6 @@ - 只保留行为指令(逐字引文、不补猜窗口外内容、不新增正文没有的设定等),原文语义不增不减;受限探索模板保留"探索完成后输出空 JSON 对象"等现有行为指令。 - 加载函数仿照 `写作任务.生成正文模板()` 返回 `(文本, sha256)`,供运行证据留痕;请求级冻结已由现有 `请求字节哈希 + 批准保留` 覆盖,不新增机制。 -### 分析参考作品.md 处置(范围外声明的例外) - -- 新建 `拆书分析.md` 时以 `分析参考作品.md` 为底稿改写(内容可对齐则复用其表达),改写完成后**删除原孤儿文件**;内容不可对齐则直接删除并在台账注明。 -- 对应台账项(资料研究组孤儿模板)随本批次销项。 - ### D3 配置探针 - 保留代码内提示:探针与 `探针合同` 代码同构、64 token、无迭代需求;加豁免注释说明理由。 @@ -182,3 +172,25 @@ blocked: { reason: string(minLength 1), additionalProperties: false } - **角色层冻结红线豁免范围(仅批次 2)**:角色合同.md 两处文字(displayName、章节标题);其余角色文件一律不动。 - **执行顺序刚性**:1 → 2 → 3 → 4(批次 2/3 依赖批次 1 的资源化落点;批次 4 依赖模板定稿)。 - **提交纪律**:每批次至少一次本地 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 比较未执行;待真实模型环境补验,无改善则回滚对应模板文本(重新登记旧文本即可)。 diff --git a/docs/实现回顾/专项检查-台账.md b/docs/实现回顾/专项检查-台账.md index 37b4c5c..af60e6d 100644 --- a/docs/实现回顾/专项检查-台账.md +++ b/docs/实现回顾/专项检查-台账.md @@ -25,6 +25,7 @@ - **位置时效**:条目位置为 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`)失去核查对象,保留为历史发现。 - **过程材料留存口径**:14 份模块报告(`专项检查-报告-*.md`)与 `/tmp` 探针日志均为过程材料,受 `.gitignore` 忽略、不入版本库;台账中指向它们的路径仅在本地可读时有效。 +- **JF-03 处置(2026-09-19)**:经用户授权采纳删除方案——`src/muse/资料研究/提示词/核对实体歧义.md` 已删除(未登记、运行时不加载、承诺不存在的任务);研究命令与「检查与维护拆书结果」技能话术改为“歧义只登记,由作者人工判定”,人工核对步骤承接进该技能;`实体归并.py` 话术检查时已合规。原行保留为历史快照。 ## 0. 汇总 diff --git a/docs/系统架构/新版设计/模块设计/B03-资料研究.md b/docs/系统架构/新版设计/模块设计/B03-资料研究.md index cf18073..a5b21bf 100644 --- a/docs/系统架构/新版设计/模块设计/B03-资料研究.md +++ b/docs/系统架构/新版设计/模块设计/B03-资料研究.md @@ -42,13 +42,13 @@ | 分章切窗.py | 分章、窗口划分及输入漂移 | | 榜单快照.py | 来源、时间、质量、排名及观察差异 | | 样本选题.py | 样本比较、选题依据及选书交接 | -| 拆书分析.py | 当前逐窗分析的内联系统提示、输出合同与执行;提取章节、阶段、实体及写法 | +| 拆书分析.py | 逐窗分析的任务模板(发布包登记)、输出合同与执行;提取章节、阶段、实体及写法 | +| 提示词/拆书分析.md | 逐窗拆书分析任务模板;随发布包登记加载,不含身份句 | | 实体归并.py | 参考作品内专名、别名及歧义 | | 分析版本.py | 完整模型成果登记、旧检查点承接及运行摘要 | | 分析保存.py | 分析不可变存储合同、S04校验与S01未决旧卡写入 | -| 提示词/核对实体歧义.md | 作者人工核对歧义的参考说明;未接入自动核对任务 | -当前文件以[资料研究源码](../../../../src/muse/资料研究/)为准;原[目标文件清单](../项目目录与文件职责.md)及[文件合同](../文件设计/后端-B03-资料研究.md)保留历史设计目标,未消费的分析模板已退出。 +当前文件以[资料研究源码](../../../../src/muse/资料研究/)为准;原[目标文件清单](../项目目录与文件职责.md)及[文件合同](../文件设计/后端-B03-资料研究.md)保留历史设计目标,未消费的分析模板与人工歧义核对说明(核对实体歧义.md)已退出,歧义只登记、由作者人工判定。 ## 实现约束 diff --git a/src/muse/任务运行/配置探针.py b/src/muse/任务运行/配置探针.py index d127190..6ac44c8 100644 --- a/src/muse/任务运行/配置探针.py +++ b/src/muse/任务运行/配置探针.py @@ -35,6 +35,8 @@ def 探针请求(任务ID: str, 角色: str, 内容: 运行配置内容, 策略 provider=声明["provider"], model=声明["model"], thinking=声明["thinking"], + # 提示词治理豁免:探针提示与探针合同同构、64 token 固定、无迭代需求, + # 保留代码内字面量;治理白名单见 tests/架构/test_提示词治理.py。 系统提示="你是运行配置探针;只输出结构要求的 JSON,不解释。", 用户输入='请输出 JSON:{"ok": true}', 输出合同=探针合同, diff --git a/src/muse/作品规划/提示词/生成作品规划.md b/src/muse/作品规划/提示词/生成作品规划.md index 6126ae1..56072f8 100644 --- a/src/muse/作品规划/提示词/生成作品规划.md +++ b/src/muse/作品规划/提示词/生成作品规划.md @@ -1,8 +1,8 @@ # 生成作品规划 -你是规划师。本次只生成输入指定的一个规划对象,输出必须满足任务给定的 JSON Schema。 +本次只生成输入指定的一个规划对象,输出必须满足任务给定的 JSON Schema。 - 使用冻结材料中的目录、作者要求和允许模型读取的规划基线。方法材料是写作参考,不是本书事实。 - 保留作者已经确定的方向;引用只能使用材料中可核对的本书身份与版本。材料不足时不得虚构实体、来源身份或确认决定。 -- 不执行工具、不读取其他作品、不输出正文,不把候选宣称为已确认规划。 -- 只输出约定 JSON 对象,不添加解释、Markdown 围栏、模型来源或用户确认字段。 +- 不把候选宣称为已确认规划。 +- 只输出约定 JSON 对象,不添加解释或 Markdown 围栏。 diff --git a/src/muse/作品规划/提示词/细化章节计划.md b/src/muse/作品规划/提示词/细化章节计划.md index 239cdc5..ccbad2f 100644 --- a/src/muse/作品规划/提示词/细化章节计划.md +++ b/src/muse/作品规划/提示词/细化章节计划.md @@ -1,8 +1,8 @@ # 章级细纲 -你是规划师。本次只细化输入指定的一章的章级细纲,输出满足任务给定的 JSON Schema。 +本次只细化输入指定的一章的章级细纲,输出满足任务给定的 JSON Schema。 - 目标章号以冻结目录为准,不改章节身份。已确认的上层方向和作者要求优先。 - 将冲突、出场、事件方向和章末承接组织成可执行的章级细纲,不输出正文。 - 方法卡仅用于选择组织方式,不得将参考作品的实体和情节写成本书事实;不能虚构引用身份或来源版本。 -- 不调用工具、不读取未授权正文,不代作者确认。只返回约定 JSON 对象。 +- 不代作者确认,只返回约定 JSON 对象。 diff --git a/src/muse/审校修订/提示词/创作质量审阅.md b/src/muse/审校修订/提示词/创作质量审阅.md index 0026316..3643dcf 100644 --- a/src/muse/审校修订/提示词/创作质量审阅.md +++ b/src/muse/审校修订/提示词/创作质量审阅.md @@ -1,14 +1,14 @@ # 本书文学片审阅 -你是受控审阅角色。输入包含固定背景、已确认声音(可能没有)、完整片段语境与核心区间;不使用工具,不改写正文。 +输入包含固定背景、已确认声音(可能没有)、完整片段语境与核心区间;不改写正文。 - 对continuity(连续性)、craft(创作质量)、voice(声音)分别给出判断,不合并成一个文学通过分。 -- continuity依据本次允许的时点事实核对因果、视角和已知条件。没有确切外部依据时不能断言事实冲突,返回insufficient_evidence或ask。 +- continuity依据本次允许的时点事实核对因果、视角和已知条件。没有确切外部依据时不能断言事实冲突,返回insufficient_evidence(依据不足)或ask(追问)。 - craft关注人物行动、冲突、节奏、情绪与场景作用,解释具体阅读效果。不要仅凭禁词、比喻或句型命中要求修改。 -- voice只比较已确认指导;没有已确认声音则状态为uncalibrated,不给reviewed或repair。 +- voice只比较已确认指导;没有已确认声音则状态为uncalibrated(未标定),不给reviewed(已审阅)或repair(修订)。 - 每个问题只从本片核心区起点定位,可延伸到本片语境结尾。start/end使用全文Unicode码点左闭右开区间,quote必须逐字来自该位置。重复文字不默认选择第一次出现。 -- judgment为repair、retain或ask。有意表达可retain,未知信号可ask,零问题是合法结果。缺依据或未标定不得输出repair。 +- judgment(判定)为repair、retain(保留)或ask。有意表达可retain,未知信号可ask,零问题是合法结果。缺依据或未标定不得输出repair。 - basis_ids只能引用输入允许的依据ID。不要创造参考资料、人物事实或来源身份。 - reviewed表示本维给出了一次判断,不代表文学收益经过实验验证。禁止输出作者身份概率、检测器规避分数、自动采纳或伪造运行完成声明。 -只输出固定JSON合同,不另附正文改写、说明前后缀或未登记字段。 +只输出固定JSON合同,不另附正文改写或说明前后缀。 diff --git a/src/muse/审校修订/提示词/连续性审阅.md b/src/muse/审校修订/提示词/连续性审阅.md index 624a537..71d3aea 100644 --- a/src/muse/审校修订/提示词/连续性审阅.md +++ b/src/muse/审校修订/提示词/连续性审阅.md @@ -7,3 +7,8 @@ 逐项覆盖required_assertions与required_constraints;每项引用对应assertion或constraint依据及候选原字,并说明pass、fail或unknown。缺证据或无法判断时使用unknown,不为凑通过推断。候选内部的视角、因果或引文问题可引用candidate;对外部事实的判断仍须有相应资料。 发现说明事实、视角、因果、引文、硬约束或连续性问题及严重度;各局部身份唯一。新设定建议只关联本次declared_new或unknown陈述,保持未决。不要提交原文位置、哈希、模型名、任务号或回执;代码会从实际引文派生位置和身份。 + +## 引文示例 + +- 合法:candidate_quote 逐字来自候选全文;evidence_refs 只用输入已给的依据id(如 "candidate"、"e1")。 +- 非法:candidate_quote 截断或转述、evidence_refs 自造id(如 "guess-1")——逐字定位与依据白名单校验都会拒绝。 diff --git a/src/muse/接入/cli/研究命令.py b/src/muse/接入/cli/研究命令.py index 7bc735c..5db5661 100644 --- a/src/muse/接入/cli/研究命令.py +++ b/src/muse/接入/cli/研究命令.py @@ -150,7 +150,7 @@ def 运行研究命令(装配: 应用装配, 动作: str, 目标: str | None) -> if 结果 else ("history_not_imported" if 快照.状态.value == "completed" else "in_progress") ) - 状态["说明"] = "歧义条目待核对实体歧义任务澄清;失败窗可经重跑窗恢复。" + 状态["说明"] = "歧义条目只登记,由作者人工判定;失败窗可经重跑窗恢复。" return 状态 raise 配置错误(f"未知研究命令动作:{动作}") diff --git a/src/muse/效果评测/语义检测.py b/src/muse/效果评测/语义检测.py index 124a952..0674485 100644 --- a/src/muse/效果评测/语义检测.py +++ b/src/muse/效果评测/语义检测.py @@ -11,6 +11,8 @@ from muse.正式变更.接口 import 固定哈希 def 读取检测材料(conn, spec, writer): if writer is None or len(spec["dependencies"]) != 1: raise 评测错误("语义检测缺少单份实际生成交付") + if "blocked" in (writer["output"] or {}): + raise 评测错误("语义检测依赖的写手声明阻塞,无正文可检") text = "\n".join(p["text"] for p in writer["output"]["paragraphs"]) return 组装语义检测材料(text, 读取文学依据(conn, spec)) diff --git a/src/muse/效果评测/逐例执行.py b/src/muse/效果评测/逐例执行.py index c85809a..203c91e 100644 --- a/src/muse/效果评测/逐例执行.py +++ b/src/muse/效果评测/逐例执行.py @@ -930,6 +930,8 @@ def _比较材料(conn, spec, writers): return 组装成对材料(left, right, tuple(spec["dimensions"])) if len(writers) != 2 or any(w is None for w in writers): 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"}: raise 评测错误("成对比较的冻结合同版本未知") texts = ["\n".join(p["text"] for p in w["output"]["paragraphs"]) for w in writers] diff --git a/src/muse/正文写作/写作任务.py b/src/muse/正文写作/写作任务.py index b6f864f..1e162f5 100644 --- a/src/muse/正文写作/写作任务.py +++ b/src/muse/正文写作/写作任务.py @@ -143,6 +143,10 @@ _写作输出合同 = { "type": "object", "properties": { "paragraphs": { + "description": ( + "正文段落数组;每项 text 是完整段落文本,不能包含 LF 或 CR 换行符," + "需要另起一段时另建一个段落对象;系统不改写或规范化段内换行。" + ), "type": "array", "minItems": 1, "items": { @@ -153,9 +157,19 @@ _写作输出合同 = { "required": ["text"], "additionalProperties": False, }, - } + }, + "blocked": { + "description": ( + "写作死结出口:仅当细纲与前置事实存在不可化解冲突" + "(任何写法都必然虚构冲突事实)时输出,reason 一句话说明。" + ), + "type": "object", + "properties": {"reason": {"type": "string", "minLength": 1}}, + "required": ["reason"], + "additionalProperties": False, + }, }, - "required": ["paragraphs"], + "oneOf": [{"required": ["paragraphs"]}, {"required": ["blocked"]}], "additionalProperties": False, } diff --git a/src/muse/正文写作/提示词/生成正文.md b/src/muse/正文写作/提示词/生成正文.md index 8762314..78c58bc 100644 --- a/src/muse/正文写作/提示词/生成正文.md +++ b/src/muse/正文写作/提示词/生成正文.md @@ -1,6 +1,6 @@ # 生成正文(正文写作 · 无工具写作模板) -你是本作品的写手。你收到的是一次冻结装配的写作材料: +你收到的是一次冻结装配的写作材料: - 目标章细纲投影(目标、硬约束、关键事件、必出场对象、章末钩子); - 截止时点的历史正文投影与事实投影(如装配入选); @@ -11,15 +11,13 @@ 1. 只使用装配材料中的设定与事实;不得虚构与材料冲突的事实。 2. 严格覆盖细纲关键事件的顺序与结果方向;必出场对象必须实际出场。 -3. 用戏剧化叙述展开,禁止把细纲措辞原样搬进正文。 +3. 用戏剧化叙述展开(用动作、对白与细节呈现事件,不复述或概括事件),禁止把细纲措辞原样搬进正文。 4. 按当前场景和人物选择适用的已确认声音指导,保持作者认可的表达;口癖只在合适语境使用,不强塞到每句。 5. 依据当前文本判断声音偏移;保护表达用于保真,不要求新造或照抄剧情。人物事实与细纲仍约束声音指导的使用。 6. 词表命中只作观察线索,判断是否偏移须给出文本依据,不能直接判为缺陷。 7. 输出只包含正文段落,不含大纲、说明、注释或元信息。 -## 输出 +## 输出与阻塞 -输出一个 JSON 对象:`{"paragraphs": [{"text": "段落文本"}, ...]}`。 - -- 每项 `text` 是完整段落文本,不能包含 LF 或 CR 换行符;需要另起一段时另建一个段落对象。系统不会改写或规范化段内换行。 -- 段落顺序即正文顺序;不输出段落标题。 +- 正常输出一个 JSON 对象:`{"paragraphs": [{"text": "段落文本"}, ...]}`;段落顺序即正文顺序,不输出段落标题。 +- 仅当细纲与前置事实材料存在不可化解的死结(任何写法都必然虚构冲突事实)时,不写正文,输出 `{"blocked": {"reason": "一句话说明"}}`;不用它回避普通难度。 diff --git a/src/muse/编排/提示词/受限探索.md b/src/muse/编排/提示词/受限探索.md new file mode 100644 index 0000000..4b7d466 --- /dev/null +++ b/src/muse/编排/提示词/受限探索.md @@ -0,0 +1,3 @@ +# 受限探索(编排 · 写手探索阶段模板) + +当前处于受限探索阶段:使用提供的只读工具了解目标章细纲、截止时点事实与历史正文,为写作收集必要材料;不要输出正文,探索完成后输出一个空 JSON 对象。 diff --git a/src/muse/编排/提示词/章后抽取.md b/src/muse/编排/提示词/章后抽取.md new file mode 100644 index 0000000..68cdbc8 --- /dev/null +++ b/src/muse/编排/提示词/章后抽取.md @@ -0,0 +1,8 @@ +# 章后抽取(编排 · 定稿章知识抽取模板) + +对刚定稿的一章做章后抽取:给出续写所需的本章摘要与候选事实;每条事实必须附正文原句引文,不新增正文没有的设定;输出一个 JSON 对象。 + +## 引文示例 + +- 合法:quote 逐字复制正文原句,如正文含「灯芯在风里矮下去」,quote 写「灯芯在风里矮下去」。 +- 非法:quote 多字(「灯芯在风里矮了下去」)、截断(「灯芯在风里…」)或转述(「灯芯被风吹矮」)——后端逐字定位失败即拒绝。 diff --git a/src/muse/编排/生成正文.py b/src/muse/编排/生成正文.py index ef15751..7307f7b 100644 --- a/src/muse/编排/生成正文.py +++ b/src/muse/编排/生成正文.py @@ -78,6 +78,17 @@ _写作输出合同 = 写作输出合同() _探索输出合同 = {"type": "object"} +def 受限探索模板() -> tuple[str, str]: + """读取发布包登记的受限探索模板及实际字节哈希。 + + 角色身份与输出合同由调用方装配;模板只含探索行为指令,不重复自我介绍。 + """ + from muse.资源加载 import 打开资源 + + data = 打开资源("正文/受限探索.md") + return data.decode("utf-8"), hashlib.sha256(data).hexdigest() + + class 生成正文错误(Muse错误): """旅程编排内的明确失败;错误说明面向作者呈现。""" @@ -201,11 +212,7 @@ def 登记生成正文(登记: 流程登记, 装配, 计价) -> None: {"name": d.名称, "description": d.说明, "parameters": d.参数合同} for d in 工具.工具.values() ) - 探索提示 = ( - "你是本作品的写手,当前处于受限探索阶段。" - "使用提供的只读工具了解目标章细纲、截止时点事实与历史正文," - "为写作收集必要材料;不要输出正文,探索完成后输出一个空 JSON 对象。" - ) + 探索提示, 探索模板哈希 = 受限探索模板() for _ in range(轮次上限): 请求 = _写手请求( 装配, @@ -251,6 +258,7 @@ def 登记生成正文(登记: 流程登记, 装配, 计价) -> None: 检查点={ "清单": [asdict(项) for 项 in 清单], "绑定": [asdict(绑) for 绑 in 绑定集.values()], + "模板哈希": 探索模板哈希, }, ) @@ -377,6 +385,10 @@ def 登记生成正文(登记: 流程登记, 装配, 计价) -> None: ), ) ) + # 写作死结出口:blocked 载荷已随模型结构化交付留痕(含 reason); + # 此处不登记方法消费、不产出候选,以编排内明确失败把理由呈现给作者。 + if "blocked" in 交付.内容: + raise 生成正文错误(f"写手声明写作死结:{交付.内容['blocked']['reason']}") # 只有真实写作交付后才登记消费,并关联模型调用;冻结不自动产生消费。 from muse.知识方法.接口 import 消费输入, 记录消费 @@ -631,6 +643,7 @@ __all__ = [ "流程定义_生成正文", "生成正文步骤", "生成正文模板", + "受限探索模板", "流程身份", "流程版本", "探索工具集", diff --git a/src/muse/编排/生成规划.py b/src/muse/编排/生成规划.py index e994bcb..652f55f 100644 --- a/src/muse/编排/生成规划.py +++ b/src/muse/编排/生成规划.py @@ -316,5 +316,5 @@ def 发起生成规划(装配, 身份, 命令ID: str, 请求: 规划保存, 要 任务ID = 装配.任务运行.创建任务于(连, 任务, 流程身份, 流程版本) 配置 = 配置版本管理(库).冻结到任务于(连, 任务ID, 配置ID) if "planner" not in 配置.内容.角色配置: - raise 作品错误("CONFIG_INVALID", "选定配置没有规划师角色") + raise 作品错误("CONFIG_INVALID", "选定配置没有规划员角色") return {"task_id": 任务ID} diff --git a/src/muse/编排/章后处理.py b/src/muse/编排/章后处理.py index b2bf531..40057f2 100644 --- a/src/muse/编排/章后处理.py +++ b/src/muse/编排/章后处理.py @@ -55,6 +55,17 @@ class 章后处理错误(Muse错误): 错误码 = "POST_FLOW_INVALID" +def 章后抽取模板() -> tuple[str, str]: + """读取发布包登记的章后抽取模板及实际字节哈希。 + + 角色身份与输出合同由调用方装配;模板只含抽取行为指令,不重复自我介绍。 + """ + from muse.资源加载 import 打开资源 + + data = 打开资源("抽取/章后抽取.md") + return data.decode("utf-8"), hashlib.sha256(data).hexdigest() + + _抽取输出合同 = { "type": "object", "properties": { @@ -159,16 +170,13 @@ def 登记章后处理(登记: 流程登记, 装配, 计价, *, 抽取配置ID: from muse.任务运行.接口 import 配置版本管理 声明 = 配置版本管理(装配.要求数据库()).读取任务绑定(任务.任务ID).内容.角色配置["extractor"] + 模板, 模板哈希 = 章后抽取模板() 请求 = 模型请求( 调用ID=str(uuid4()), provider=声明["provider"], model=声明["model"], thinking=声明.get("thinking"), - 系统提示=( - "你是本作品的抽取员,对刚定稿的一章做章后抽取:" - "给出续写所需的本章摘要与候选事实;每条事实必须附正文原句引文," - "不新增正文没有的设定;输出一个 JSON 对象。" - ), + 系统提示=模板, 用户输入=json.dumps( { "chapter_id": 来源["chapter_id"], @@ -199,7 +207,12 @@ def 登记章后处理(登记: 流程登记, 装配, 计价, *, 抽取配置ID: 交付 = asyncio.run(执行器.执行(上下文, 请求, 阶段="抽取", 原文授权ID=授权)) return 步骤结果( 输出={"摘要字数": len(交付.内容["summary"]), "事实数": len(交付.内容["facts"])}, - 检查点={"抽取": 交付.内容, "调用ID": 交付.调用ID, "证据回执": 交付.证据回执}, + 检查点={ + "抽取": 交付.内容, + "调用ID": 交付.调用ID, + "证据回执": 交付.证据回执, + "模板哈希": 模板哈希, + }, ) def 登记事实提案(上下文: 执行上下文) -> 步骤结果: @@ -438,6 +451,7 @@ __all__ = [ "查询章后状态", "事实批内去重", "章后处理错误", + "章后抽取模板", "流程身份", "流程版本", "处理器版本集", diff --git a/src/muse/资料研究/拆书分析.py b/src/muse/资料研究/拆书分析.py index a3ea333..10b111b 100644 --- a/src/muse/资料研究/拆书分析.py +++ b/src/muse/资料研究/拆书分析.py @@ -64,11 +64,17 @@ from muse.资料研究.模型 import 资料错误 "additionalProperties": False, } -系统提示 = ( - "你是参考作品分析员,对给定窗口做拆书分析:" - "列出章节与情节节拍、叙事阶段、出现的人物/地点/事物实体(附原文引文证据)与可借鉴的写法。" - "只依据窗口原文,不补猜窗口外内容;输出一个 JSON 对象。" -) + +def 拆书分析模板() -> tuple[str, str]: + """读取发布包登记的拆书分析模板及实际字节哈希。 + + 角色身份与输出合同由调用方装配;模板只含分析行为指令,不重复自我介绍。 + 模板字节已冻结进请求哈希(批准保留留痕),不再单独落库。 + """ + from muse.资源加载 import 打开资源 + + data = 打开资源("抽取/拆书分析.md") + return data.decode("utf-8"), hashlib.sha256(data).hexdigest() def 执行窗口提取( @@ -88,12 +94,13 @@ def 执行窗口提取( from muse.任务运行.接口 import 配置版本管理 声明 = 配置版本管理(工厂).读取任务绑定(上下文.任务.任务ID).内容.角色配置["extractor"] + 模板, _模板哈希 = 拆书分析模板() 请求 = 模型请求( 调用ID=str(uuid4()), provider=声明["provider"], model=声明["model"], thinking=声明.get("thinking"), - 系统提示=系统提示, + 系统提示=模板, 用户输入=json.dumps( { "source_id": 拆书输入.get("source_id"), @@ -132,4 +139,4 @@ def 执行窗口提取( return 内容 -__all__ = ["窗口输出合同", "系统提示", "执行窗口提取", "资料错误"] +__all__ = ["窗口输出合同", "拆书分析模板", "执行窗口提取", "资料错误"] diff --git a/src/muse/资料研究/提示词/拆书分析.md b/src/muse/资料研究/提示词/拆书分析.md new file mode 100644 index 0000000..d9cdea3 --- /dev/null +++ b/src/muse/资料研究/提示词/拆书分析.md @@ -0,0 +1,8 @@ +# 拆书分析(资料研究 · 参考作品逐窗分析模板) + +对给定窗口做拆书分析:列出章节与情节节拍、叙事阶段、出现的人物/地点/事物实体(附原文引文证据)与可借鉴的写法。只依据窗口原文,不补猜窗口外内容;输出一个 JSON 对象。 + +## 引文示例 + +- 合法:实体 evidence 逐字摘自窗口原文,如窗口含「旧码头的雾灯亮了」,evidence 写「旧码头的雾灯亮了」。 +- 非法:evidence 截断(「旧码头的雾…」)或转述(「雾灯亮起」)——不是原文逐字,属无效证据。 diff --git a/src/muse/资料研究/提示词/核对实体歧义.md b/src/muse/资料研究/提示词/核对实体歧义.md deleted file mode 100644 index b267713..0000000 --- a/src/muse/资料研究/提示词/核对实体歧义.md +++ /dev/null @@ -1,17 +0,0 @@ -# 核对实体歧义 · 人工核对说明 - -歧义只登记,由作者人工判定。以下是人工查看证据时的参考步骤,尚无自动语义核对任务或自动合并入口。 - -## 核对步骤 - -对拆书归并中登记的歧义条目(同名不同类型的实体候选)逐项核对: - -1. 列出各候选的出现窗与证据引文。 -2. 依据上下文判断各候选是否同一实体;判断必须给出引文级依据。 -3. 结论只能是三态之一:同一实体(合并并说明)、不同实体(分别命名区分)、证据不足(维持歧义)。 - -## 合同 - -- 不因为同名就直接合并;类型冲突的同名默认不同实体。 -- 只处理参考作品内部身份判定;不跨作品合并,不改写本书事实。 -- 证据不足时维持歧义,不得输出倾向性猜测。 diff --git a/tests/单元/test_提示词装配.py b/tests/单元/test_提示词装配.py new file mode 100644 index 0000000..62f7740 --- /dev/null +++ b/tests/单元/test_提示词装配.py @@ -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 diff --git a/tests/单元/test_规划审校发布模板.py b/tests/单元/test_规划审校发布模板.py index 2c4ba56..20c141d 100644 --- a/tests/单元/test_规划审校发布模板.py +++ b/tests/单元/test_规划审校发布模板.py @@ -1,4 +1,4 @@ -"""规划与文学审校仅消费发布资源;发现技能不等于全部运行能力。""" +"""规划、审校与编排/资料研究链路仅消费发布资源;发现技能不等于全部运行能力。""" import hashlib 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 使用资源提供器, 安装包资源提供器, 读取能力 模板集 = ( (lambda: 规划模板("fine_outline"), "规划/细化章节计划.md"), (lambda: 规划模板("outline"), "规划/生成作品规划.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.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, 读取, 名称): 内容 = "仅存在于当前发布包的测试提示词。".encode() _安装资源(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.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")) def test_发布包模板缺失或篡改不从源码补齐(tmp_path, monkeypatch, 读取, 名称, 故障): _安装资源( diff --git a/tests/单元/test_评测阻塞分流.py b/tests/单元/test_评测阻塞分流.py new file mode 100644 index 0000000..89279ca --- /dev/null +++ b/tests/单元/test_评测阻塞分流.py @@ -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"]}, [正常写手, 阻塞写手]) diff --git a/tests/契约/test_正文与模型输出.py b/tests/契约/test_正文与模型输出.py index 9c03c6a..3d5b278 100644 --- a/tests/契约/test_正文与模型输出.py +++ b/tests/契约/test_正文与模型输出.py @@ -225,3 +225,34 @@ def test_领域写作粒度兼容而新生成入口仅接受新章__e13005(): assert 写作任务合同(**领域, 粒度="expand_scene").选区 == () with pytest.raises(写作任务错误, match="选段修改必须携带明确选区"): 写作任务合同(**领域, 粒度="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(非法))) diff --git a/tests/架构/test_提示词治理.py b/tests/架构/test_提示词治理.py new file mode 100644 index 0000000..9839bf0 --- /dev/null +++ b/tests/架构/test_提示词治理.py @@ -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"守卫误报变量传递:{样例}" diff --git a/tests/用例清单.json b/tests/用例清单.json index 83f3b92..a0fab2e 100644 --- a/tests/用例清单.json +++ b/tests/用例清单.json @@ -3091,6 +3091,24 @@ ], "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", "when": "写手输出在模型合同阶段拒绝段内换行", @@ -6627,6 +6645,22 @@ "file": "web/tests/编辑/保存冲突.test.tsx", "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", "environment": "隔离 PostgreSQL 与合成原文", @@ -8941,16 +8975,81 @@ ], "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", "file": "tests/单元/test_规划审校发布模板.py", "symbol": "test_规划审校无源树从发布包读相同内容与哈希", "parameter_ids": [ + "book-analysis", + "chapter-extract", + "explore", "fine-plan", "literary", "work-plan" ], "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_规划审校无源树从发布包读相同内容与哈希[literary]", "tests/单元/test_规划审校发布模板.py::test_规划审校无源树从发布包读相同内容与哈希[work-plan]" @@ -8975,17 +9074,29 @@ "file": "tests/单元/test_规划审校发布模板.py", "symbol": "test_发布包模板缺失或篡改不从源码补齐", "parameter_ids": [ + "hash-book-analysis", + "hash-chapter-extract", + "hash-explore", "hash-fine-plan", "hash-literary", "hash-work-plan", + "missing-book-analysis", + "missing-chapter-extract", + "missing-explore", "missing-fine-plan", "missing-literary", "missing-work-plan" ], "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-literary]", "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-literary]", "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", "environment": "真实隔离 PG", diff --git a/tests/集成/test_两阶段写手.py b/tests/集成/test_两阶段写手.py index 34d7954..5b7fb40 100644 --- a/tests/集成/test_两阶段写手.py +++ b/tests/集成/test_两阶段写手.py @@ -885,3 +885,68 @@ def test_模型自填无效输出被输出合同拒绝__93c5b7(生成环境): 运行.执行一步(领取) 终态 = 运行.读取任务(任务ID) 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 写作死结不得登记方法消费" diff --git a/tests/集成/test_受控规划方法消费.py b/tests/集成/test_受控规划方法消费.py index 088b104..fd92b3c 100644 --- a/tests/集成/test_受控规划方法消费.py +++ b/tests/集成/test_受控规划方法消费.py @@ -112,7 +112,7 @@ def test_规划真实方法消费且只产出候选__20c01a(规划环境): task_id, 终态 = _运行(环境) assert 终态.状态 == 任务状态.已完成 assert len(环境["收到"]) == 1 and not 环境["收到"][0].get("tools") - assert "规划师" in 环境["收到"][0]["instructions"] + assert "规划员" in 环境["收到"][0]["instructions"] 输入 = json.loads(环境["收到"][0]["input"]) assert "用动作表现角色的迟疑" in 输入["装配材料"] assert "方法字段仅供规划定位,不应发给写手。" in 输入["装配材料"] @@ -190,7 +190,7 @@ def test_错误角色配置不留孤儿规划任务__20c02b(规划环境): 环境 = 规划环境 with 环境["库"].连接(只读=True) as 连: 前 = 连.execute("SELECT COUNT(*) FROM muse_task").fetchone()[0] - with pytest.raises(作品错误, match="规划师角色"): + with pytest.raises(作品错误, match="规划员角色"): 发起生成规划( 环境["装配"], 环境["作者"], diff --git a/工具/资源登记.json b/工具/资源登记.json index 078ca9b..d79e7ce 100644 --- a/工具/资源登记.json +++ b/工具/资源登记.json @@ -143,6 +143,18 @@ "源路径": "src/muse/正文写作/提示词/修改选段.md", "打包路径": "正文/修改选段.md" }, + { + "源路径": "src/muse/编排/提示词/受限探索.md", + "打包路径": "正文/受限探索.md" + }, + { + "源路径": "src/muse/编排/提示词/章后抽取.md", + "打包路径": "抽取/章后抽取.md" + }, + { + "源路径": "src/muse/资料研究/提示词/拆书分析.md", + "打包路径": "抽取/拆书分析.md" + }, { "源路径": ".agent/skills/操作/记录机器味案例/references/案例合同.md", "打包路径": "审校/案例合同.md"