W18 拆书、归并、互斥与补偿(收口):分章切窗(确定性章界/窗计划哈希/漂移守卫/覆盖核对)、逐窗模型提取(S02 证据合同、输出合同含实体引文)、确定性实体归并(同名异型必歧义不误并、合并带证据与窗归属)、分析版本(uuid5 版本身份、版本台账、失败窗单独重试、旧结论不抹)、样本选题观察陈述;研究拆书五步流程(同源互斥=排他范围领取层强制、窗口断点逐窗检查点、恢复只补缺失窗、预算含重试余量、补偿只触本任务检查点);CLI 研究命令+HTTP 4 端点+工作台拆书工作区/原文分析对照/榜单与选题页(资料对照页退役并入资料页,structure-form 纵排修复表单遮挡)+拆解书稿/检查与维护拆书结果技能;12 新 NC(NC-w18-*),离线 275 / 非宿主数据库 231 / 前端 27,浏览器 2/2+视觉判定通过;整包验证一次通过,W18 置 verified。

This commit is contained in:
zizi 2026-09-11 10:16:58 +08:00
parent 5b22b31b74
commit 06fcfb32cd
116 changed files with 3041 additions and 336 deletions

View File

@ -0,0 +1,26 @@
---
name: 拆解书稿
description: 发起参考作品分窗拆解并跟踪进度;同源互斥、失败可恢复。研究一部作品的章节、实体与写法时使用。
---
# 拆解书稿
把参考作品来源按窗口拆解为版本化分析成果;同源不可并发,失败停在可解释断点。
## 何时使用
- 需要研究参考作品的章节节拍、叙事阶段、实体与写法时。
- 来源更新到新版本,需要重新拆解时。
## 操作步骤
1. 在「拆书工作区」选定来源,写明选书理由(写入拆书依据),或经 `python -m muse 研究 拆解 <请求JSON>` 发起。
2. 台账显示每次拆解的状态、完成窗口、失败窗与歧义数;版本身份由来源版本与窗计划哈希确定。
3. 失败任务点「恢复重试」或 `POST /api/v1/analyses/{task_id}/resume`;已完成窗口不重跑。
4. 任务完成后在「原文与分析对照」逐窗查看原文与分析结果。
## 边界
- 同一来源同时只有一个拆解任务;接管由作用域租约强制。
- 输入漂移的窗口拒绝写入旧分析版本;失败不伪称窗口完成。
- 分析成果是参考作品观察,不自动成为本书设定。

View File

@ -0,0 +1,3 @@
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|------|----------|----------|----------|----------|
| 技能说明 | SKILL.md | 参考作品分窗拆解的发起与进度跟踪 | 研究参考作品章节、实体与写法时 | 同源互斥;失败可恢复;分析不是本书事实 |

View File

@ -0,0 +1,26 @@
---
name: 检查与维护拆书结果
description: 检查拆书台账、歧义与失败窗口,按窗口重算或以新版本重建;旧结论保留可回查。维护分析质量时使用。
---
# 检查与维护拆书结果
对拆书成果做检查与维护:歧义逐项处理、失败窗口重算、版本对照与选用。
## 何时使用
- 台账出现失败窗或歧义时。
- 来源前进到新版本,需要对照或重建分析时。
## 操作步骤
1. `python -m muse 研究 进度 <来源ID>` 或「拆书工作区」台账查看状态。
2. `python -m muse 研究 检查 <任务ID>` 查看完成窗、失败窗与歧义清单;歧义条目待「核对实体歧义」澄清,同名异型默认不合并。
3. 失败窗重算:经恢复入口带 `retry_windows` 指定窗口;已完成窗不重跑,成本包络含重试余量。
4. 来源新版本重建:直接重新发起拆解;旧版本分析保留可回查,版本身份不冲突。
## 边界
- 只撤销本任务可证明归属的中间产物;不触碰外部确认与来源版本。
- 榜单名次与拆解成果都是观察记录,不作效果归因。
- 重建不删除参考原文,不暗改本书事实。

View File

@ -0,0 +1,3 @@
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|------|----------|----------|----------|----------|
| 技能说明 | SKILL.md | 拆书成果的检查、重算与版本维护 | 处理失败窗、歧义与版本对照时 | 歧义不误并;旧结论保留;只撤可证明归属 |

View File

@ -11,3 +11,5 @@
| 确认事实增量 | [确认事实增量/SKILL.md](确认事实增量/SKILL.md) | 章后派生事实提案的作者独立确认 | 采纳或保存正文后处理候选事实提案时 | 只确认已审提案;不代作者决定 | | 确认事实增量 | [确认事实增量/SKILL.md](确认事实增量/SKILL.md) | 章后派生事实提案的作者独立确认 | 采纳或保存正文后处理候选事实提案时 | 只确认已审提案;不代作者决定 |
| 导入资料 | [导入资料/SKILL.md](导入资料/SKILL.md) | 参考书、网页、笔记与灵感的来源导入 | 建立来源、版本与原文对照时 | 授权用途决定消费范围;外部资料不是事实授权 | | 导入资料 | [导入资料/SKILL.md](导入资料/SKILL.md) | 参考书、网页、笔记与灵感的来源导入 | 建立来源、版本与原文对照时 | 授权用途决定消费范围;外部资料不是事实授权 |
| 采集资料与榜单 | [采集资料与榜单/SKILL.md](采集资料与榜单/SKILL.md) | 具名榜单与网页的时点采集 | 榜单观察与网页资料留存时 | 失败如实保留;不补造数据、不作效果归因 | | 采集资料与榜单 | [采集资料与榜单/SKILL.md](采集资料与榜单/SKILL.md) | 具名榜单与网页的时点采集 | 榜单观察与网页资料留存时 | 失败如实保留;不补造数据、不作效果归因 |
| 拆解书稿 | [拆解书稿/SKILL.md](拆解书稿/SKILL.md) | 参考作品分窗拆解的发起与进度 | 研究参考作品章节、实体与写法时 | 同源互斥;失败可恢复;分析不是本书事实 |
| 检查与维护拆书结果 | [检查与维护拆书结果/SKILL.md](检查与维护拆书结果/SKILL.md) | 拆书成果的检查、重算与版本维护 | 处理失败窗、歧义与版本对照时 | 歧义不误并;旧结论保留 |

View File

@ -2496,7 +2496,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/muse______http____________________8" "$ref": "#/components/schemas/muse______http____________________9"
} }
} }
} }
@ -2555,7 +2555,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/muse______http____________________9" "$ref": "#/components/schemas/muse______http____________________10"
} }
} }
} }
@ -3033,6 +3033,146 @@
} }
} }
}, },
"/api/v1/sources/{source_id}/analyses": {
"post": {
"tags": [
"资料研究"
],
"summary": "发起拆书",
"operationId": "start_source_analysis",
"parameters": [
{
"name": "source_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Source Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/muse______http____________________8"
}
}
}
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"title": "Response Start Source Analysis"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"get": {
"tags": [
"资料研究"
],
"summary": "列出拆书",
"operationId": "list_source_analyses",
"parameters": [
{
"name": "source_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Source Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"title": "Response List Source Analyses"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/analyses/{task_id}/resume": {
"post": {
"tags": [
"资料研究"
],
"summary": "恢复拆书",
"operationId": "resume_source_analysis",
"parameters": [
{
"name": "task_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Task Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true,
"title": "Response Resume Source Analysis"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/chapters/{chapter_id}/writing-candidates": { "/api/v1/chapters/{chapter_id}/writing-candidates": {
"get": { "get": {
"tags": [ "tags": [
@ -7907,6 +8047,47 @@
], ],
"title": "会话授权请求" "title": "会话授权请求"
}, },
"muse______http____________________10": {
"properties": {
"report_id": {
"type": "string",
"title": "Report Id"
},
"candidate_id": {
"type": "string",
"title": "Candidate Id"
},
"candidate_revision": {
"type": "integer",
"title": "Candidate Revision"
},
"snapshot_id": {
"type": "string",
"title": "Snapshot Id"
},
"basic": {
"$ref": "#/components/schemas/muse______http__________________3"
},
"continuity": {
"$ref": "#/components/schemas/muse______http___________________5"
},
"verdict": {
"type": "string",
"title": "Verdict"
}
},
"type": "object",
"required": [
"report_id",
"candidate_id",
"candidate_revision",
"snapshot_id",
"basic",
"continuity",
"verdict"
],
"title": "审校报告呈现"
},
"muse______http____________________2": { "muse______http____________________2": {
"properties": { "properties": {
"command_id": { "command_id": {
@ -8165,6 +8346,68 @@
"title": "登记榜单请求" "title": "登记榜单请求"
}, },
"muse______http____________________8": { "muse______http____________________8": {
"properties": {
"command_id": {
"type": "string",
"minLength": 1,
"title": "Command Id"
},
"config_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Config Id"
},
"reason": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Reason"
},
"retry_windows": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Retry Windows"
},
"window_chapters": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Window Chapters"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"command_id"
],
"title": "发起拆书请求"
},
"muse______http____________________9": {
"properties": { "properties": {
"task_id": { "task_id": {
"type": "string", "type": "string",
@ -8181,47 +8424,6 @@
"writing_task_id" "writing_task_id"
], ],
"title": "发起生成回执" "title": "发起生成回执"
},
"muse______http____________________9": {
"properties": {
"report_id": {
"type": "string",
"title": "Report Id"
},
"candidate_id": {
"type": "string",
"title": "Candidate Id"
},
"candidate_revision": {
"type": "integer",
"title": "Candidate Revision"
},
"snapshot_id": {
"type": "string",
"title": "Snapshot Id"
},
"basic": {
"$ref": "#/components/schemas/muse______http__________________3"
},
"continuity": {
"$ref": "#/components/schemas/muse______http___________________5"
},
"verdict": {
"type": "string",
"title": "Verdict"
}
},
"type": "object",
"required": [
"report_id",
"candidate_id",
"candidate_revision",
"snapshot_id",
"basic",
"continuity",
"verdict"
],
"title": "审校报告呈现"
} }
} }
} }

View File

@ -1100,7 +1100,7 @@
| [web/src/功能/资料研究/拆书工作区.tsx](../文件设计/前端-功能工作台.md#file-adfdf2edec303955) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 | | [web/src/功能/资料研究/拆书工作区.tsx](../文件设计/前端-功能工作台.md#file-adfdf2edec303955) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 |
| [web/src/功能/资料研究/数据.ts](../文件设计/前端-功能工作台.md#file-ecd1fd4bf151c343) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 | | [web/src/功能/资料研究/数据.ts](../文件设计/前端-功能工作台.md#file-ecd1fd4bf151c343) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 |
| [web/src/功能/资料研究/榜单与选题页.tsx](../文件设计/前端-功能工作台.md#file-3dc463a2bc9050e8) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 | | [web/src/功能/资料研究/榜单与选题页.tsx](../文件设计/前端-功能工作台.md#file-3dc463a2bc9050e8) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 |
| [web/src/功能/资料研究/资料页.tsx](../文件设计/前端-功能工作台.md#file-529b1c403b83b56f) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 | | [web/src/功能/资料研究/资料页.tsx](../文件设计/前端-应用与通用界面.md#file-529b1c403b83b56f) | [W18](P4-研究与知识资产.md#w18) | [W17](P4-研究与知识资产.md#w17)、[W18](P4-研究与知识资产.md#w18) | 页面随对应真实接口接线,业务约束由后端执行 |
| [web/src/应用/作品会话.ts](../文件设计/前端-应用与通用界面.md#file-5f2e8c5e70fe4284) | [W07](P1-安装与运行底座.md#w07) | [W07](P1-安装与运行底座.md#w07)、[W28](P6-交付与全量恢复.md#w28) | 前端工程、应用框架、客户端或共享展示 | | [web/src/应用/作品会话.ts](../文件设计/前端-应用与通用界面.md#file-5f2e8c5e70fe4284) | [W07](P1-安装与运行底座.md#w07) | [W07](P1-安装与运行底座.md#w07)、[W28](P6-交付与全量恢复.md#w28) | 前端工程、应用框架、客户端或共享展示 |
| [web/src/应用/作者会话.ts](../文件设计/前端-应用与通用界面.md#file-ce6fa23dad8a5cbf) | [W07](P1-安装与运行底座.md#w07) | [W07](P1-安装与运行底座.md#w07)、[W28](P6-交付与全量恢复.md#w28) | 前端工程、应用框架、客户端或共享展示 | | [web/src/应用/作者会话.ts](../文件设计/前端-应用与通用界面.md#file-ce6fa23dad8a5cbf) | [W07](P1-安装与运行底座.md#w07) | [W07](P1-安装与运行底座.md#w07)、[W28](P6-交付与全量恢复.md#w28) | 前端工程、应用框架、客户端或共享展示 |
| [web/src/应用/依赖装配.tsx](../文件设计/前端-应用与通用界面.md#file-d5edc843e3e30305) | [W07](P1-安装与运行底座.md#w07) | [W07](P1-安装与运行底座.md#w07)、[W28](P6-交付与全量恢复.md#w28) | 前端工程、应用框架、客户端或共享展示 | | [web/src/应用/依赖装配.tsx](../文件设计/前端-应用与通用界面.md#file-d5edc843e3e30305) | [W07](P1-安装与运行底座.md#w07) | [W07](P1-安装与运行底座.md#w07)、[W28](P6-交付与全量恢复.md#w28) | 前端工程、应用框架、客户端或共享展示 |

View File

@ -2833,8 +2833,18 @@
"模块设计/B03-资料研究.md", "模块设计/B03-资料研究.md",
"数据模型/旧知识记录分流.md" "数据模型/旧知识记录分流.md"
], ],
"status": "planned", "status": "verified",
"execution_evidence": [], "execution_evidence": [
{
"batch": "R2-20260909",
"date": "2026-09-11",
"summary": "拆书、归并、互斥与补偿整包落地:分章切窗与漂移守卫、逐窗 S02 模型提取、确定性归并(同名异型歧义保留)、分析版本台账(uuid5 身份、失败窗单独重试、旧结论不抹)、同源互斥(排他范围领取层强制)与窗口断点补偿;CLI+HTTP+工作台拆书工作区与对照;12 个新 NC 通过;离线 275、非宿主数据库 231、前端 27;浏览器 2/2+视觉判定(一轮表单遮挡修复后通过)。整包验证一次通过。",
"evidence": [
".agents.local/改造/R2-20260909/W18/回合开发/整包/执行证据.md",
".agents.local/改造/R2-20260909/W18/回合验证/整包/验证报告.md"
]
}
],
"owned_file_paths": [ "owned_file_paths": [
".agent/skills/操作/拆解书稿/SKILL.md", ".agent/skills/操作/拆解书稿/SKILL.md",
".agent/skills/操作/拆解书稿/目录.md", ".agent/skills/操作/拆解书稿/目录.md",

View File

@ -19,7 +19,7 @@ agent-example/
│ ├── vite-env.d.ts # 构建环境类型 │ ├── vite-env.d.ts # 构建环境类型
│ ├── 功能/ │ ├── 功能/
│ │ └── 资料研究/ │ │ └── 资料研究/
│ │ └── 资料对照页.tsx # 来源与原文对照页面 │ │ └── 资料页.tsx # 来源与原文对照页面(W18 起并入研究入口)
│ ├── 应用/ │ ├── 应用/
│ │ ├── 作品会话.ts # 切作品屏障、取消和迟到隔离 │ │ ├── 作品会话.ts # 切作品屏障、取消和迟到隔离
│ │ ├── 作者会话.ts # 作者登录与会话 │ │ ├── 作者会话.ts # 作者登录与会话
@ -749,16 +749,45 @@ agent-example/
- 边界:真实隔离库与真实浏览器;不 mock 后端;视觉判定独立留证。 - 边界:真实隔离库与真实浏览器;不 mock 后端;视觉判定独立留证。
- 目标:通过任务与失败候选的检查呈现及服务端阻断可复核,页面无预期外控制台错误。 - 目标:通过任务与失败候选的检查呈现及服务端阻断可复核,页面无预期外控制台错误。
<a id="file-8d1a7fc626190cb1"></a> <a id="file-529b1c403b83b56f"></a>
### `web/src/功能/资料研究/资料对照页.tsx` ### `web/src/功能/资料研究/资料页.tsx`
- 归属与种类:前端;页面组件。 - 归属与种类:前端;页面组件。
- 意图:让作者在一页内对照来源版本原文、清理候选结果与榜单快照。 - 意图:让作者在一页内对照来源版本原文、清理候选结果并进入研究工作区。
- 职责:来源清单选择、版本原文呈现、清理对照应用、快照列表(失败态如实显示)。 - 职责:来源清单选择、版本原文呈现、清理对照应用、研究入口链接。
- 边界:只读呈现与显式动作;不伪造数据、不推断效果因果。 - 边界:只读呈现与显式动作;不伪造数据、不推断效果因果。
- 目标:来源、原文与快照可追溯;失败可见可重试。 - 目标:来源、原文可追溯;清理对照可见;研究入口可达。
- 合同依据:[B03 资料研究](../模块设计/B03-资料研究.md)。 - 合同依据:[B03 资料研究](../模块设计/B03-资料研究.md)。
- 旧文件承接:取代 W17 的 `资料对照页.tsx`(榜单与拆书职责分页,见 榜单与选题页/拆书工作区)。
<a id="file-adfdf2edec303955"></a>
### `web/src/功能/资料研究/拆书工作区.tsx`
- 归属与种类:前端;页面组件。
- 意图:发起来源拆解、跟踪窗口进度并从失败恢复。
- 职责:来源选择、拆解发起(选书理由/运行配置)、台账与失败窗恢复、原文分析对照入口。
- 边界:同源互斥与漂移错误如实显示;不伪造进度。
- 目标:错误与缺证据可逐项处理;失败可恢复。
<a id="file-862e73b351467608"></a>
### `web/src/功能/资料研究/原文分析对照.tsx`
- 归属与种类:前端;组件。
- 意图:窗口原文与逐窗分析结果并排对照。
- 职责:按码点区间取原文、实体/写法计数与歧义提示呈现。
- 边界:只呈现已登记检查点内容;不补造分析。
<a id="file-3dc463a2bc9050e8"></a>
### `web/src/功能/资料研究/榜单与选题页.tsx`
- 归属与种类:前端;页面组件。
- 意图:榜单时点观察与样本选题依据集中呈现。
- 职责:快照列表(失败如实显示)、样本字数比较、选书理由发起拆解。
- 边界:观察差异只陈述指标;不构成优劣结论或效果因果。
<a id="file-0c134b40a9faae3a"></a> <a id="file-0c134b40a9faae3a"></a>

View File

@ -14223,6 +14223,12 @@
"legacy_sources": [], "legacy_sources": [],
"design_location": "文件设计/测试与夹具.md#file-49d4f4baa42126b9", "design_location": "文件设计/测试与夹具.md#file-49d4f4baa42126b9",
"test_case_refs": [ "test_case_refs": [
"NC-w18-18a01a",
"NC-w18-18a02b",
"NC-w18-18a03c",
"NC-w18-18a04d",
"NC-w18-18a05e",
"NC-w18-18a06f",
"TC-0045c6f3ed48", "TC-0045c6f3ed48",
"TC-030fe0a0b69b", "TC-030fe0a0b69b",
"TC-052b4fa41fd7", "TC-052b4fa41fd7",
@ -16517,6 +16523,8 @@
], ],
"design_location": "文件设计/测试与夹具.md#file-33a0f3ae98d9755f", "design_location": "文件设计/测试与夹具.md#file-33a0f3ae98d9755f",
"test_case_refs": [ "test_case_refs": [
"NC-w18-18b01a",
"NC-w18-18b02b",
"TC-2c9a90e4b1c1", "TC-2c9a90e4b1c1",
"TC-43841d36dba4", "TC-43841d36dba4",
"TC-49f968fe703b", "TC-49f968fe703b",
@ -16667,6 +16675,9 @@
"design_location": "文件设计/测试与夹具.md#file-2c4bcda0dfce8f6e", "design_location": "文件设计/测试与夹具.md#file-2c4bcda0dfce8f6e",
"legacy_sources": [], "legacy_sources": [],
"test_case_refs": [ "test_case_refs": [
"NC-w18-18b03c",
"NC-w18-18b04d",
"NC-w18-18b05e",
"TC-00c1a8dabf0d", "TC-00c1a8dabf0d",
"TC-039b3827b252", "TC-039b3827b252",
"TC-068b740760eb", "TC-068b740760eb",
@ -21591,21 +21602,6 @@
"NC-browser-generation-dialog" "NC-browser-generation-dialog"
] ]
}, },
{
"path": "web/src/功能/资料研究/资料对照页.tsx",
"owner": "前端",
"kind": "源码",
"responsibility": "来源与原文对照页面",
"project_part": "前端",
"intent": "来源与原文对照页面",
"boundary": "只经公开接口与登记输入;不越权、不伪造完成",
"goal": "行为可复验",
"contract_basis": "专属业务职责",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/前端-应用与通用界面.md#file-8d1a7fc626190cb1",
"test_case_refs": []
},
{ {
"path": "web/tests/旅程/资料对照.spec.ts", "path": "web/tests/旅程/资料对照.spec.ts",
"owner": "验证", "owner": "验证",
@ -21620,7 +21616,8 @@
"legacy_sources": [], "legacy_sources": [],
"design_location": "文件设计/前端-应用与通用界面.md#file-0c134b40a9faae3a", "design_location": "文件设计/前端-应用与通用界面.md#file-0c134b40a9faae3a",
"test_case_refs": [ "test_case_refs": [
"NC-w17-browser" "NC-w17-browser",
"NC-w18-browser"
] ]
}, },
{ {
@ -21660,6 +21657,23 @@
"NC-w17-w17b01", "NC-w17-w17b01",
"NC-w17-w17b02" "NC-w17-w17b02"
] ]
},
{
"path": "web/src/功能/资料研究/资料页.tsx",
"owner": "前端",
"kind": "源码",
"responsibility": "来源与原文对照页面(W18 起并入研究入口)",
"project_part": "前端",
"intent": "来源、版本原文与清理对照呈现",
"boundary": "只读呈现与显式动作;不伪造数据、不推断效果因果",
"goal": "行为可复验",
"contract_basis": "专属业务职责",
"status": "目标合同;不表示已实现或已验证",
"legacy_sources": [],
"design_location": "文件设计/前端-应用与通用界面.md#file-529b1c403b83b56f",
"test_case_refs": [
"NC-w17-browser"
]
} }
], ],
"contract_fields": { "contract_fields": {

View File

@ -1305,8 +1305,7 @@ agent-example/
│ │ │ ├── 拆书工作区.tsx # 分析进度与恢复 │ │ │ ├── 拆书工作区.tsx # 分析进度与恢复
│ │ │ ├── 数据.ts # 资料研究查询和业务命令 │ │ │ ├── 数据.ts # 资料研究查询和业务命令
│ │ │ ├── 榜单与选题页.tsx # 快照、比较和选书 │ │ │ ├── 榜单与选题页.tsx # 快照、比较和选书
│ │ │ ├── 资料对照页.tsx # 来源与原文对照页面 │ │ │ └── 资料页.tsx # 来源与原文对照页面(W18 起并入研究入口)
│ │ │ └── 资料页.tsx # 来源导入和版本
│ │ ├── 应用/ │ │ ├── 应用/
│ │ │ ├── 作品会话.ts # 切作品屏障、取消和迟到隔离 │ │ │ ├── 作品会话.ts # 切作品屏障、取消和迟到隔离
│ │ │ ├── 作者会话.ts # 作者登录与会话 │ │ │ ├── 作者会话.ts # 作者登录与会话

View File

@ -3246,3 +3246,15 @@
- 环境:真实隔离库 + Chrome。 - 环境:真实隔离库 + Chrome。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。 - 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-browser-research-workbench"></a>
## NC-w18-browser · NC-browser-research-workbench:拆书工作区发起与台账呈现
- 位置:`web/tests/旅程/资料对照.spec.ts::NC-browser-research-workbench:拆书工作区发起与台账呈现`。
- 给定:隔离浏览器环境与预置来源。
- 动作:打开拆书工作区并按选书理由发起拆解。
- 预期:台账条目出现;进度与失败如实呈现。
- 环境:真实隔离库 + Chrome。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。

View File

@ -123223,6 +123223,198 @@
"environment": "真实隔离库 + Chrome", "environment": "真实隔离库 + Chrome",
"verification_state": "已实现未执行", "verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W17/回合开发/整包/" "execution_evidence": ".agents.local/改造/R2-20260909/W17/回合开发/整包/"
},
{
"case_id": "NC-w18-18a01a",
"file": "tests/单元/test_分章切窗与实体归并.py",
"symbol": "test_切窗覆盖与无重叠__18a01a",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18a01a",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "切窗覆盖与无重叠",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "离线单元",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18a02b",
"file": "tests/单元/test_分章切窗与实体归并.py",
"symbol": "test_切窗漂移拒绝写入__18a02b",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18a02b",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "切窗漂移拒绝写入",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "离线单元",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18a03c",
"file": "tests/单元/test_分章切窗与实体归并.py",
"symbol": "test_无章界整篇单窗__18a03c",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18a03c",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "无章界整篇单窗",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "离线单元",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18a04d",
"file": "tests/单元/test_分章切窗与实体归并.py",
"symbol": "test_同名异型歧义不误并__18a04d",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18a04d",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "同名异型歧义不误并",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "离线单元",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18a05e",
"file": "tests/单元/test_分章切窗与实体归并.py",
"symbol": "test_同型别名带证据合并__18a05e",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18a05e",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "同型别名带证据合并",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "离线单元",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18a06f",
"file": "tests/单元/test_分章切窗与实体归并.py",
"symbol": "test_样本比较是观察陈述__18a06f",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18a06f",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "样本比较是观察陈述",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "离线单元",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18b01a",
"file": "tests/集成/test_同作品抽取互斥.py",
"symbol": "test_同源并发拆解被作用域互斥阻塞__18b01a",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18b01a",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "同源并发拆解被作用域互斥阻塞",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "真实隔离 PG",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18b02b",
"file": "tests/集成/test_同作品抽取互斥.py",
"symbol": "test_失败任务释放范围可重建__18b02b",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18b02b",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "失败任务释放范围可重建",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "真实隔离 PG",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18b03c",
"file": "tests/集成/test_抽取版本与补偿.py",
"symbol": "test_批量原子登记与版本身份__18b03c",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18b03c",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "批量原子登记与版本身份",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "真实隔离 PG",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18b04d",
"file": "tests/集成/test_抽取版本与补偿.py",
"symbol": "test_失败窗口断点续跑只补缺失窗__18b04d",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18b04d",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "失败窗口断点续跑只补缺失窗",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "真实隔离 PG",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-18b05e",
"file": "tests/集成/test_抽取版本与补偿.py",
"symbol": "test_来源前进旧版本结论保留且新拆书指向新版本__18b05e",
"design_location": "验证设计/资料与结构用例.md#nc-w18-18b05e",
"contract": "模块设计/B03-资料研究.md",
"given": "确定性输入或隔离数据库",
"when": "来源前进旧版本结论保留且新拆书指向新版本",
"then": [
"行为可复验",
"失败与歧义如实呈现"
],
"environment": "真实隔离 PG",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
},
{
"case_id": "NC-w18-browser",
"file": "web/tests/旅程/资料对照.spec.ts",
"symbol": "NC-browser-research-workbench:拆书工作区发起与台账呈现",
"design_location": "验证设计/工作台与工程用例.md#nc-browser-research-workbench",
"contract": "模块设计/B03-资料研究.md",
"given": "隔离浏览器环境与预置来源",
"when": "打开拆书工作区并按选书理由发起拆解",
"then": [
"台账条目出现",
"进度与失败如实呈现"
],
"environment": "真实隔离库 + Chrome",
"verification_state": "已实现未执行",
"execution_evidence": ".agents.local/改造/R2-20260909/W18/回合开发/整包/"
} }
] ]
} }

View File

@ -3745,3 +3745,135 @@
- 环境:隔离 PostgreSQL、真实 CLI 与 ASGI。 - 环境:隔离 PostgreSQL、真实 CLI 与 ASGI。
- 合同:[权威定义](../接口契约/元数据投影与版本.md)。 - 合同:[权威定义](../接口契约/元数据投影与版本.md)。
<a id="nc-w18-18a01a"></a>
## NC-w18-18a01a · test_切窗覆盖与无重叠__18a01a
- 位置:`tests/单元/test_分章切窗与实体归并.py::test_切窗覆盖与无重叠__18a01a`。
- 给定:确定性输入或隔离数据库。
- 动作:切窗覆盖与无重叠。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:离线单元。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18a02b"></a>
## NC-w18-18a02b · test_切窗漂移拒绝写入__18a02b
- 位置:`tests/单元/test_分章切窗与实体归并.py::test_切窗漂移拒绝写入__18a02b`。
- 给定:确定性输入或隔离数据库。
- 动作:切窗漂移拒绝写入。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:离线单元。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18a03c"></a>
## NC-w18-18a03c · test_无章界整篇单窗__18a03c
- 位置:`tests/单元/test_分章切窗与实体归并.py::test_无章界整篇单窗__18a03c`。
- 给定:确定性输入或隔离数据库。
- 动作:无章界整篇单窗。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:离线单元。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18a04d"></a>
## NC-w18-18a04d · test_同名异型歧义不误并__18a04d
- 位置:`tests/单元/test_分章切窗与实体归并.py::test_同名异型歧义不误并__18a04d`。
- 给定:确定性输入或隔离数据库。
- 动作:同名异型歧义不误并。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:离线单元。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18a05e"></a>
## NC-w18-18a05e · test_同型别名带证据合并__18a05e
- 位置:`tests/单元/test_分章切窗与实体归并.py::test_同型别名带证据合并__18a05e`。
- 给定:确定性输入或隔离数据库。
- 动作:同型别名带证据合并。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:离线单元。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18a06f"></a>
## NC-w18-18a06f · test_样本比较是观察陈述__18a06f
- 位置:`tests/单元/test_分章切窗与实体归并.py::test_样本比较是观察陈述__18a06f`。
- 给定:确定性输入或隔离数据库。
- 动作:样本比较是观察陈述。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:离线单元。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18b01a"></a>
## NC-w18-18b01a · test_同源并发拆解被作用域互斥阻塞__18b01a
- 位置:`tests/集成/test_同作品抽取互斥.py::test_同源并发拆解被作用域互斥阻塞__18b01a`。
- 给定:确定性输入或隔离数据库。
- 动作:同源并发拆解被作用域互斥阻塞。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:真实隔离 PG。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18b02b"></a>
## NC-w18-18b02b · test_失败任务释放范围可重建__18b02b
- 位置:`tests/集成/test_同作品抽取互斥.py::test_失败任务释放范围可重建__18b02b`。
- 给定:确定性输入或隔离数据库。
- 动作:失败任务释放范围可重建。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:真实隔离 PG。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18b03c"></a>
## NC-w18-18b03c · test_批量原子登记与版本身份__18b03c
- 位置:`tests/集成/test_抽取版本与补偿.py::test_批量原子登记与版本身份__18b03c`。
- 给定:确定性输入或隔离数据库。
- 动作:批量原子登记与版本身份。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:真实隔离 PG。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18b04d"></a>
## NC-w18-18b04d · test_失败窗口断点续跑只补缺失窗__18b04d
- 位置:`tests/集成/test_抽取版本与补偿.py::test_失败窗口断点续跑只补缺失窗__18b04d`。
- 给定:确定性输入或隔离数据库。
- 动作:失败窗口断点续跑只补缺失窗。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:真实隔离 PG。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。
<a id="nc-w18-18b05e"></a>
## NC-w18-18b05e · test_来源前进旧版本结论保留且新拆书指向新版本__18b05e
- 位置:`tests/集成/test_抽取版本与补偿.py::test_来源前进旧版本结论保留且新拆书指向新版本__18b05e`。
- 给定:确定性输入或隔离数据库。
- 动作:来源前进旧版本结论保留且新拆书指向新版本。
- 预期:行为可复验;失败与歧义如实呈现。
- 环境:真实隔离 PG。
- 合同:[权威定义](../模块设计/B03-资料研究.md)。

View File

@ -232,6 +232,23 @@ def 装配章后处理(
流程服务(装配.任务运行, 登记).发布(定义) 流程服务(装配.任务运行, 登记).发布(定义)
def 装配研究拆书(
装配: 应用装配, 计价, 模板路径: Path | None = None, *, 分析配置ID: str | None = None
) -> None:
"""登记并发布研究拆书流程;分析配置由部署装配声明,作者配置仍走验证启用链。"""
from muse.编排.接口 import 流程服务
from muse.编排.研究拆书 import 登记研究拆书
from muse.编排.载入模板 import 载入流程定义
登记 = 装配.流程登记
if 登记 is None or 装配.任务运行 is None:
raise 装配未完成("任务运行与流程登记未注入")
登记研究拆书(登记, 装配, 计价, 分析配置ID=分析配置ID)
定义 = 载入流程定义(模板路径 or Path("配置/流程模板/研究拆书.yaml"))
流程服务(装配.任务运行, 登记).发布(定义)
def 装配生成正文(装配: 应用装配, 计价, 模板路径: Path | None = None) -> None: def 装配生成正文(装配: 应用装配, 计价, 模板路径: Path | None = None) -> None:
"""登记并发布生成正文流程;计价由调用方注入,模板默认取仓内可审配置。""" """登记并发布生成正文流程;计价由调用方注入,模板默认取仓内可审配置。"""

View File

@ -45,6 +45,12 @@ def _解析参数(argv: list[str] | None) -> argparse.Namespace:
创作解析.add_argument("动作", choices=["发起", "报告"]) 创作解析.add_argument("动作", choices=["发起", "报告"])
创作解析.add_argument("目标", nargs="?", help="发起时为请求 JSON 文件;报告时为候选 ID") 创作解析.add_argument("目标", nargs="?", help="发起时为请求 JSON 文件;报告时为候选 ID")
创作解析.add_argument("--版本", type=int, default=None, help="报告的目标候选版本") 创作解析.add_argument("--版本", type=int, default=None, help="报告的目标候选版本")
研究解析 = 子命令.add_parser("研究", help="发起资料拆解、查看进度与检查拆书结果")
研究解析.add_argument("配置", help="作者配置 TOML 路径")
研究解析.add_argument("动作", choices=["拆解", "进度", "检查"])
研究解析.add_argument(
"目标", nargs="?", help="拆解为请求 JSON 文件;进度为来源 ID;检查为任务 ID"
)
结构解析 = 子命令.add_parser("结构", help="发布内置种子或查看已发布的固定结构") 结构解析 = 子命令.add_parser("结构", help="发布内置种子或查看已发布的固定结构")
结构解析.add_argument("配置", help="应用配置 TOML 路径") 结构解析.add_argument("配置", help="应用配置 TOML 路径")
结构解析.add_argument("动作", choices=["发布内置", "查看", "维护"]) 结构解析.add_argument("动作", choices=["发布内置", "查看", "维护"])
@ -207,6 +213,12 @@ def main(argv: list[str] | None = None) -> int:
结果 = 运行创作命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 版本=参数.版本) 结果 = 运行创作命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 版本=参数.版本)
print(json.dumps(结果, ensure_ascii=False, default=str)) print(json.dumps(结果, ensure_ascii=False, default=str))
return _退出码_成功 return _退出码_成功
if 参数.命令 == "研究":
from muse.接入.cli.研究命令 import 运行研究命令
结果 = 运行研究命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标)
print(json.dumps(结果, ensure_ascii=False, default=str))
return _退出码_成功
if 参数.命令 == "自检": if 参数.命令 == "自检":
装配 = 构建(读取配置(参数.配置)) 装配 = 构建(读取配置(参数.配置))
for 键, 值 in 自检(装配).items(): for 键, 值 in 自检(装配).items():

View File

@ -0,0 +1,98 @@
"""研究命令:拆解发起、进度查询与结果检查维护的命令入口。"""
from __future__ import annotations
from pathlib import Path
from pydantic import BaseModel, ConfigDict, ValidationError
from muse.共享.错误 import 配置错误
from muse.启动 import 应用装配
from muse.接入.创作操作 import 创作身份
class 拆解请求(BaseModel):
model_config = ConfigDict(populate_by_name=True)
command_id: str
source_id: str
config_id: str | None = None
reason: str | None = None
retry_windows: list[str] | None = None
window_chapters: int | None = None
@property
def 命令ID(self) -> str:
return self.command_id
@property
def 来源ID(self) -> str:
return self.source_id
@property
def 配置ID(self) -> str | None:
return self.config_id
@property
def 理由(self) -> str | None:
return self.reason
@property
def 重跑窗(self) -> list[str] | None:
return self.retry_windows
@property
def 窗口章数(self) -> int | None:
return self.window_chapters
def 运行研究命令(装配: 应用装配, 动作: str, 目标: str | None) -> dict | list:
if 装配.配置.HTTP is None:
raise 配置错误("研究命令需要本地作者身份配置")
作者 = 创作身份(装配.配置.HTTP.作者ID, 装配.配置)
if 动作 == "拆解":
if not 目标:
raise 配置错误("拆解命令需要请求 JSON 文件")
try:
请求 = 拆解请求.model_validate_json(Path(目标).read_text(encoding="utf-8"))
except ValidationError as exc:
from muse.接入.主会话 import 参数合同错误
raise 参数合同错误(exc.errors(), 前缀=("body",)) from None
from muse.编排.研究拆书 import 发起研究拆书
样本 = {"理由": 请求.理由} if 请求.理由 else None
return 发起研究拆书(
装配,
作者,
请求.命令ID,
source_id=请求.来源ID,
样本依据=样本,
配置ID=请求.配置ID,
重跑窗=请求.重跑窗,
窗口章数=请求.窗口章数,
)
if 动作 == "进度":
if not 目标:
raise 配置错误("进度命令需要来源身份")
from muse.编排.研究拆书 import 查询拆书状态
状态 = 查询拆书状态(装配, 作者.作者 or "", 目标)
return 状态 or {"台账": [], "对照": None, "说明": "该来源尚无拆书任务"}
if 动作 == "检查":
if not 目标:
raise 配置错误("检查命令需要任务身份")
from muse.资料研究.分析版本 import 读取版本状态
if 装配.任务运行 is None:
raise 配置错误("任务运行未注入")
快照 = 装配.任务运行.读取任务(目标)
if 快照.流程.流程ID != "研究拆书":
raise 配置错误(f"任务 {目标} 不是研究拆书任务")
状态 = 读取版本状态(快照)
状态["说明"] = "歧义条目待核对实体歧义任务澄清;失败窗可经重跑窗恢复。"
return 状态
raise 配置错误(f"未知研究命令动作:{动作}")
__all__ = ["运行研究命令", "拆解请求"]

View File

@ -1,4 +1,4 @@
"""资料研究的 HTTP 外壳:导入、来源版本对照、清理候选与榜单快照。""" """资料研究的 HTTP 外壳:导入、来源版本对照、清理候选、榜单快照与研究拆书。"""
from typing import Any, Literal from typing import Any, Literal
@ -38,6 +38,15 @@ class 采集榜单请求(BaseModel):
ranking_id: str = Field(min_length=1) ranking_id: str = Field(min_length=1)
class 发起拆书请求(BaseModel):
model_config = ConfigDict(extra="forbid")
command_id: str = Field(min_length=1)
config_id: str | None = None
reason: str | None = None
retry_windows: list[str] | None = None
window_chapters: int | None = None
def _装配(request: Request): def _装配(request: Request):
return request.app.state.装配 return request.app.state.装配
@ -157,3 +166,49 @@ def 列出快照(ranking_id: str, request: Request, 作者: 作者依赖) -> lis
装配 = _装配(request) 装配 = _装配(request)
with 装配.要求数据库().连接(只读=True) as 连: with 装配.要求数据库().连接(只读=True) as 连:
return 装配.要求资料().列出快照(连, ranking_id) return 装配.要求资料().列出快照(连, ranking_id)
# ---- 研究拆书 ----
@路由.post("/sources/{source_id}/analyses", operation_id="start_source_analysis", status_code=201)
def 发起拆书(source_id: str, 请求: 发起拆书请求, request: Request, 作者: 作者依赖) -> dict:
装配 = _装配(request)
from muse.编排.研究拆书 import 发起研究拆书
样本 = {"理由": 请求.reason} if 请求.reason else None
回执 = 发起研究拆书(
装配,
创作身份(作者, 装配.配置),
请求.command_id,
source_id=source_id,
样本依据=样本,
配置ID=请求.config_id,
重跑窗=请求.retry_windows,
窗口章数=请求.window_chapters,
)
return 回执
@路由.get("/sources/{source_id}/analyses", operation_id="list_source_analyses")
def 列出拆书(source_id: str, request: Request, 作者: 作者依赖) -> dict[str, Any]:
装配 = _装配(request)
from muse.编排.研究拆书 import 查询拆书状态
状态 = 查询拆书状态(装配, 创作身份(作者, 装配.配置).作者 or "", source_id)
return 状态 or {"台账": [], "对照": None, "说明": "该来源尚无拆书任务"}
@路由.post("/analyses/{task_id}/resume", operation_id="resume_source_analysis")
def 恢复拆书(task_id: str, request: Request, 作者: 作者依赖) -> dict:
装配 = _装配(request)
from muse.任务运行.模型 import 任务状态
快照 = 装配.任务运行.控制任务(
task_id,
创作身份(作者, 装配.配置).作者,
任务状态.已失败,
动作="恢复",
命令ID="resume-" + task_id,
)
return {"task_id": 快照.任务ID, "状态": 快照.状态.value}

View File

@ -0,0 +1,359 @@
"""研究拆书旅程:切窗核对、分析配置绑定、逐窗模型提取、实体归并与分析版本登记。
拆书对象是参考作品来源:任务以来源排他范围互斥(同源不可并发拆书);
窗口结果绑定窗哈希(输入漂移即拒绝),失败窗口可单独重试且不覆盖已完成窗;
归并结果每次按检查点重建,只保留本任务可证明归属的成果,不触碰外部确认。
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from typing import Any
from muse.任务运行.接口 import (
任务快照,
执行上下文,
步骤处理器,
步骤结果,
步骤计划,
)
from muse.任务运行.模型 import 任务请求, 作用域
from muse.任务运行.预算管理 import (
任务预算计划,
角色预算,
预算状态冲突,
预算管理,
)
from muse.共享.调用身份 import 内容用途
from muse.共享.错误 import Muse错误, 配置错误
from muse.编排.接口 import 流程登记
from muse.编排.流程版本 import 流程定义
from muse.编排.章后处理 import _工厂, 前步检查点
from muse.资料研究.分章切窗 import 切窗, 漂移检查, 窗口, 窗口载荷, 窗计划哈希, 覆盖核对
from muse.资料研究.存储 import 资料存储
from muse.资料研究.实体归并 import 归并, 收集实体
from muse.资料研究.样本选题 import 选书交接单
流程身份 = "研究拆书"
流程版本 = "1"
处理器版本集 = {
"rs.plan": "1",
"rs.config": "1",
"rs.extract": "1",
"rs.merge": "1",
"rs.version": "1",
}
class 拆书错误(Muse错误):
错误码 = "ANALYSIS_FLOW_INVALID"
def 拆书输入(任务: 任务快照) -> dict[str, Any]:
输入 = (任务.冻结输入.get("输入") or {}).get("拆书") or {}
for 名 in ("source_id", "revision", "content_hash"):
if 名 not in 输入:
raise 拆书错误(f"拆书冻结输入缺少 {名}")
return 输入
def 窗计划(任务: 任务快照) -> list[dict[str, Any]]:
计划 = (任务.冻结输入.get("输入") or {}).get("窗计划") or []
if not 计划:
raise 拆书错误("拆书冻结输入缺少窗计划")
return 计划
def _来源复检(装配, 输入: dict[str, Any]) -> str:
"""来源版本与内容哈希一致才放行;不一致即旧分析不能适用。"""
with _工厂(装配).连接(只读=True) as 连:
行 = 资料存储(连).读版本(输入["source_id"], int(输入["revision"]))
if 行 is None:
raise 拆书错误(f"来源版本不存在:{输入['source_id']}@r{输入['revision']}")
if 行["content_hash"] != 输入["content_hash"]:
raise 拆书错误("来源内容与冻结哈希不一致,旧分析结果不能适用;按新版本重建")
return str(行["content"])
def 登记研究拆书(登记: 流程登记, 装配, 计价, *, 分析配置ID: str | None = None) -> None:
"""登记研究拆书流程处理器、类型与恢复检查;分析配置由部署装配声明。"""
登记.登记类型(流程身份, 必需保护=())
def 恢复复检(任务: 任务快照) -> None:
_来源复检(装配, 拆书输入(任务))
登记.登记恢复检查(流程身份, 流程版本, 恢复复检)
def 切窗核对(上下文: 执行上下文) -> 步骤结果:
输入 = 拆书输入(上下文.任务)
文本 = _来源复检(装配, 输入)
计划 = 窗计划(上下文.任务)
# 每个冻结窗与当前原文逐一核对;漂移窗口名单留待逐窗提取阶段拒绝。
漂移窗: list[str] = []
for 窗 in 计划:
try:
漂移检查(文本, 窗口(**窗))
except Muse错误:
漂移窗.append(窗["窗ID"])
统计 = 覆盖核对(文本, [窗口(**窗) for 窗 in 计划])
return 步骤结果(
输出={"窗口数": len(计划), "漂移窗": 漂移窗, **统计},
检查点={"窗核对": {"漂移窗": 漂移窗, **统计}},
)
def 绑定分析配置(上下文: 执行上下文) -> 步骤结果:
任务 = 上下文.任务
配置ID = (任务.冻结输入.get("输入") or {}).get("配置ID") or 分析配置ID
if not 配置ID:
raise 配置错误("拆书分析未绑定运行配置;发起时经入口明确指定")
from muse.任务运行.配置版本 import 配置版本管理
工厂 = _工厂(装配)
快照 = 配置版本管理(工厂).冻结到任务(任务.任务ID, 配置ID)
窗口数 = len(窗计划(任务))
预算 = 预算管理(工厂, 快照.内容.预算策略引用)
try:
# 包络随窗数走:计划=每窗一次加重试余量两次;安全容量=两倍窗数加余量。
预算.登记任务预算(
任务.任务ID,
任务预算计划(
Decimal("2") * (窗口数 + 2),
(角色预算("extractor", 窗口数 + 2, 2 * 窗口数 + 2, Decimal("2")),),
"research-analysis-envelope-1",
datetime.now(UTC) + timedelta(hours=12),
),
)
except 预算状态冲突:
pass # 恢复重试时包络已登记且哈希一致;不一致会被上一步骤拒绝。
return 步骤结果(
输出={"配置ID": 配置ID},
检查点={"配置ID": 配置ID, "预算账户": 快照.内容.预算策略引用},
)
def 逐窗提取(上下文: 执行上下文) -> 步骤结果:
from muse.资料研究.拆书分析 import 执行窗口提取
任务 = 上下文.任务
输入 = 拆书输入(任务)
文本 = _来源复检(装配, 输入)
重跑 = set((任务.冻结输入.get("输入") or {}).get("重跑窗") or [])
# 断点续跑:已完成窗保留;指定重跑窗或漂移窗按归属重建,不保留错归属结果。
保留: dict[str, Any] = {}
for 项 in 任务.步骤:
检查点 = 项.get("checkpoint") or {}
if 项["step_id"] == "逐窗提取" and 检查点.get("窗口结果"):
for 窗ID, 结果 in 检查点["窗口结果"].items():
if 窗ID in 重跑 or 窗ID in (检查点.get("漂移重建") or []):
continue
保留[窗ID] = 结果
计划 = 窗计划(任务)
结果: dict[str, Any] = dict(保留)
漂移重建: list[str] = []
for 窗 in 计划:
if 窗["窗ID"] in 结果:
continue
try:
漂移检查(文本, 窗口(**窗))
except Muse错误:
# 输入漂移:不调用模型、不保留旧归属,登记后由恢复按新版本重建。
漂移重建.append(窗["窗ID"])
continue
结果[窗["窗ID"]] = 执行窗口提取(装配, 上下文, 计价, 窗, 文本)
装配.任务运行.保存检查点(
上下文.领取,
{
"窗口结果": 结果,
"漂移重建": 漂移重建,
"已提取窗数": len(结果),
},
)
if 漂移重建:
raise 拆书错误(f"窗口输入漂移,拒绝写入旧分析结果:{'、'.join(漂移重建)}")
return 步骤结果(
输出={"已提取窗数": len(结果), "窗口总数": len(计划)},
检查点={"窗口结果": 结果, "漂移重建": 漂移重建, "已提取窗数": len(结果)},
)
def 实体归并步(上下文: 执行上下文) -> 步骤结果:
窗口结果 = 前步检查点(上下文, "逐窗提取")["窗口结果"]
输入序列 = [{**结果, "window_id": 窗ID} for 窗ID, 结果 in sorted(窗口结果.items())]
归并结果 = 归并(收集实体(输入序列))
return 步骤结果(
输出={"合并组数": len(归并结果["合并组"]), "歧义数": len(归并结果["歧义"])},
检查点={"归并": 归并结果},
)
def 登记分析版本(上下文: 执行上下文) -> 步骤结果:
from muse.资料研究.分析版本 import 版本身份
任务 = 上下文.任务
输入 = 拆书输入(任务)
归并点 = 前步检查点(上下文, "实体归并")["归并"]
窗口结果 = 前步检查点(上下文, "逐窗提取")["窗口结果"]
窗口列表 = [窗口(**窗) for 窗 in 窗计划(任务)]
样本 = (任务.冻结输入.get("输入") or {}).get("样本依据")
版本 = {
"版本ID": 版本身份(输入["source_id"], int(输入["revision"]), 窗口列表),
"source_id": 输入["source_id"],
"revision": int(输入["revision"]),
"content_hash": 输入["content_hash"],
"窗计划哈希": 窗计划哈希(窗口列表),
"完成窗": sorted(窗口结果),
"实体组数": len(归并点["合并组"]),
"歧义数": len(归并点["歧义"]),
"样本依据": 样本,
}
return 步骤结果(
输出={"版本ID": 版本["版本ID"], "歧义数": 版本["歧义数"]},
检查点={"版本": 版本},
)
登记.登记处理器(步骤处理器("rs.plan", "1", 切窗核对, "post-source", "post-source"))
登记.登记处理器(步骤处理器("rs.config", "1", 绑定分析配置, "post-source", "post-config"))
登记.登记处理器(
步骤处理器("rs.extract", "1", 逐窗提取, "post-config", "post-extraction", 角色="extractor")
)
登记.登记处理器(步骤处理器("rs.merge", "1", 实体归并步, "post-extraction", "post-proposals"))
登记.登记处理器(步骤处理器("rs.version", "1", 登记分析版本, "post-proposals", "post-summary"))
def 研究拆书步骤() -> tuple[步骤计划, ...]:
return (
步骤计划("切窗核对", "rs.plan", "1"),
步骤计划("绑定分析配置", "rs.config", "1", 依赖=("切窗核对",)),
步骤计划("逐窗提取", "rs.extract", "1", 依赖=("绑定分析配置",), 角色="extractor"),
步骤计划("实体归并", "rs.merge", "1", 依赖=("逐窗提取",)),
步骤计划("分析版本登记", "rs.version", "1", 依赖=("实体归并",)),
)
def 流程定义_研究拆书() -> 流程定义:
return 流程定义(流程身份, 流程版本, 研究拆书步骤())
def 发起研究拆书(
装配,
身份,
命令ID: str,
*,
source_id: str,
样本依据: dict[str, Any] | None = None,
配置ID: str | None = None,
重跑窗: list[str] | None = None,
窗口章数: int | None = None,
) -> dict[str, str]:
"""按来源当前版本登记拆书任务;同源互斥由排他范围强制,同窗计划得到同版本身份。"""
from muse.任务运行.角色策略 import 角色策略目录
from muse.资料研究.来源导入 import 核对来源授权
with _工厂(装配).连接(只读=True) as 连:
行 = 资料存储(连).读版本(source_id)
if 行 is None:
raise 拆书错误(f"来源不存在:{source_id}")
核对来源授权(连, source_id, "analysis")
文本 = str(行["content"])
参数 = {} if 窗口章数 is None else {"窗口章数": 窗口章数}
窗口列表, 计划哈希 = 切窗(文本, **参数)
交接单 = 选书交接单(
{
"source_id": source_id,
"revision": int(行["revision"]),
"content_hash": 行["content_hash"],
},
(样本依据 or {}).get("理由") or "按来源当前版本直接拆解",
)
冻结输入 = {
"拆书": {
"source_id": source_id,
"revision": int(行["revision"]),
"content_hash": 行["content_hash"],
},
"窗计划": 窗口载荷(窗口列表),
"样本依据": {**交接单, "依据哈希": (样本依据 or {}).get("依据哈希")},
"配置ID": 配置ID,
"重跑窗": sorted(重跑窗 or []),
}
策略 = 角色策略目录.从发布包()
请求 = 任务请求(
任务类型=流程身份,
命令ID=命令ID,
作者=身份.作者,
执行用途=装配.配置.运行用途,
内容用途=内容用途.抽取,
输入=冻结输入,
角色策略版本=策略.定义["version"],
资源发布身份=策略.资源发布身份,
冻结上下文={
"source_scope": 冻结输入,
"schema_versions": {},
"authorization": "research-analysis",
"budget": {},
"stop_conditions": {},
},
来源ID=source_id,
排他范围=作用域(对象类型="source", 对象ID=source_id, 操作族="analysis"),
)
任务ID = 装配.任务运行.创建任务(请求, 流程身份, 流程版本)
if 配置ID:
from muse.任务运行.配置版本 import 配置版本管理
配置版本管理(_工厂(装配)).冻结到任务(任务ID, 配置ID)
return {
"task_id": 任务ID,
"版本ID": _预计算版本ID(source_id, int(行["revision"]), 窗口列表),
"窗计划哈希": 计划哈希,
}
def _预计算版本ID(source_id: str, revision: int, 窗口列表) -> str:
from muse.资料研究.分析版本 import 版本身份
return 版本身份(source_id, revision, 窗口列表)
def 查询拆书状态(装配, 作者: str, source_id: str) -> dict[str, Any] | None:
"""返回该来源的拆书台账与最近一次任务的对照数据;无拆书时返回 None。"""
from muse.资料研究.分析版本 import 版本台账, 读取版本状态
服务 = 装配.任务运行
台账 = 版本台账(服务.列出任务(作者))
本源 = [项 for 项 in 台账 if 项["source_id"] == source_id]
if not 本源:
return None
最新 = None
for 快照 in 服务.列出任务(作者):
if 快照.流程.流程ID == 流程身份 and 拆书输入(快照)["source_id"] == source_id:
最新 = 快照
break
对照 = None
if 最新 is not None:
状态 = 读取版本状态(最新)
对照 = {
"task_id": 最新.任务ID,
"状态": 最新.状态.value,
"版本ID": (状态["版本"] or {}).get("版本ID"),
"完成窗": 状态["完成窗"],
"失败窗": 状态["失败窗"],
"歧义": 状态["歧义"],
"样本依据": (最新.冻结输入.get("输入") or {}).get("样本依据"),
}
return {"台账": 本源, "对照": 对照}
__all__ = [
"登记研究拆书",
"发起研究拆书",
"查询拆书状态",
"流程定义_研究拆书",
"研究拆书步骤",
"拆书错误",
"流程身份",
"流程版本",
"处理器版本集",
]

View File

@ -0,0 +1,101 @@
"""分析版本:分窗成果的版本台账与恢复状态;失败窗口可单独重试,选用版本可回查。
版本本体落在 S02 任务检查点(W18 无新迁移);版本身份由来源版本与窗计划哈希
确定性推导,同一来源版本的重复分析得到同一版本身份,可回查、可对照。
"""
from __future__ import annotations
from typing import Any
from uuid import NAMESPACE_URL, uuid5
from muse.资料研究.分章切窗 import 窗口, 窗计划哈希
from muse.资料研究.模型 import 资料错误
def 版本身份(source_id: str, revision: int, 窗口列表: list[窗口]) -> str:
"""来源版本 + 窗计划哈希 → 确定性分析版本身份。"""
if not 窗口列表:
raise 资料错误("窗口列表为空,不能立分析版本")
return str(uuid5(NAMESPACE_URL, f"analysis:{source_id}:{int(revision)}:{窗计划哈希(窗口列表)}"))
def 读取版本状态(任务快照) -> dict[str, Any]:
"""从拆书任务的步骤检查点汇总版本状态:完成窗、失败窗、歧义与版本身份。"""
状态: dict[str, Any] = {
"task_id": 任务快照.任务ID,
"状态": 任务快照.状态.value,
"完成窗": {},
"失败窗": [],
"歧义": [],
"版本": None,
}
输入 = (任务快照.冻结输入.get("输入") or {}).get("拆书") or {}
状态["source_id"] = 输入.get("source_id")
状态["revision"] = 输入.get("revision")
窗计划 = (任务快照.冻结输入.get("输入") or {}).get("窗计划") or []
状态["窗计划哈希"] = 窗计划哈希([窗口(**窗) for 窗 in 窗计划]) if 窗计划 else None
for 项 in 任务快照.步骤:
检查点 = 项.get("checkpoint") or {}
if 项["step_id"] == "逐窗提取" and 检查点.get("窗口结果"):
for 窗ID, 结果 in 检查点["窗口结果"].items():
状态["完成窗"][窗ID] = {
"实体数": len(结果.get("entities") or []),
"写法数": len(结果.get("techniques") or []),
}
状态["失败窗"] = sorted(检查点.get("失败窗") or [])
if 项["step_id"] == "实体归并" and 检查点.get("归并"):
状态["歧义"] = 检查点["归并"].get("歧义") or []
if 项["step_id"] == "分析版本登记" and 检查点.get("版本"):
状态["版本"] = 检查点["版本"]
return 状态
def 待重试窗口(任务快照) -> list[str]:
"""恢复时需要重跑的窗口:无结果或与冻结窗计划不一致的窗口。"""
输入 = (任务快照.冻结输入.get("输入") or {}).get("拆书") or {}
全部 = [窗["窗ID"] for 窗 in (任务快照.冻结输入.get("输入") or {}).get("窗计划") or []]
完成: set[str] = set()
for 项 in 任务快照.步骤:
检查点 = 项.get("checkpoint") or {}
if 项["step_id"] == "逐窗提取":
完成 |= set(检查点.get("窗口结果") or {})
if 输入.get("重跑窗"):
return [窗ID for 窗ID in 全部 if 窗ID in set(输入["重跑窗"])]
return [窗ID for 窗ID in 全部 if 窗ID not in 完成]
def 版本台账(任务快照列表: list) -> list[dict[str, Any]]:
"""列出同一来源版本的历史分析任务及其版本身份与状态。"""
台账: list[dict[str, Any]] = []
for 快照 in 任务快照列表:
if 快照.流程.流程ID != "研究拆书":
continue
状态 = 读取版本状态(快照)
台账.append(
{
"task_id": 状态["task_id"],
"状态": 状态["状态"],
"source_id": 状态["source_id"],
"revision": 状态["revision"],
"窗计划哈希": 状态["窗计划哈希"],
"版本ID": (状态["版本"] or {}).get("版本ID"),
"完成窗": len(状态["完成窗"]),
"失败窗": 状态["失败窗"],
"歧义数": len(状态["歧义"]),
}
)
return 台账
def 基准引用(source_id: str, revision: int, 版本ID: str) -> dict[str, Any]:
"""下游任务消费某分析版本时的基准引用;选用记录随任务冻结输入可回查。"""
return {"基准版本": {"source_id": source_id, "revision": int(revision), "版本ID": 版本ID}}
__all__ = ["版本身份", "读取版本状态", "待重试窗口", "版本台账", "基准引用", "资料错误"]

View File

@ -0,0 +1,117 @@
"""分章切窗:长文分析的分批与恢复边界;窗口覆盖可检查,输入漂移不能写入旧分析版本。"""
from __future__ import annotations
import hashlib
import re
from dataclasses import asdict, dataclass
from muse.资料研究.模型 import 资料错误
# 章界识别用确定性分隔模式;命中失败时整篇按单章处理,不用模型补猜。
_章界模式 = re.compile(
r"^[ \t]*(?:第[0-9零一二三四五六七八九十百千两]+[章回节卷][^\n]{0,30})[ \t]*$", re.M
)
默认窗口章数 = 3
@dataclass(frozen=True, slots=True)
class 窗口:
"""一个分析窗口:码点区间绑定内容哈希;窗号只是切分序号,不是真实章号。"""
窗ID: str
起章: int
止章: int
起点: int
终点: int
内容哈希: str
def 分章(文本: str) -> list[dict[str, int]]:
"""按章界模式切分文本;返回章序号与码点区间,无章界时整篇为一章。"""
命中 = [匹配.start() for 匹配 in _章界模式.finditer(文本)]
界 = [起点 for 起点 in 命中 if 起点 > 0]
if not 界:
return [{"章": 1, "起点": 0, "终点": len(文本)}]
区间: list[dict[str, int]] = []
边 = [0] + 界 + [len(文本)]
for 序, (起, 止) in enumerate(zip(边, 边[1:], strict=False), 1):
if 起 < 止:
区间.append({"章": 序, "起点": 起, "终点": 止})
return 区间
def 切窗(文本: str, *, 窗口章数: int = 默认窗口章数) -> tuple[list[窗口], str]:
"""把分章文本切成连续窗口;返回窗口列表与窗计划哈希。"""
if 窗口章数 < 1:
raise 资料错误("窗口章数必须大于零")
章 = 分章(文本)
窗口列表: list[窗口] = []
for 序 in range(0, len(章), 窗口章数):
组 = 章[序 : 序 + 窗口章数]
起点, 终点 = 组[0]["起点"], 组[-1]["终点"]
片段 = 文本[起点:终点]
窗口列表.append(
窗口(
窗ID=f"w{len(窗口列表) + 1:03d}",
起章=组[0]["章"],
止章=组[-1]["章"],
起点=起点,
终点=终点,
内容哈希=hashlib.sha256(片段.encode("utf-8")).hexdigest(),
)
)
return 窗口列表, 窗计划哈希(窗口列表)
def 窗计划哈希(窗口列表: list[窗口]) -> str:
return hashlib.sha256(
"\n".join(
f"{窗口.窗ID}:{窗口.起点}:{窗口.终点}:{窗口.内容哈希}" for 窗口 in 窗口列表
).encode()
).hexdigest()
def 漂移检查(文本: str, 窗: 窗口) -> None:
"""窗口当前内容与冻结哈希一致才放行;不一致即输入已漂移,拒绝写入旧分析版本。"""
if not (0 <= 窗.起点 < 窗.终点 <= len(文本)):
raise 资料错误(f"窗口 {窗.窗ID} 区间越界,输入已漂移")
当前 = hashlib.sha256(文本[窗.起点 : 窗.终点].encode("utf-8")).hexdigest()
if 当前 != 窗.内容哈希:
raise 资料错误(f"窗口 {窗.窗ID} 内容与冻结哈希不一致,输入已漂移")
def 覆盖核对(文本: str, 窗口列表: list[窗口]) -> dict[str, int]:
"""窗口应无重叠且完整覆盖正文;返回核对统计供检查与维护使用。"""
排序 = sorted(窗口列表, key=lambda 窗: 窗.起点)
覆盖 = 0
游标 = 0
重叠 = 0
for 窗 in 排序:
if 窗.起点 < 游标:
重叠 += 游标 - 窗.起点
覆盖 += max(0, 窗.终点 - max(窗.起点, 游标))
游标 = max(游标, 窗.终点)
return {"窗口数": len(排序), "覆盖码点": 覆盖, "重叠码点": 重叠, "正文码点": len(文本)}
def 窗口载荷(窗口列表: list[窗口]) -> list[dict]:
return [asdict(窗) for 窗 in 窗口列表]
__all__ = [
"窗口",
"分章",
"切窗",
"窗计划哈希",
"漂移检查",
"覆盖核对",
"窗口载荷",
"默认窗口章数",
"资料错误",
]

View File

@ -0,0 +1,100 @@
"""实体归并:参考作品内部专名、别名与歧义的确定性归并;可疑同名不覆盖已有实体。"""
from __future__ import annotations
import hashlib
from typing import Any
from muse.资料研究.模型 import 资料错误
def 收集实体(窗口结果: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""把逐窗分析的实体展开为带窗口归属与证据的候选记录。"""
候选: list[dict[str, Any]] = []
for 结果 in 窗口结果:
for 项 in 结果.get("entities") or []:
名 = (项.get("name") or "").strip()
if not 名:
raise 资料错误("实体缺少名称")
候选.append(
{
"name": 名,
"type": (项.get("type") or "unknown").strip() or "unknown",
"aliases": [别名 for 别名 in (项.get("aliases") or []) if 别名],
"evidence": list(项.get("evidence") or ()),
"window_id": 结果.get("window_id"),
}
)
return 候选
def 归并(候选: list[dict[str, Any]]) -> dict[str, Any]:
"""同类型且名称(规范形)一致的候选合并为一组并保留证据;同名异型一律歧义。
规则完全确定性:候选按 (规范化名, 类型) 分组,别名只扩入组的别名集,不另立组;
同一规范名出现在多个类型下时整体登记歧义,不猜测、不投票、不越作品合并。
"""
def 规范(名: str) -> str:
return 名.strip().lower()
组: dict[tuple[str, str], list[dict[str, Any]]] = {}
for 项 in 候选:
组.setdefault((规范(项["name"]), 项["type"]), []).append(项)
按名: dict[str, set[str]] = {}
for 键名, 类型 in 组:
按名.setdefault(键名, set()).add(类型)
合并组: list[dict[str, Any]] = []
歧义: list[dict[str, Any]] = []
for (键名, 类型), 成员 in sorted(组.items()):
if len(按名[键名]) > 1:
出现窗 = sorted(
{
项["window_id"]
for 类型2 in 按名[键名]
for 项 in 组.get((键名, 类型2), [])
if 项["window_id"]
}
)
歧义.append(
{
"名": 键名,
"类型冲突": sorted(按名[键名]),
"出现窗": 出现窗,
"处理": "歧义保留;待核对实体歧义任务澄清",
}
)
continue
合并组.append(
{
"canonical": 成员[0]["name"],
"type": 类型,
"aliases": sorted(
{项["name"] for 项 in 成员}
| {别名 for 项 in 成员 for 别名 in 项["aliases"]} - {成员[0]["name"]}
),
"windows": sorted({项["window_id"] for 项 in 成员 if 项["window_id"]}),
"evidence": [证据 for 项 in 成员 for 证据 in 项["evidence"]],
}
)
return {
"合并组": sorted(合并组, key=lambda g: g["canonical"]),
"歧义": sorted(歧义, key=lambda g: g["名"]),
}
def 归并哈希(归并结果: dict[str, Any]) -> str:
return hashlib.sha256(
hashlib.sha256(
str(sorted((g["canonical"], g["type"]) for g in 归并结果["合并组"])).encode()
+ str(sorted(g["名"] for g in 归并结果["歧义"])).encode()
)
.hexdigest()[:16]
.encode()
).hexdigest()
__all__ = ["收集实体", "归并", "归并哈希", "资料错误"]

View File

@ -0,0 +1,124 @@
"""拆书分析:把参考作品逐窗拆为章节、阶段、实体与写法的版本化成果。
模型调用走 S02(analyst 角色、冻结配置、预算包络);字段结构是固定输出合同,
窗口结果绑定窗哈希,输入漂移时拒绝写入。
"""
from __future__ import annotations
import asyncio
import hashlib
import json
from datetime import UTC, datetime, timedelta
from typing import Any
from uuid import uuid4
from muse.任务运行.接口 import 执行上下文, 模型请求
from muse.任务运行.模型调用 import 请求字节
from muse.任务运行.角色会话 import 组装角色请求
from muse.任务运行.角色策略 import 角色策略目录
from muse.共享.调用身份 import 内容用途
from muse.资料研究.分章切窗 import 漂移检查, 窗口
from muse.资料研究.模型 import 资料错误
# 逐窗分析的输出合同:章节、阶段、实体(带证据)与写法;结构走 S04 固定合同。
窗口输出合同 = {
"type": "object",
"properties": {
"chapters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string", "minLength": 1},
"beat": {"type": "string", "minLength": 1},
},
"required": ["title", "beat"],
"additionalProperties": False,
},
},
"phases": {"type": "array", "items": {"type": "string", "minLength": 1}},
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"type": {"type": "string", "minLength": 1},
"aliases": {"type": "array", "items": {"type": "string"}},
"evidence": {"type": "array", "items": {"type": "string", "minLength": 1}},
},
"required": ["name", "type"],
"additionalProperties": False,
},
},
"techniques": {"type": "array", "items": {"type": "string", "minLength": 1}},
},
"required": ["chapters", "phases", "entities", "techniques"],
"additionalProperties": False,
}
系统提示 = (
"你是参考作品分析员,对给定窗口做拆书分析:"
"列出章节与情节节拍、叙事阶段、出现的人物/地点/事物实体(附原文引文证据)与可借鉴的写法。"
"只依据窗口原文,不补猜窗口外内容;输出一个 JSON 对象。"
)
def 执行窗口提取(装配, 上下文: 执行上下文, 计价, 窗: dict[str, Any], 文本: str) -> dict[str, Any]:
"""对单窗执行模型提取;输入漂移即失败,模型调用按 S02 证据合同授权留痕。"""
窗对象 = 窗口(**窗)
漂移检查(文本, 窗对象)
片段 = 文本[窗对象.起点 : 窗对象.终点]
拆书输入 = (上下文.任务.冻结输入.get("输入") or {}).get("拆书") or {}
from muse.任务运行.配置版本 import 配置版本管理
from muse.编排.章后处理 import _工厂
声明 = 配置版本管理(_工厂(装配)).读取任务绑定(上下文.任务.任务ID).内容.角色配置["extractor"]
请求 = 模型请求(
调用ID=str(uuid4()),
provider=声明["provider"],
model=声明["model"],
thinking=声明.get("thinking"),
系统提示=系统提示,
用户输入=json.dumps(
{
"source_id": 拆书输入.get("source_id"),
"revision": 拆书输入.get("revision"),
"window_id": 窗对象.窗ID,
"起章": 窗对象.起章,
"止章": 窗对象.止章,
"窗口原文": 片段,
},
ensure_ascii=False,
),
输出合同=窗口输出合同,
最大输出token=8_000,
总期限秒=600.0,
)
请求 = 组装角色请求(角色策略目录.从发布包(), "extractor", 请求)
请求哈希 = hashlib.sha256(请求字节(请求)).hexdigest()
用途值 = 上下文.任务.冻结输入.get("内容用途", 内容用途.抽取.value)
授权 = 装配.要求原文().批准保留(
上下文.任务.任务ID,
上下文.任务.作者,
"raw-" + 请求.调用ID,
来源版本=f"{拆书输入.get('source_id')}@r{拆书输入.get('revision')}",
哈希=(请求哈希,),
内容用途=用途值,
方式="persistent",
有效期=datetime.now(UTC) + timedelta(minutes=30),
调用ID=请求.调用ID,
调用请求哈希=请求哈希,
)
执行器 = 装配.要求模型执行器(上下文, 计价)
交付 = asyncio.run(执行器.执行(上下文, 请求, 阶段="拆书分析", 原文授权ID=授权))
内容 = dict(交付.内容)
for 项 in 内容["entities"]:
项.setdefault("aliases", [])
项.setdefault("evidence", [])
return 内容
__all__ = ["窗口输出合同", "系统提示", "执行窗口提取", "资料错误"]

View File

@ -0,0 +1,19 @@
# 分析参考作品 · 任务提示模板
本模板定义「分析参考作品」逐窗提取任务的可版本化表达;角色身份、字段结构、权限与预算从各自权威装配,本模板不重复。
## 任务
对给定窗口(来源版本 + 码点区间 + 内容哈希)输出拆书分析:
1. **章节与节拍**:窗口内每章的标题与情节节拍(beat,一句话)。
2. **叙事阶段**:窗口覆盖的叙事阶段(如 开局/推进/转折/收束)。
3. **实体**:出现的人物、地点、事物;每人附类型与原文引文证据;别名一并列出。
4. **写法**:可借鉴的写作技法(视角、节奏、悬念、对话等),逐条一句话。
## 合同
- 只依据窗口原文;窗口外内容不补猜、不引用。
- 实体证据必须是窗口原文中的字面引文。
- 输出一个 JSON 对象,字段结构以任务输出合同为准(chapters/phases/entities/techniques)。
- 窗口号是切分序号,不是真实章号;章号以窗口内章节标题为准。

View File

@ -0,0 +1,18 @@
# 核对实体歧义 · 任务提示模板
本模板为「核对实体歧义」任务提供可版本化表达;W18 阶段歧义只登记不调用模型,
本模板供后续语义核对阶段装配使用。
## 任务
对拆书归并中登记的歧义条目(同名不同类型的实体候选)逐项核对:
1. 列出各候选的出现窗与证据引文。
2. 依据上下文判断各候选是否同一实体;判断必须给出引文级依据。
3. 结论只能是三态之一:同一实体(合并并说明)、不同实体(分别命名区分)、证据不足(维持歧义)。
## 合同
- 不因为同名就直接合并;类型冲突的同名默认不同实体。
- 只处理参考作品内部身份判定;不跨作品合并,不改写本书事实。
- 证据不足时维持歧义,不得输出倾向性猜测。

View File

@ -0,0 +1,64 @@
"""样本选题:样本比较、选题依据与选书交接;观察陈述可回查,不把相关性当因果。"""
from __future__ import annotations
import hashlib
from typing import Any
from muse.资料研究.模型 import 资料错误
def 样本比较(A: dict[str, Any], B: dict[str, Any]) -> dict[str, Any]:
"""两份样本的结构观察差异:只陈述可复核指标,不作效果归因。"""
指标 = ("章数", "字数", "实体数", "窗口数")
差异 = {
名: {"A": A.get(名), "B": B.get(名)}
for 名 in 指标
if A.get(名) is not None or B.get(名) is not None
}
return {
"样本A": _身份(A),
"样本B": _身份(B),
"差异": 差异,
"说明": "以上为时点观察记录;不构成优劣结论或效果因果。",
}
def 选书交接单(样本: dict[str, Any], 理由: str) -> dict[str, Any]:
"""把选定样本交接给拆书:原文身份(来源、版本、哈希)必须完整可回查。"""
身份 = _身份(样本)
if not 理由.strip():
raise 资料错误("选书依据必须写明理由")
return {
"source_id": 身份["source_id"],
"revision": int(身份["revision"]),
"content_hash": 身份["content_hash"],
"理由": 理由.strip(),
"观察": {
名: 样本[名] for 名 in ("章数", "字数", "实体数", "窗口数") if 样本.get(名) is not None
},
}
def 选题依据哈希(交接单: dict[str, Any]) -> str:
return hashlib.sha256(
str(
[交接单["source_id"], 交接单["revision"], 交接单["content_hash"], 交接单["理由"]]
).encode()
).hexdigest()
def _身份(样本: dict[str, Any]) -> dict[str, Any]:
for 名 in ("source_id", "revision", "content_hash"):
if 样本.get(名) is None:
raise 资料错误(f"样本缺少 {名},原文身份不完整")
return {
"source_id": str(样本["source_id"]),
"revision": int(样本["revision"]),
"content_hash": str(样本["content_hash"]),
}
__all__ = ["样本比较", "选书交接单", "选题依据哈希", "资料错误"]

View File

@ -4,10 +4,8 @@ from __future__ import annotations
import pathlib import pathlib
import sys import sys
import tempfile
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -7,7 +7,6 @@ import pathlib
import re import re
import unittest import unittest
ROOT = pathlib.Path(__file__).resolve().parents[2] ROOT = pathlib.Path(__file__).resolve().parents[2]
AUTHOR_ROOT = ROOT / ".agent" / "作者" AUTHOR_ROOT = ROOT / ".agent" / "作者"
SCENE_ROOT = AUTHOR_ROOT / "场景" SCENE_ROOT = AUTHOR_ROOT / "场景"

View File

@ -32,7 +32,6 @@ from framework.adapters.dsh.runner import ( # noqa: E402
from framework.primitives.artifacts import read_jsonl # noqa: E402 from framework.primitives.artifacts import read_jsonl # noqa: E402
from framework.primitives.execution import FrameworkExecutionRequest # noqa: E402 from framework.primitives.execution import FrameworkExecutionRequest # noqa: E402
SESSION_LINES = [ SESSION_LINES = [
{ {
"type": "session", "type": "session",

View File

@ -7,7 +7,6 @@ import pathlib
import tempfile import tempfile
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -6,7 +6,6 @@ import pathlib
import re import re
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -4,8 +4,8 @@ from __future__ import annotations
import json import json
import pathlib import pathlib
import tempfile
import sys import sys
import tempfile
import unittest import unittest
from jsonschema import Draft202012Validator from jsonschema import Draft202012Validator
@ -31,7 +31,6 @@ from framework.primitives.execution import (
FrameworkExecutionResult, FrameworkExecutionResult,
) )
SCHEMA_DIR = ROOT / "framework" / "primitives" / "schemas" SCHEMA_DIR = ROOT / "framework" / "primitives" / "schemas"

View File

@ -13,6 +13,7 @@ import pathlib
import re import re
import unittest import unittest
def _find_project_root(start: pathlib.Path) -> pathlib.Path: def _find_project_root(start: pathlib.Path) -> pathlib.Path:
resolved = start.resolve() resolved = start.resolve()
for directory in (resolved, *resolved.parents): for directory in (resolved, *resolved.parents):

View File

@ -7,7 +7,6 @@ import re
import unittest import unittest
import urllib.parse import urllib.parse
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -6,7 +6,6 @@ import pathlib
import re import re
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -6,7 +6,6 @@ import pathlib
import unittest import unittest
from unittest import mock from unittest import mock
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -496,6 +496,7 @@ def 章后环境(应用测试库, tmp_path):
"剧本": 剧本, "剧本": 剧本,
"收到": 收到, "收到": 收到,
"新作品": 新作品, "新作品": 新作品,
"提供方端口": server.server_port,
"处理器能力": sorted(set(生成处理器集) | set(章后处理器集)), "处理器能力": sorted(set(生成处理器集) | set(章后处理器集)),
} }
yield 环境 yield 环境
@ -549,3 +550,129 @@ def 章后工具(章后环境):
return {"类型": "文本", "文本": {"summary": 摘要, "facts": 事实}} return {"类型": "文本", "文本": {"summary": 摘要, "facts": 事实}}
return SimpleNamespace(保存一章=保存一章, 跑任务=跑任务, 抽取输出=抽取输出, 环境=环境) return SimpleNamespace(保存一章=保存一章, 跑任务=跑任务, 抽取输出=抽取输出, 环境=环境)
@pytest.fixture
def 研究环境(章后环境, tmp_path):
"""研究拆书环境:analyst 配置、研究流程装配与带授权的参考来源。"""
from decimal import Decimal
from types import SimpleNamespace
from uuid import uuid4
from muse.任务运行.配置版本 import 凭据引用, 提供方配置, 运行配置内容, 配置版本管理
from muse.任务运行.预算管理 import 预算管理, 额度策略
from muse.启动 import 装配研究拆书
from muse.编排.研究拆书 import 处理器版本集 as 研究处理器集
from muse.资料研究.接口 import 资料服务
from muse.资料研究.模型 import 导入请求
环境 = 章后环境
环境["处理器能力"] = sorted(set(环境["处理器能力"]) | set(研究处理器集))
库, 装配 = 环境["库"], 环境["装配"]
策略 = __import__("muse.任务运行.角色策略", fromlist=["角色策略目录"]).角色策略目录.从发布包()
装配研究拆书(装配, 合成派生计价(), 分析配置ID="research-config")
凭据 = tmp_path / "research-key"
凭据.write_text("synthetic-research-credential")
凭据.chmod(0o600)
研究配置 = 运行配置内容(
"direct",
"1",
策略.定义["version"],
策略.资源发布身份,
"synthetic",
{
"extractor": {
"provider": "synthetic",
"model": "claude-opus-4-8[1M]",
"thinking": "high",
"stage_tools": {"抽取": [], "拆书分析": []},
},
},
(凭据引用("provider-key", "受控存储", str(凭据)),),
(
提供方配置(
"synthetic",
"responses",
f"http://127.0.0.1:{环境['提供方端口']}/v1/responses",
"provider-key",
),
),
合成派生计价().版本,
)
管理 = 配置版本管理(库, _离线派生配置检查())
管理.保存草案("research-config", "1", 研究配置)
启用回执 = 管理.验证版本("research-config", "1")
管理.启用("research-config", "1", 验证回执=启用回执, 批准引用="synthetic-approval", 预期代次=0)
预算 = 预算管理(库, "synthetic-research")
预算.登记策略(额度策略("synthetic-research", "1", Decimal("40"), 80))
服务 = 资料服务()
def 导入来源(标题: str, 正文: str, 用途集: tuple[str, ...] = ("analysis",)) -> dict:
with 库.连接() as 连, 连.transaction():
回执 = 服务.导入(
连,
环境["作者"].作者,
导入请求("reference", 标题, f"file://资料/{标题}.txt", 正文, 用途集),
)
return {
"source_id": 回执.source_id,
"revision": 回执.revision,
"content_hash": 回执.content_hash,
}
def 发起(
source_id: str, *, 配置ID: str = "research-config", 重跑窗=None, 理由=None, 窗口章数=None
):
from muse.编排.研究拆书 import 发起研究拆书
样本 = {"理由": 理由} if 理由 else None
return 发起研究拆书(
装配,
环境["作者"],
"research-" + uuid4().hex[:8],
source_id=source_id,
配置ID=配置ID,
重跑窗=重跑窗,
样本依据=样本,
窗口章数=窗口章数,
)
def 跑任务(任务ID: str):
from muse.任务运行.接口 import 任务状态
运行 = 装配.任务运行
能力 = sorted(set(环境["处理器能力"]) | set(研究处理器集))
while True:
快照 = 运行.读取任务(任务ID)
if 快照.状态 in {任务状态.已完成, 任务状态.已失败, 任务状态.已取消}:
return 快照
领取 = 运行.领取步骤("worker", 能力)
assert 领取 is not None, "任务未完成却没有可领取步骤"
运行.执行一步(领取)
def 分析输出(实体: list[dict], 写法=("三段式推进",)) -> dict:
return {
"类型": "文本",
"文本": {
"chapters": [{"title": "章", "beat": "节拍"}],
"phases": ["推进"],
"entities": 实体,
"techniques": list(写法),
},
}
return SimpleNamespace(
环境=环境,
库=库,
装配=装配,
剧本=环境["剧本"],
服务=服务,
导入来源=导入来源,
发起=发起,
跑任务=跑任务,
分析输出=分析输出,
)

View File

@ -11,7 +11,6 @@ import sys
import tempfile import tempfile
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
@ -22,7 +21,6 @@ if str(ROOT) not in sys.path:
from unittest import mock from unittest import mock
from muse import replay as replay_mod # noqa: E402
from muse.flow import dispatch as dispatch_mod # noqa: E402 from muse.flow import dispatch as dispatch_mod # noqa: E402
from muse.flow.adopt import adopt_candidate # noqa: E402 from muse.flow.adopt import adopt_candidate # noqa: E402
from muse.store import ( # noqa: E402 from muse.store import ( # noqa: E402
@ -33,6 +31,8 @@ from muse.store import ( # noqa: E402
) )
from web import app as webapp # noqa: E402 from web import app as webapp # noqa: E402
from muse import replay as replay_mod # noqa: E402
class CompoundingTest(unittest.TestCase): class CompoundingTest(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:

View File

@ -7,7 +7,6 @@ import sys
import tempfile import tempfile
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -9,7 +9,6 @@ import sys
import tempfile import tempfile
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -8,7 +8,6 @@ import sys
import tempfile import tempfile
import unittest import unittest
ROOT = next( ROOT = next(
parent parent
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents) for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)

View File

@ -13,8 +13,8 @@ DIAGNOSE_SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humaniz
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
sys.path.insert(0, str(DIAGNOSE_SCRIPT_DIR)) sys.path.insert(0, str(DIAGNOSE_SCRIPT_DIR))
import revise_ai_flavor as rev # noqa: E402
import diagnose_ai_flavor as diag # noqa: E402 import diagnose_ai_flavor as diag # noqa: E402
import revise_ai_flavor as rev # noqa: E402
from deai.pipeline import DowngradedToAudit # noqa: E402 from deai.pipeline import DowngradedToAudit # noqa: E402
CLEAN_TEXT = "值得注意的是,门外已经下起了雨。" CLEAN_TEXT = "值得注意的是,门外已经下起了雨。"

View File

@ -30,9 +30,9 @@ for path in (READ_CONTEXT_DIR, DETECT_DIR, SCRIPT_DIR, TEST_DIR, CHECK_TEST_DIR)
from candidate_cas import PostgresCasStateStore # noqa: E402 from candidate_cas import PostgresCasStateStore # noqa: E402
from run_writer_pipeline import CasToken, PipelineError, run_writer_pipeline # noqa: E402 from run_writer_pipeline import CasToken, PipelineError, run_writer_pipeline # noqa: E402
from run_writer_semantic_detector import canonical_sha256 # noqa: E402 from run_writer_semantic_detector import canonical_sha256 # noqa: E402
from test_check_writer_candidate import _candidate_body, _requirements, _valid_pair # noqa: E402 from test_check_writer_candidate import _requirements, _valid_pair # noqa: E402
from test_run_writer_pipeline import _semantic_pass, _writer_output # noqa: E402 from test_run_writer_pipeline import _semantic_pass, _writer_output # noqa: E402
from writer_contract import build_candidate_envelope, retrieval_identity # noqa: E402 from writer_contract import retrieval_identity # noqa: E402
class FakeCursor: class FakeCursor:

View File

@ -21,7 +21,6 @@ for path in (SCRIPT_DIR, DISPATCH_TEST_DIR, CHECK_TEST_DIR, WNC_TEST_DIR):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
import dispatch_writer_bridge as bridge # noqa: E402
from dispatch_agent_task import DEFAULT_RUN_DIR_ROOT # noqa: E402 from dispatch_agent_task import DEFAULT_RUN_DIR_ROOT # noqa: E402
from dispatch_writer_bridge import ( # noqa: E402 from dispatch_writer_bridge import ( # noqa: E402
DispatchWriterError, DispatchWriterError,
@ -31,7 +30,12 @@ from dispatch_writer_bridge import ( # noqa: E402
) )
from read_tools import TOOL_REGISTRY # noqa: E402 from read_tools import TOOL_REGISTRY # noqa: E402
from test_check_writer_candidate import _valid_pair # noqa: E402 from test_check_writer_candidate import _valid_pair # noqa: E402
from test_dispatch_agent_task import FIXED_MODEL, RecordingConnect, fake_launcher, pi_stream_lines # noqa: E402 from test_dispatch_agent_task import ( # noqa: E402
FIXED_MODEL,
RecordingConnect,
fake_launcher,
pi_stream_lines,
)
class BridgeConnect(RecordingConnect): class BridgeConnect(RecordingConnect):

View File

@ -4,7 +4,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "work" / "skills" / "generate" / "写下一章" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "work" / "skills" / "generate" / "写下一章" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -21,11 +21,14 @@ from production_evidence_reassemble import ( # noqa: E402
EvidenceReassembleError, EvidenceReassembleError,
reassemble_writer_context_for_gaps, reassemble_writer_context_for_gaps,
) )
from run_writer_pipeline import InMemoryCasStateStore, PipelineError, run_writer_pipeline # noqa: E402 from run_writer_pipeline import ( # noqa: E402
from writer_contract import build_writer_creative_input, retrieval_identity # noqa: E402 InMemoryCasStateStore,
PipelineError,
run_writer_pipeline,
)
from test_check_writer_candidate import _requirements, _valid_pair # noqa: E402 from test_check_writer_candidate import _requirements, _valid_pair # noqa: E402
from test_run_writer_pipeline import _semantic_report, _writer_output # noqa: E402 from test_run_writer_pipeline import _semantic_report, _writer_output # noqa: E402
from writer_contract import build_writer_creative_input, retrieval_identity # noqa: E402
class ProductionEvidenceReassembleTests(unittest.TestCase): class ProductionEvidenceReassembleTests(unittest.TestCase):

View File

@ -24,8 +24,6 @@ sys.path.insert(0, str(SCRIPT_DIR))
sys.path.insert(0, str(READ_CONTEXT_DIR)) sys.path.insert(0, str(READ_CONTEXT_DIR))
sys.path.insert(0, str(TEST_CONTEXT_DIR)) sys.path.insert(0, str(TEST_CONTEXT_DIR))
from test_writer_contract import valid_context, valid_draft # noqa: E402
from writer_contract import build_writer_creative_input, canonical_json, retrieval_identity # noqa: E402
from muse_role import ( # noqa: E402 from muse_role import ( # noqa: E402
FIXED_OPUS_MODEL_ID, FIXED_OPUS_MODEL_ID,
FIXED_OPUS_POLICY_ALIAS, FIXED_OPUS_POLICY_ALIAS,
@ -40,6 +38,12 @@ from run_writer import ( # noqa: E402
calculate_dynamic_output_contract, calculate_dynamic_output_contract,
run_writer, run_writer,
) )
from test_writer_contract import valid_context, valid_draft # noqa: E402
from writer_contract import ( # noqa: E402
build_writer_creative_input,
canonical_json,
retrieval_identity,
)
USAGE = {"input_tokens": 100, "output_tokens": 20} USAGE = {"input_tokens": 100, "output_tokens": 20}
ACTUAL_MODEL = FIXED_OPUS_MODEL_ID ACTUAL_MODEL = FIXED_OPUS_MODEL_ID

View File

@ -22,9 +22,14 @@ READ_CONTEXT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "context" / "skills" /
for path in (READ_CONTEXT_DIR, DETECT_DIR, SCRIPT_DIR, TEST_DIR, CHECK_TEST_DIR): for path in (READ_CONTEXT_DIR, DETECT_DIR, SCRIPT_DIR, TEST_DIR, CHECK_TEST_DIR):
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from test_check_writer_candidate import _candidate_body, _requirements, _valid_pair # noqa: E402 from run_writer_pipeline import ( # noqa: E402
from run_writer_pipeline import CasToken, InMemoryCasStateStore, PipelineError, atomic_write_json, run_writer_pipeline # noqa: E402 InMemoryCasStateStore,
PipelineError,
atomic_write_json,
run_writer_pipeline,
)
from run_writer_semantic_detector import canonical_sha256 # noqa: E402 from run_writer_semantic_detector import canonical_sha256 # noqa: E402
from test_check_writer_candidate import _candidate_body, _requirements, _valid_pair # noqa: E402
from writer_contract import build_candidate_envelope, retrieval_identity # noqa: E402 from writer_contract import build_candidate_envelope, retrieval_identity # noqa: E402

View File

@ -21,7 +21,6 @@ if str(SCRIPT_DIR) not in sys.path:
import persist_writer_run as writer_persist # noqa: E402 import persist_writer_run as writer_persist # noqa: E402
CANDIDATE_SHA = "a" * 64 CANDIDATE_SHA = "a" * 64
CONTEXT_SHA = "sha256:" + "b" * 64 CONTEXT_SHA = "sha256:" + "b" * 64

View File

@ -20,8 +20,8 @@ for path in (SCRIPT_DIR,):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
import two_phase_writer # noqa: E402
import dispatch_writer_bridge as bridge # noqa: E402 import dispatch_writer_bridge as bridge # noqa: E402
import two_phase_writer # noqa: E402
from two_phase_writer import ( # noqa: E402 from two_phase_writer import ( # noqa: E402
EXPLORATION_MAX_MATERIALS, EXPLORATION_MAX_MATERIALS,
EXPLORATION_SCHEMA_VERSION, EXPLORATION_SCHEMA_VERSION,

View File

@ -9,7 +9,6 @@
from __future__ import annotations from __future__ import annotations
import copy
import pathlib import pathlib
import sys import sys
import unittest import unittest
@ -22,7 +21,10 @@ for path in (SCRIPT_DIR, PROJECT_ROOT / "muse" / "lifecycle" / "context" / "skil
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from fact_delta import ( # noqa: E402 from fact_delta import ( # noqa: E402
DELTA_TYPES, FactDeltaError, validate_delta_batch, validate_delta_proposal, DELTA_TYPES,
FactDeltaError,
validate_delta_batch,
validate_delta_proposal,
) )
BODY = "林深把黑纹缠上手臂,异种核心在胸腔里低鸣。何岚站在舱门口没有说话。" BODY = "林深把黑纹缠上手臂,异种核心在胸腔里低鸣。何岚站在舱门口没有说话。"

View File

@ -30,9 +30,9 @@ for path in (SCRIPT_DIR,):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from muse_db import connect # noqa: E402
from fact_delta import FactDeltaError, propose_fact_deltas # noqa: E402 from fact_delta import FactDeltaError, propose_fact_deltas # noqa: E402
from write_canonical import ConflictError, accept # noqa: E402 from muse_db import connect # noqa: E402
from write_canonical import accept # noqa: E402
SUFFIX = uuid.uuid4().hex[:10] SUFFIX = uuid.uuid4().hex[:10]
WORK_TITLE = f"unittest-delta-{SUFFIX}" WORK_TITLE = f"unittest-delta-{SUFFIX}"

View File

@ -31,7 +31,10 @@ for path in (SCRIPT_DIR,):
from muse_db import connect # noqa: E402 from muse_db import connect # noqa: E402
from projection_registry import ( # noqa: E402 from projection_registry import ( # noqa: E402
ProjectionError, finish_projection, refresh_staleness, retry_projection, ProjectionError,
finish_projection,
refresh_staleness,
retry_projection,
) )
from write_canonical import accept # noqa: E402 from write_canonical import accept # noqa: E402

View File

@ -21,7 +21,6 @@ import hashlib
import pathlib import pathlib
import sys import sys
import uuid import uuid
from typing import Any
from unittest import mock from unittest import mock
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
@ -31,8 +30,8 @@ for path in (SCRIPT_DIR,):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from muse_db import connect # noqa: E402
import write_canonical # noqa: E402 import write_canonical # noqa: E402
from muse_db import connect # noqa: E402
from write_canonical import ConflictError, accept, discard # noqa: E402 from write_canonical import ConflictError, accept, discard # noqa: E402
SUFFIX = uuid.uuid4().hex[:10] SUFFIX = uuid.uuid4().hex[:10]

View File

@ -25,8 +25,8 @@ for path in (SCRIPT_DIR,):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from persist_context_freeze import persist_freeze # noqa: E402
from muse_db import connect # noqa: E402 from muse_db import connect # noqa: E402
from persist_context_freeze import persist_freeze # noqa: E402
class FreezeReplayIdempotencyDbTest(unittest.TestCase): class FreezeReplayIdempotencyDbTest(unittest.TestCase):

View File

@ -20,7 +20,6 @@ for script_dir in (
sys.path.insert(0, str(script_dir)) sys.path.insert(0, str(script_dir))
from load_reference_work import begin_read_snapshot # noqa: E402 from load_reference_work import begin_read_snapshot # noqa: E402
from search import search_cards # noqa: E402
from retrieve_writer_sources import ( # noqa: E402 from retrieve_writer_sources import ( # noqa: E402
ProductionCardIndexRepository, ProductionCardIndexRepository,
ReplayCardIndexRepository, ReplayCardIndexRepository,
@ -30,7 +29,7 @@ from retrieve_writer_sources import ( # noqa: E402
retrieve_writer_sources, retrieve_writer_sources,
stable_sort_cards, stable_sort_cards,
) )
from search import search_cards # noqa: E402
SOURCE_VERSION = "raw-file-v1:sha256:" + "a" * 64 SOURCE_VERSION = "raw-file-v1:sha256:" + "a" * 64

View File

@ -465,7 +465,7 @@ class WriterContractTest(unittest.TestCase):
self.assertEqual(output["candidateVersion"], 3) self.assertEqual(output["candidateVersion"], 3)
self.assertEqual( self.assertEqual(
output["candidateSha256"], output["candidateSha256"],
"sha256:" + hashlib.sha256("Café\n正文".encode("utf-8")).hexdigest(), "sha256:" + hashlib.sha256("Café\n正文".encode()).hexdigest(),
) )
validate_writer_output(output) validate_writer_output(output)

View File

@ -11,7 +11,7 @@ import sys
import tempfile import tempfile
import types import types
import unittest import unittest
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
@ -41,6 +41,7 @@ for _path in (
import load_writer_reference_work as loader # noqa: E402 import load_writer_reference_work as loader # noqa: E402
import run_writer_replay as replay_module # noqa: E402 import run_writer_replay as replay_module # noqa: E402
from check_writer_candidate import check_writer_candidate # noqa: E402
from load_writer_reference_work import ( # noqa: E402 from load_writer_reference_work import ( # noqa: E402
WriterReferenceWorkError, WriterReferenceWorkError,
_requirement_hard_constraints, _requirement_hard_constraints,
@ -51,20 +52,18 @@ from load_writer_reference_work import ( # noqa: E402
validate_oracle_truth_pack, validate_oracle_truth_pack,
write_temporary_config, write_temporary_config,
) )
from run_writer_replay import run_writer_replay # noqa: E402
from check_writer_candidate import check_writer_candidate # noqa: E402
from writer_contract import build_candidate_envelope, han_count # noqa: E402
from test_run_writer import _bound_context # noqa: E402
from refresh_runtime_probe import ( # noqa: E402
make_dry_run_invoker,
refresh_runtime_probe,
)
from muse_role import ( # noqa: E402 from muse_role import ( # noqa: E402
MODEL_POLICY_VERSION, MODEL_POLICY_VERSION,
RUNTIME_ADAPTER, RUNTIME_ADAPTER,
RUNTIME_ADAPTER_VERSION, RUNTIME_ADAPTER_VERSION,
) )
from refresh_runtime_probe import ( # noqa: E402
make_dry_run_invoker,
refresh_runtime_probe,
)
from run_writer_replay import run_writer_replay # noqa: E402
from test_run_writer import _bound_context # noqa: E402
from writer_contract import build_candidate_envelope, han_count # noqa: E402
CONFIGS_DIR = WRITER_REPLAY_SCRIPTS.parent / "configs" CONFIGS_DIR = WRITER_REPLAY_SCRIPTS.parent / "configs"
BASE_CONFIG_PATH = CONFIGS_DIR / "writer-gate-a-deep-space-v1-cn-skills.json" BASE_CONFIG_PATH = CONFIGS_DIR / "writer-gate-a-deep-space-v1-cn-skills.json"
@ -724,7 +723,7 @@ class LoadWriterReferenceWorkTest(unittest.TestCase):
parsed = datetime.fromisoformat(stamped["retainUntil"]) parsed = datetime.fromisoformat(stamped["retainUntil"])
self.assertIsNotNone(parsed.tzinfo) self.assertIsNotNone(parsed.tzinfo)
# (c) 解析后在未来、且距 now 不超过 24 小时(实际约等于 23 小时) # (c) 解析后在未来、且距 now 不超过 24 小时(实际约等于 23 小时)
now = datetime.now(timezone.utc) now = datetime.now(UTC)
self.assertGreater(parsed, now) self.assertGreater(parsed, now)
self.assertLessEqual(parsed - now, timedelta(hours=24)) self.assertLessEqual(parsed - now, timedelta(hours=24))
# (d) receiptSha256 == 去掉 receiptSha256 的整段规范自哈希(重签正确) # (d) receiptSha256 == 去掉 receiptSha256 的整段规范自哈希(重签正确)

View File

@ -39,7 +39,6 @@ for _path in (
if str(_path) not in sys.path: if str(_path) not in sys.path:
sys.path.insert(0, str(_path)) sys.path.insert(0, str(_path))
import load_writer_reference_work as loader # noqa: E402
from load_writer_reference_work import ( # noqa: E402 from load_writer_reference_work import ( # noqa: E402
PATTERN_CARD_TYPES, PATTERN_CARD_TYPES,
PATTERN_TOTAL_CAP, PATTERN_TOTAL_CAP,
@ -52,19 +51,6 @@ from run_writer_replay.sample import ( # noqa: E402
_build_arm_contexts, _build_arm_contexts,
_common_controls, _common_controls,
) )
from writer_contract import ( # noqa: E402
PATTERN_NAME_MAX_CHARS,
PATTERN_POINTS_MAX_FIELDS,
PATTERN_POINT_MAX_CHARS,
PATTERN_SUMMARY_MAX_CHARS,
ContractError,
_pattern_source_ref,
_source_ref,
build_writer_creative_input,
normalize_text,
pattern_references_for_arm,
retrieval_identity,
)
# 复用既有测试夹具:真实五章 base 配置 + 纯数据快照,能让装配器跑通真实 dry-run。 # 复用既有测试夹具:真实五章 base 配置 + 纯数据快照,能让装配器跑通真实 dry-run。
from test_load_writer_reference_work import ( # noqa: E402 from test_load_writer_reference_work import ( # noqa: E402
@ -74,6 +60,19 @@ from test_load_writer_reference_work import ( # noqa: E402
_assembly_rows, _assembly_rows,
_refresh_self_hash, _refresh_self_hash,
) )
from writer_contract import ( # noqa: E402
PATTERN_NAME_MAX_CHARS,
PATTERN_POINT_MAX_CHARS,
PATTERN_POINTS_MAX_FIELDS,
PATTERN_SUMMARY_MAX_CHARS,
ContractError,
_pattern_source_ref,
_source_ref,
build_writer_creative_input,
normalize_text,
pattern_references_for_arm,
retrieval_identity,
)
def _prepared_base_config() -> dict[str, object]: def _prepared_base_config() -> dict[str, object]:

View File

@ -17,7 +17,7 @@ for _import_dir in (SCRIPT_DIR, TEST_DIR, QUALITY_GATE_DIR):
if str(_import_dir) not in sys.path: if str(_import_dir) not in sys.path:
sys.path.insert(0, str(_import_dir)) sys.path.insert(0, str(_import_dir))
from gate_input_builder import GateInputBuildError, GateInputBuilder, canonical_sha256 from gate_input_builder import GateInputBuilder, GateInputBuildError, canonical_sha256
from test_writer_gate import build_input, source_bundle from test_writer_gate import build_input, source_bundle
from writer_rubric import RUBRIC_POLICY_VERSION from writer_rubric import RUBRIC_POLICY_VERSION

View File

@ -20,8 +20,8 @@ for _import_dir in (SCRIPT_DIR, WRITER_REPLAY_DIR, QUALITY_GATE_DIR, EVIDENCE_DI
if str(_import_dir) not in sys.path: if str(_import_dir) not in sys.path:
sys.path.insert(0, str(_import_dir)) sys.path.insert(0, str(_import_dir))
from muse_role import FIXED_OPUS_MODEL_ID, FIXED_OPUS_POLICY_ALIAS
from gate_input_builder import GateInputBuilder, canonical_sha256 from gate_input_builder import GateInputBuilder, canonical_sha256
from muse_role import FIXED_OPUS_MODEL_ID, FIXED_OPUS_POLICY_ALIAS
from writer_eval_preregister import build_balanced_preregistration from writer_eval_preregister import build_balanced_preregistration
from writer_gate import ( from writer_gate import (
decide_gate, decide_gate,
@ -35,7 +35,6 @@ from writer_rubric import (
adjudicate_structured_reviews, adjudicate_structured_reviews,
) )
SCENARIOS = ( SCENARIOS = (
"battle", "battle",
"character_dialogue", "character_dialogue",

View File

@ -10,17 +10,16 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.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)) sys.path.insert(0, str(SCRIPT_DIR))
from persist_planning import ( # noqa: E402 from persist_planning import ( # noqa: E402
check_field_coverage, FINE_OUTLINE_OWNER,
write_section,
_load_schema, _load_schema,
assert_section_owner, assert_section_owner,
FINE_OUTLINE_OWNER, check_field_coverage,
write_section,
) )
REQUIRED = ["targetChapter", "sourceRef", "chapterGoal", "hardConstraints", "keyEvents", REQUIRED = ["targetChapter", "sourceRef", "chapterGoal", "hardConstraints", "keyEvents",

View File

@ -5,7 +5,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.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)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -5,7 +5,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.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)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -6,7 +6,6 @@ import sys
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.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)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -13,10 +13,10 @@ import signal
import sys import sys
import tempfile import tempfile
import unittest import unittest
from unittest import mock
from dataclasses import replace from dataclasses import replace
from datetime import UTC, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from datetime import datetime, timedelta, timezone from unittest import mock
TEST_DIR = pathlib.Path(__file__).resolve().parent TEST_DIR = pathlib.Path(__file__).resolve().parent
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
@ -38,19 +38,8 @@ for import_path in (
import run_writer_replay as replay_module # noqa: E402 import run_writer_replay as replay_module # noqa: E402
import run_writer_replay.execute as execute_module # noqa: E402 import run_writer_replay.execute as execute_module # noqa: E402
from run_writer_replay import ( # noqa: E402 from file_cas import CasConflictError, FileCasStore # noqa: E402
BudgetLedgerError, from gate_input_builder import GateInputBuilder, GateInputBuildError, canonical_sha256 # noqa: E402
WriterReplayError,
WriterReplayProductionAdapters,
WriterReplayTestAdapters,
run_writer_replay,
)
from run_writer_replay.blind import ( # noqa: E402
_SOURCE_REF_ALLOWED,
_clean_source_ref,
_project_oracle_pack,
_semantic_input_v3,
)
from muse_role import ( # noqa: E402 from muse_role import ( # noqa: E402
FIXED_OPUS_MODEL_ID, FIXED_OPUS_MODEL_ID,
FIXED_OPUS_POLICY_ALIAS, FIXED_OPUS_POLICY_ALIAS,
@ -64,18 +53,29 @@ from muse_role import ( # noqa: E402
sha256_json, sha256_json,
sha256_text, sha256_text,
) )
from file_cas import CasConflictError, FileCasStore # noqa: E402
from gate_input_builder import GateInputBuildError, GateInputBuilder, canonical_sha256 # noqa: E402
from raw_vault import RawVaultError, RawVaultManager # noqa: E402 from raw_vault import RawVaultError, RawVaultManager # noqa: E402
from run_writer import build_writer_execution_profile # noqa: E402
from run_writer_blind_judge import BLIND_JUDGE_REPORT_JSON_SCHEMA # noqa: E402 from run_writer_blind_judge import BLIND_JUDGE_REPORT_JSON_SCHEMA # noqa: E402
from run_writer_replay import ( # noqa: E402
BudgetLedgerError,
WriterReplayError,
WriterReplayProductionAdapters,
WriterReplayTestAdapters,
run_writer_replay,
)
from run_writer_replay.blind import ( # noqa: E402
_SOURCE_REF_ALLOWED,
_clean_source_ref,
_project_oracle_pack,
_semantic_input_v3,
)
from run_writer_semantic_detector import ( # noqa: E402 from run_writer_semantic_detector import ( # noqa: E402
SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA, SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA,
is_semantic_schema_specialization, is_semantic_schema_specialization,
) )
from run_writer import build_writer_execution_profile # noqa: E402 from writer_contract import calculate_target_chars, han_count # noqa: E402
from writer_eval_preregister import build_balanced_preregistration # noqa: E402 from writer_eval_preregister import build_balanced_preregistration # noqa: E402
from writer_gate import decide_gate # noqa: E402 from writer_gate import decide_gate # noqa: E402
from writer_contract import calculate_target_chars, han_count, validate_writer_context # noqa: E402
from writer_rubric import ( # noqa: E402 from writer_rubric import ( # noqa: E402
COMMON_DIMENSIONS, COMMON_DIMENSIONS,
DIMENSIONS, DIMENSIONS,
@ -90,7 +90,7 @@ def _utc_stamp(delta: timedelta) -> str:
"""相对当前时刻生成 UTC 时间戳。""" """相对当前时刻生成 UTC 时间戳。"""
return ( return (
(datetime.now(timezone.utc) + delta) (datetime.now(UTC) + delta)
.replace(microsecond=0) .replace(microsecond=0)
.isoformat() .isoformat()
.replace("+00:00", "Z") .replace("+00:00", "Z")
@ -702,7 +702,7 @@ def _test_execution_authorization() -> dict[str, object]:
"status": "approved", "status": "approved",
"authorizationId": "raw-test-adapter-1", "authorizationId": "raw-test-adapter-1",
"approvedBy": "user", "approvedBy": "user",
"retainUntil": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(), "retainUntil": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
} }
for item in (probe, budget, raw): for item in (probe, budget, raw):
item["receiptSha256"] = canonical_sha256(item) item["receiptSha256"] = canonical_sha256(item)
@ -1090,7 +1090,7 @@ def _production_config(*, budget_approved: bool = True) -> dict[str, object]:
"status": "approved", "status": "approved",
"authorizationId": "raw-test-1", "authorizationId": "raw-test-1",
"approvedBy": "user", "approvedBy": "user",
"retainUntil": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(), "retainUntil": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
} }
for item in (probe, budget, raw): for item in (probe, budget, raw):
item["receiptSha256"] = canonical_sha256(item) item["receiptSha256"] = canonical_sha256(item)
@ -1330,7 +1330,7 @@ class WriterReplayDryRunTest(unittest.TestCase):
def test_raw_call_window_uses_one_role_timeout_plus_cleanup_margin(self): def test_raw_call_window_uses_one_role_timeout_plus_cleanup_margin(self):
"""固定时钟证明单次 30 秒 timeout 加 60 秒清理余量的精确边界。""" """固定时钟证明单次 30 秒 timeout 加 60 秒清理余量的精确边界。"""
now = datetime(2026, 7, 27, 0, 0, tzinfo=timezone.utc) now = datetime(2026, 7, 27, 0, 0, tzinfo=UTC)
profile = _role_profile("semantic_detector", SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA) profile = _role_profile("semantic_detector", SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA)
replay_module._assert_raw_call_window( replay_module._assert_raw_call_window(
@ -2459,7 +2459,7 @@ class WriterReplayProductionIntegrationTest(unittest.TestCase):
adapters, writer, semantic, judge = _production_adapters(vault_factory=vault_factory) adapters, writer, semantic, judge = _production_adapters(vault_factory=vault_factory)
config = _production_config() config = _production_config()
raw = config["executionAuthorization"]["rawRetention"] raw = config["executionAuthorization"]["rawRetention"]
raw["retainUntil"] = (datetime.now(timezone.utc) + timedelta(minutes=4)).isoformat() raw["retainUntil"] = (datetime.now(UTC) + timedelta(minutes=4)).isoformat()
raw["receiptSha256"] = canonical_sha256( raw["receiptSha256"] = canonical_sha256(
{key: value for key, value in raw.items() if key != "receiptSha256"} {key: value for key, value in raw.items() if key != "receiptSha256"}
) )
@ -3710,7 +3710,7 @@ class WriterReplayProductionIntegrationTest(unittest.TestCase):
adapters, writer, semantic, judge = _production_adapters(vault_factory=vault_factory) adapters, writer, semantic, judge = _production_adapters(vault_factory=vault_factory)
short = _production_config() short = _production_config()
raw = short["executionAuthorization"]["rawRetention"] raw = short["executionAuthorization"]["rawRetention"]
raw["retainUntil"] = (datetime.now(timezone.utc) + timedelta(seconds=27)).isoformat() raw["retainUntil"] = (datetime.now(UTC) + timedelta(seconds=27)).isoformat()
raw["receiptSha256"] = canonical_sha256( raw["receiptSha256"] = canonical_sha256(
{key: value for key, value in raw.items() if key != "receiptSha256"} {key: value for key, value in raw.items() if key != "receiptSha256"}
) )

View File

@ -45,7 +45,7 @@ class WriterEvalPreregisterTest(unittest.TestCase):
expected_order = sorted( expected_order = sorted(
self.sample_ids, self.sample_ids,
key=lambda sample_id: hashlib.sha256( key=lambda sample_id: hashlib.sha256(
f"{self.evaluation_set_version}{sample_id}".encode("utf-8") f"{self.evaluation_set_version}{sample_id}".encode()
).hexdigest(), ).hexdigest(),
) )
self.assertEqual( self.assertEqual(

View File

@ -4,7 +4,6 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import pathlib import pathlib
import sys import sys
import tempfile import tempfile
@ -21,11 +20,11 @@ for _import_dir in (SCRIPT_DIR, TEST_DIR, QUALITY_GATE_DIR, REPLAY_GATE_TEST_DIR
if str(_import_dir) not in sys.path: if str(_import_dir) not in sys.path:
sys.path.insert(0, str(_import_dir)) sys.path.insert(0, str(_import_dir))
from muse_role import FIXED_OPUS_MODEL_ID # noqa: E402 from muse_role import FIXED_OPUS_MODEL_ID # noqa: E402
from run_replay import _parse_args, _planner_prompt, run_replay as _run_replay # noqa: E402 from run_replay import _parse_args, _planner_prompt # noqa: E402
from run_replay import run_replay as _run_replay
from test_writer_gate import build_input, source_bundle # noqa: E402 from test_writer_gate import build_input, source_bundle # noqa: E402
from writer_gate import issue_gate_report_and_receipt # noqa: E402
from write_report import render_report # noqa: E402 from write_report import render_report # noqa: E402
from writer_gate import issue_gate_report_and_receipt # noqa: E402
SOURCE_HASH = "sha256:02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4" SOURCE_HASH = "sha256:02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4"
SOURCE_VERSION = f"raw-file-v1:{SOURCE_HASH}" SOURCE_VERSION = f"raw-file-v1:{SOURCE_HASH}"

View File

@ -18,7 +18,6 @@ from build_snapshot import ( # noqa: E402
remove_terminal_fields, remove_terminal_fields,
) )
META = { META = {
"targetChapter": 489, "targetChapter": 489,
"referenceWork": {"id": "deep-space", "version": "v1"}, "referenceWork": {"id": "deep-space", "version": "v1"},

View File

@ -21,7 +21,6 @@ from check_snapshot import ( # noqa: E402
check_target_sources, check_target_sources,
) )
AUTH = { AUTH = {
"sourceStatus": "active", "sourceStatus": "active",
"copyrightStatus": "research_only", "copyrightStatus": "research_only",

View File

@ -3,8 +3,8 @@
from __future__ import annotations from __future__ import annotations
import json
import inspect import inspect
import json
import pathlib import pathlib
import sys import sys
import unittest import unittest
@ -20,7 +20,6 @@ from load_reference_work import ( # noqa: E402
project_card, project_card,
) )
FILE_HASH = "02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4" FILE_HASH = "02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4"
SOURCE_HASH = f"sha256:{FILE_HASH}" SOURCE_HASH = f"sha256:{FILE_HASH}"
SOURCE_VERSION = f"raw-file-v1:{SOURCE_HASH}" SOURCE_VERSION = f"raw-file-v1:{SOURCE_HASH}"

View File

@ -11,7 +11,6 @@ from contextlib import contextmanager
from decimal import Decimal from decimal import Decimal
from unittest import mock from unittest import mock
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "备份作品抽取结果" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "备份作品抽取结果" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -2,9 +2,9 @@
"""最终交付格式校验器的回归检查;不模拟或约束创作对话。""" """最终交付格式校验器的回归检查;不模拟或约束创作对话。"""
import sys import sys
from pathlib import Path
import tempfile import tempfile
import unittest import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[3] 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"

View File

@ -10,7 +10,6 @@ from decimal import Decimal
from unittest import mock from unittest import mock
import muse_llm import muse_llm
from muse_role import ( from muse_role import (
CONTENT_POLICY_ALIAS, CONTENT_POLICY_ALIAS,
CONTENT_POLICY_VERSION, CONTENT_POLICY_VERSION,

View File

@ -15,15 +15,14 @@
跑法:仓库根目录 `.venv/bin/python tests/skills/抽取作品知识/test_parse_upgrade_offline.py` 跑法:仓库根目录 `.venv/bin/python tests/skills/抽取作品知识/test_parse_upgrade_offline.py`
""" """
import json
import hashlib import hashlib
import inspect import inspect
from io import StringIO import json
import pathlib import pathlib
import sys import sys
from contextlib import nullcontext from contextlib import nullcontext, redirect_stderr
from contextlib import redirect_stderr
from copy import deepcopy from copy import deepcopy
from io import StringIO
from unittest.mock import patch from unittest.mock import patch
from click.testing import CliRunner from click.testing import CliRunner
@ -32,8 +31,8 @@ from click.testing import CliRunner
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "抽取作品知识" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "抽取作品知识" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
import upgrade as pu # noqa: E402
import muse_embed as embed_drafts import muse_embed as embed_drafts
import upgrade as pu # noqa: E402
_passed = 0 _passed = 0

View File

@ -5,7 +5,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "抽取作品知识" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "抽取作品知识" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -6,8 +6,11 @@ import sys
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "extract" / "抽取章节知识" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "extract" / "抽取章节知识" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
from extract_knowledge import ExtractionContractError, normalize_extraction, salvage_extraction # noqa: E402 from extract_knowledge import ( # noqa: E402
ExtractionContractError,
normalize_extraction,
salvage_extraction,
)
BODY = "林深走进舰桥,何岚把黑色钥匙交给他。" BODY = "林深走进舰桥,何岚把黑色钥匙交给他。"

View File

@ -10,7 +10,6 @@ import tempfile
import unittest import unittest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "拆解书稿" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "拆解书稿" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -25,7 +25,8 @@ for path in (TEST_DIR, SCRIPT_DIR, CONTINUATION_DIR, READ_CONTEXT_DIR):
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from run_writer_semantic_detector import ( # noqa: E402 from run_writer_semantic_detector import ( # noqa: E402
build_semantic_input_v3, validate_semantic_detector_input, build_semantic_input_v3,
validate_semantic_detector_input,
) )
from test_check_writer_candidate import _valid_pair # noqa: E402 from test_check_writer_candidate import _valid_pair # noqa: E402

View File

@ -19,8 +19,8 @@ for path in (SCRIPT_DIR, CONTINUATION_DIR, READ_CONTEXT_DIR, WRITER_TEST_DIR):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from test_run_writer import _bound_context # noqa: E402
from check_writer_candidate import check_writer_candidate # noqa: E402 from check_writer_candidate import check_writer_candidate # noqa: E402
from test_run_writer import _bound_context # noqa: E402
from writer_contract import build_candidate_envelope, han_count # noqa: E402 from writer_contract import build_candidate_envelope, han_count # noqa: E402

View File

@ -8,7 +8,8 @@ import hashlib
import pathlib import pathlib
import sys import sys
import unittest import unittest
from typing import Any, Mapping, Sequence from collections.abc import Mapping, Sequence
from typing import Any
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "semantic" / "核对内容一致性" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "semantic" / "核对内容一致性" / "scripts"
@ -22,8 +23,8 @@ from run_writer_semantic_detector import ( # noqa: E402
build_safe_semantic_diagnostic, build_safe_semantic_diagnostic,
build_semantic_model_input, build_semantic_model_input,
build_semantic_report_json_schema, build_semantic_report_json_schema,
canonical_sha256,
calculate_semantic_metrics, calculate_semantic_metrics,
canonical_sha256,
is_semantic_schema_specialization, is_semantic_schema_specialization,
run_writer_semantic_detector, run_writer_semantic_detector,
validate_semantic_detector_input, validate_semantic_detector_input,

View File

@ -13,7 +13,6 @@ import sys
import tempfile import tempfile
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SKILL_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "dispatch" / "skills" / "派发智能体任务" / "scripts" SKILL_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "dispatch" / "skills" / "派发智能体任务" / "scripts"
EVIDENCE_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts" EVIDENCE_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
@ -21,7 +20,14 @@ for path in (PROJECT_ROOT, SKILL_DIR, EVIDENCE_DIR):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
import role_task as agent_task # noqa: E402 from agent_trace import AgentTraceWriter # noqa: E402
from dispatch_agent_task import EXIT_OUTPUT_INVALID, EXIT_SPEC_INVALID, run_dispatch # noqa: E402
from framework.adapters.pi.runner import ( # noqa: E402
ExecutionPolicy,
FrameworkError,
PiAgentRunner,
build_pi_argv,
)
from role_task import ( # noqa: E402 from role_task import ( # noqa: E402
OutputInvalidError, OutputInvalidError,
TaskSpecError, TaskSpecError,
@ -29,14 +35,6 @@ from role_task import ( # noqa: E402
load_spec, load_spec,
validate_structured_output, validate_structured_output,
) )
from framework.adapters.pi.runner import ( # noqa: E402
ExecutionPolicy,
FrameworkError,
PiAgentRunner,
build_pi_argv,
)
from dispatch_agent_task import EXIT_OUTPUT_INVALID, EXIT_SPEC_INVALID, run_dispatch # noqa: E402
from agent_trace import AgentTraceWriter # noqa: E402
REPO_ROOT = PROJECT_ROOT REPO_ROOT = PROJECT_ROOT

View File

@ -11,7 +11,6 @@ import json
import pathlib import pathlib
import subprocess import subprocess
import sys import sys
import tempfile
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
@ -21,7 +20,6 @@ for path in (PROJECT_ROOT, SKILL_DIR):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
import read_tools # noqa: E402
from read_tools import TOOL_REGISTRY, execute_tool # noqa: E402 from read_tools import TOOL_REGISTRY, execute_tool # noqa: E402
READ_TOOLS_PY = SKILL_DIR / "read_tools.py" READ_TOOLS_PY = SKILL_DIR / "read_tools.py"

View File

@ -8,9 +8,8 @@ import pathlib
import unittest import unittest
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
from click.testing import CliRunner
import muse_embed as embed import muse_embed as embed
from click.testing import CliRunner
# CLI 壳留在 Skill 里、不随包安装,只能按路径加载 # CLI 壳留在 Skill 里、不随包安装,只能按路径加载
CLI_PATH = (pathlib.Path(__file__).resolve().parents[3] CLI_PATH = (pathlib.Path(__file__).resolve().parents[3]

View File

@ -4,7 +4,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "sovereignty" / "确认知识草稿" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "sovereignty" / "确认知识草稿" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -5,9 +5,8 @@
该 schema(字段权威)声明的必填/推荐字段一致,防止两者再次漂移。 该 schema(字段权威)声明的必填/推荐字段一致,防止两者再次漂移。
""" """
from pathlib import Path
import unittest import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[3] PROJECT_ROOT = Path(__file__).resolve().parents[3]
SKILL = ( SKILL = (

View File

@ -14,26 +14,25 @@ SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humanization" /
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
import yaml import yaml
from capture_cases import ( from capture_cases import (
CaseCardError,
CAPTURE_CLI_COMMANDS, CAPTURE_CLI_COMMANDS,
LIFECYCLE_COMMANDS_FORBIDDEN, LIFECYCLE_COMMANDS_FORBIDDEN,
PATTERNS, PATTERNS,
CaseCardError,
_parser,
annotate_card, annotate_card,
build_case_card,
build_inventory,
build_revalidation_report,
capture_feedback, capture_feedback,
capture_file, capture_file,
build_inventory,
build_case_card,
confirm_card, confirm_card,
project_sample,
build_revalidation_report,
load_bundle, load_bundle,
main,
project_sample,
revalidate_card, revalidate_card,
text_sha256, text_sha256,
validate_card, validate_card,
main,
_parser,
) )

View File

@ -11,13 +11,11 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
if str(SCRIPT_DIR) not in sys.path: if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
import agent_trace # noqa: E402
from agent_trace import AgentTraceWriter, model_ids_match, persist_agent_evidence # noqa: E402 from agent_trace import AgentTraceWriter, model_ids_match, persist_agent_evidence # noqa: E402

View File

@ -27,11 +27,16 @@ SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记
if str(SCRIPT_DIR) not in sys.path: if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))
from muse_db import connect # noqa: E402
import lesson_registry # noqa: E402
from lesson_registry import ( # noqa: E402 from lesson_registry import ( # noqa: E402
LessonError, list_lessons, promote, propose_lesson, propose_lesson_dedup, reject, start_review, LessonError,
list_lessons,
promote,
propose_lesson,
propose_lesson_dedup,
reject,
start_review,
) )
from muse_db import connect # noqa: E402
SUFFIX = uuid.uuid4().hex[:10] SUFFIX = uuid.uuid4().hex[:10]
RUN_PREFIX = f"unittest-lesson-{SUFFIX}" RUN_PREFIX = f"unittest-lesson-{SUFFIX}"

View File

@ -10,7 +10,7 @@ import stat
import sys import sys
import tempfile import tempfile
import unittest import unittest
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from unittest import mock from unittest import mock
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
@ -24,7 +24,7 @@ from raw_vault import RawVaultError, RawVaultManager # noqa: E402
def future_time(hours: int = 1) -> str: def future_time(hours: int = 1) -> str:
"""生成不超过 24 小时的 UTC 保留截止时间。""" """生成不超过 24 小时的 UTC 保留截止时间。"""
return (datetime.now(timezone.utc) + timedelta(hours=hours)).isoformat() return (datetime.now(UTC) + timedelta(hours=hours)).isoformat()
class RawVaultTest(unittest.TestCase): class RawVaultTest(unittest.TestCase):
@ -231,7 +231,7 @@ class RawVaultTest(unittest.TestCase):
def test_create_rejects_insufficient_min_remaining_without_lease_or_vault(self): def test_create_rejects_insufficient_min_remaining_without_lease_or_vault(self):
"""声明的最低剩余租期不足时必须在落 lease / 建 vault 前稳定失败关闭。""" """声明的最低剩余租期不足时必须在落 lease / 建 vault 前稳定失败关闭。"""
short_retention = (datetime.now(timezone.utc) + timedelta(seconds=30)).isoformat() short_retention = (datetime.now(UTC) + timedelta(seconds=30)).isoformat()
with self.assertRaises(RawVaultError) as raised: with self.assertRaises(RawVaultError) as raised:
self.manager.create_vault( self.manager.create_vault(
authorization_id="auth-min", authorization_id="auth-min",
@ -267,7 +267,7 @@ class RawVaultTest(unittest.TestCase):
def test_write_rejected_after_expiry_but_cleanup_still_works(self): def test_write_rejected_after_expiry_but_cleanup_still_works(self):
"""租约到期后 write 立即失败关闭,但 cleanup 仍能收敛过期 lease。""" """租约到期后 write 立即失败关闭,但 cleanup 仍能收敛过期 lease。"""
base = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) base = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
clock = {"now": base} clock = {"now": base}
with mock.patch.object(raw_vault, "_utc_now", side_effect=lambda: clock["now"]): with mock.patch.object(raw_vault, "_utc_now", side_effect=lambda: clock["now"]):
lease = self.manager.create_vault( lease = self.manager.create_vault(
@ -292,7 +292,7 @@ class RawVaultTest(unittest.TestCase):
def test_recover_cleans_expired_open_lease(self): def test_recover_cleans_expired_open_lease(self):
"""恢复扫描必须能收敛已过期的 open lease,不因过期而卡住清理。""" """恢复扫描必须能收敛已过期的 open lease,不因过期而卡住清理。"""
base = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) base = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
clock = {"now": base} clock = {"now": base}
with mock.patch.object(raw_vault, "_utc_now", side_effect=lambda: clock["now"]): with mock.patch.object(raw_vault, "_utc_now", side_effect=lambda: clock["now"]):
lease = self.manager.create_vault( lease = self.manager.create_vault(

View File

@ -4,7 +4,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -4,7 +4,6 @@ import pathlib
import sys import sys
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR)) sys.path.insert(0, str(SCRIPT_DIR))

View File

@ -5,7 +5,6 @@ import pathlib
import re import re
import unittest import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
DDL_PATH = PROJECT_ROOT / "muse" / "authority" / "db" / "ddl" / "96-example参考作品授权快照.sql" DDL_PATH = PROJECT_ROOT / "muse" / "authority" / "db" / "ddl" / "96-example参考作品授权快照.sql"

View File

@ -13,7 +13,6 @@ SCRIPTS_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "访
sys.path.insert(0, str(SCRIPTS_DIR)) sys.path.insert(0, str(SCRIPTS_DIR))
import click # noqa: E402 import click # noqa: E402
import db # noqa: E402 import db # noqa: E402

View File

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""细纲 rubric 的离线回归测试。""" """细纲 rubric 的离线回归测试。"""
import pathlib
import inspect import inspect
import pathlib
import sys import sys
import unittest import unittest

View File

@ -9,7 +9,8 @@ import json
import pathlib import pathlib
import sys import sys
import unittest import unittest
from typing import Any, Mapping from collections.abc import Mapping
from typing import Any
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "judge" / "评估内容质量" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "judge" / "评估内容质量" / "scripts"

View File

@ -15,8 +15,8 @@ if str(SCRIPT_DIR) not in sys.path:
from writer_rubric import ( # noqa: E402 from writer_rubric import ( # noqa: E402
COMMON_DIMENSIONS, COMMON_DIMENSIONS,
DIMENSIONS, DIMENSIONS,
RUBRIC_PROFILE,
RUBRIC_POLICY_VERSION, RUBRIC_POLICY_VERSION,
RUBRIC_PROFILE,
SCENARIO_DIMENSION, SCENARIO_DIMENSION,
SCENARIO_TYPES, SCENARIO_TYPES,
adjudicate_reviews, adjudicate_reviews,

View File

@ -9,7 +9,6 @@ import pathlib
import sys import sys
import types import types
import muse_llm as llm import muse_llm as llm
llm.TOKEN = "test-token" llm.TOKEN = "test-token"

View File

@ -8,7 +8,6 @@ SCRIPTS = PROJECT_ROOT / "muse" / "content" / "work" / "skills" / "generate" / "
sys.path.insert(0, str(SCRIPTS)) sys.path.insert(0, str(SCRIPTS))
import click # noqa: E402 import click # noqa: E402
from assert_expected_revision import ( # noqa: E402 from assert_expected_revision import ( # noqa: E402
RevisionConflict, RevisionConflict,
assert_expected_revision, assert_expected_revision,

View File

@ -10,7 +10,6 @@ from unittest.mock import Mock, patch
from click.testing import CliRunner from click.testing import CliRunner
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SKILLS_DIR = PROJECT_ROOT / ".agent" / "skills" SKILLS_DIR = PROJECT_ROOT / ".agent" / "skills"
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "重置作品抽取结果" / "scripts" SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "重置作品抽取结果" / "scripts"
@ -20,12 +19,11 @@ sys.path.insert(0, str(SCRIPT_DIR))
sys.path.insert(0, str(BACKUP_SCRIPTS)) sys.path.insert(0, str(BACKUP_SCRIPTS))
sys.path.insert(0, str(EXTRACTION_SCRIPTS)) sys.path.insert(0, str(EXTRACTION_SCRIPTS))
import upgrade as parse # noqa: E402
import backup_upgrade_work as backup # noqa: E402 import backup_upgrade_work as backup # noqa: E402
import reset_upgrade_work as reset # noqa: E402 import reset_upgrade_work as reset # noqa: E402
import upgrade as parse # noqa: E402
from upgrade_work_lock import UpgradeWorkLockUnavailable # noqa: E402 from upgrade_work_lock import UpgradeWorkLockUnavailable # noqa: E402
BACKUP_ID = "11111111-1111-4111-8111-111111111111" BACKUP_ID = "11111111-1111-4111-8111-111111111111"
CONFIRMATION_SHA = "c" * 64 CONFIRMATION_SHA = "c" * 64
BACKUP_ARGS = [ BACKUP_ARGS = [

Some files were not shown because too many files have changed in this diff Show More