W18 拆书、归并、互斥与补偿(收口):分章切窗(确定性章界/窗计划哈希/漂移守卫/覆盖核对)、逐窗模型提取(S02 证据合同、输出合同含实体引文)、确定性实体归并(同名异型必歧义不误并、合并带证据与窗归属)、分析版本(uuid5 版本身份、版本台账、失败窗单独重试、旧结论不抹)、样本选题观察陈述;研究拆书五步流程(同源互斥=排他范围领取层强制、窗口断点逐窗检查点、恢复只补缺失窗、预算含重试余量、补偿只触本任务检查点);CLI 研究命令+HTTP 4 端点+工作台拆书工作区/原文分析对照/榜单与选题页(资料对照页退役并入资料页,structure-form 纵排修复表单遮挡)+拆解书稿/检查与维护拆书结果技能;12 新 NC(NC-w18-*),离线 275 / 非宿主数据库 231 / 前端 27,浏览器 2/2+视觉判定通过;整包验证一次通过,W18 置 verified。
This commit is contained in:
parent
5b22b31b74
commit
06fcfb32cd
26
.agent/skills/操作/拆解书稿/SKILL.md
Normal file
26
.agent/skills/操作/拆解书稿/SKILL.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: 拆解书稿
|
||||
description: 发起参考作品分窗拆解并跟踪进度;同源互斥、失败可恢复。研究一部作品的章节、实体与写法时使用。
|
||||
---
|
||||
|
||||
# 拆解书稿
|
||||
|
||||
把参考作品来源按窗口拆解为版本化分析成果;同源不可并发,失败停在可解释断点。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 需要研究参考作品的章节节拍、叙事阶段、实体与写法时。
|
||||
- 来源更新到新版本,需要重新拆解时。
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. 在「拆书工作区」选定来源,写明选书理由(写入拆书依据),或经 `python -m muse 研究 拆解 <请求JSON>` 发起。
|
||||
2. 台账显示每次拆解的状态、完成窗口、失败窗与歧义数;版本身份由来源版本与窗计划哈希确定。
|
||||
3. 失败任务点「恢复重试」或 `POST /api/v1/analyses/{task_id}/resume`;已完成窗口不重跑。
|
||||
4. 任务完成后在「原文与分析对照」逐窗查看原文与分析结果。
|
||||
|
||||
## 边界
|
||||
|
||||
- 同一来源同时只有一个拆解任务;接管由作用域租约强制。
|
||||
- 输入漂移的窗口拒绝写入旧分析版本;失败不伪称窗口完成。
|
||||
- 分析成果是参考作品观察,不自动成为本书设定。
|
||||
3
.agent/skills/操作/拆解书稿/目录.md
Normal file
3
.agent/skills/操作/拆解书稿/目录.md
Normal file
@ -0,0 +1,3 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 技能说明 | SKILL.md | 参考作品分窗拆解的发起与进度跟踪 | 研究参考作品章节、实体与写法时 | 同源互斥;失败可恢复;分析不是本书事实 |
|
||||
26
.agent/skills/操作/检查与维护拆书结果/SKILL.md
Normal file
26
.agent/skills/操作/检查与维护拆书结果/SKILL.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
name: 检查与维护拆书结果
|
||||
description: 检查拆书台账、歧义与失败窗口,按窗口重算或以新版本重建;旧结论保留可回查。维护分析质量时使用。
|
||||
---
|
||||
|
||||
# 检查与维护拆书结果
|
||||
|
||||
对拆书成果做检查与维护:歧义逐项处理、失败窗口重算、版本对照与选用。
|
||||
|
||||
## 何时使用
|
||||
|
||||
- 台账出现失败窗或歧义时。
|
||||
- 来源前进到新版本,需要对照或重建分析时。
|
||||
|
||||
## 操作步骤
|
||||
|
||||
1. `python -m muse 研究 进度 <来源ID>` 或「拆书工作区」台账查看状态。
|
||||
2. `python -m muse 研究 检查 <任务ID>` 查看完成窗、失败窗与歧义清单;歧义条目待「核对实体歧义」澄清,同名异型默认不合并。
|
||||
3. 失败窗重算:经恢复入口带 `retry_windows` 指定窗口;已完成窗不重跑,成本包络含重试余量。
|
||||
4. 来源新版本重建:直接重新发起拆解;旧版本分析保留可回查,版本身份不冲突。
|
||||
|
||||
## 边界
|
||||
|
||||
- 只撤销本任务可证明归属的中间产物;不触碰外部确认与来源版本。
|
||||
- 榜单名次与拆解成果都是观察记录,不作效果归因。
|
||||
- 重建不删除参考原文,不暗改本书事实。
|
||||
3
.agent/skills/操作/检查与维护拆书结果/目录.md
Normal file
3
.agent/skills/操作/检查与维护拆书结果/目录.md
Normal file
@ -0,0 +1,3 @@
|
||||
| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |
|
||||
|------|----------|----------|----------|----------|
|
||||
| 技能说明 | SKILL.md | 拆书成果的检查、重算与版本维护 | 处理失败窗、歧义与版本对照时 | 歧义不误并;旧结论保留;只撤可证明归属 |
|
||||
@ -11,3 +11,5 @@
|
||||
| 确认事实增量 | [确认事实增量/SKILL.md](确认事实增量/SKILL.md) | 章后派生事实提案的作者独立确认 | 采纳或保存正文后处理候选事实提案时 | 只确认已审提案;不代作者决定 |
|
||||
| 导入资料 | [导入资料/SKILL.md](导入资料/SKILL.md) | 参考书、网页、笔记与灵感的来源导入 | 建立来源、版本与原文对照时 | 授权用途决定消费范围;外部资料不是事实授权 |
|
||||
| 采集资料与榜单 | [采集资料与榜单/SKILL.md](采集资料与榜单/SKILL.md) | 具名榜单与网页的时点采集 | 榜单观察与网页资料留存时 | 失败如实保留;不补造数据、不作效果归因 |
|
||||
| 拆解书稿 | [拆解书稿/SKILL.md](拆解书稿/SKILL.md) | 参考作品分窗拆解的发起与进度 | 研究参考作品章节、实体与写法时 | 同源互斥;失败可恢复;分析不是本书事实 |
|
||||
| 检查与维护拆书结果 | [检查与维护拆书结果/SKILL.md](检查与维护拆书结果/SKILL.md) | 拆书成果的检查、重算与版本维护 | 处理失败窗、歧义与版本对照时 | 歧义不误并;旧结论保留 |
|
||||
|
||||
@ -2496,7 +2496,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/muse______http____________________8"
|
||||
"$ref": "#/components/schemas/muse______http____________________9"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2555,7 +2555,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"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": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -7907,6 +8047,47 @@
|
||||
],
|
||||
"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": {
|
||||
"properties": {
|
||||
"command_id": {
|
||||
@ -8165,6 +8346,68 @@
|
||||
"title": "登记榜单请求"
|
||||
},
|
||||
"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": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
@ -8181,47 +8424,6 @@
|
||||
"writing_task_id"
|
||||
],
|
||||
"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": "审校报告呈现"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1100,7 +1100,7 @@
|
||||
| [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/功能/资料研究/榜单与选题页.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-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) | 前端工程、应用框架、客户端或共享展示 |
|
||||
|
||||
@ -2833,8 +2833,18 @@
|
||||
"模块设计/B03-资料研究.md",
|
||||
"数据模型/旧知识记录分流.md"
|
||||
],
|
||||
"status": "planned",
|
||||
"execution_evidence": [],
|
||||
"status": "verified",
|
||||
"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": [
|
||||
".agent/skills/操作/拆解书稿/SKILL.md",
|
||||
".agent/skills/操作/拆解书稿/目录.md",
|
||||
|
||||
@ -19,7 +19,7 @@ agent-example/
|
||||
│ ├── vite-env.d.ts # 构建环境类型
|
||||
│ ├── 功能/
|
||||
│ │ └── 资料研究/
|
||||
│ │ └── 资料对照页.tsx # 来源与原文对照页面
|
||||
│ │ └── 资料页.tsx # 来源与原文对照页面(W18 起并入研究入口)
|
||||
│ ├── 应用/
|
||||
│ │ ├── 作品会话.ts # 切作品屏障、取消和迟到隔离
|
||||
│ │ ├── 作者会话.ts # 作者登录与会话
|
||||
@ -749,16 +749,45 @@ agent-example/
|
||||
- 边界:真实隔离库与真实浏览器;不 mock 后端;视觉判定独立留证。
|
||||
- 目标:通过任务与失败候选的检查呈现及服务端阻断可复核,页面无预期外控制台错误。
|
||||
|
||||
<a id="file-8d1a7fc626190cb1"></a>
|
||||
<a id="file-529b1c403b83b56f"></a>
|
||||
|
||||
### `web/src/功能/资料研究/资料对照页.tsx`
|
||||
### `web/src/功能/资料研究/资料页.tsx`
|
||||
|
||||
- 归属与种类:前端;页面组件。
|
||||
- 意图:让作者在一页内对照来源版本原文、清理候选结果与榜单快照。
|
||||
- 职责:来源清单选择、版本原文呈现、清理对照应用、快照列表(失败态如实显示)。
|
||||
- 意图:让作者在一页内对照来源版本原文、清理候选结果并进入研究工作区。
|
||||
- 职责:来源清单选择、版本原文呈现、清理对照应用、研究入口链接。
|
||||
- 边界:只读呈现与显式动作;不伪造数据、不推断效果因果。
|
||||
- 目标:来源、原文与快照可追溯;失败可见可重试。
|
||||
- 目标:来源、原文可追溯;清理对照可见;研究入口可达。
|
||||
- 合同依据:[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>
|
||||
|
||||
|
||||
@ -14223,6 +14223,12 @@
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-49d4f4baa42126b9",
|
||||
"test_case_refs": [
|
||||
"NC-w18-18a01a",
|
||||
"NC-w18-18a02b",
|
||||
"NC-w18-18a03c",
|
||||
"NC-w18-18a04d",
|
||||
"NC-w18-18a05e",
|
||||
"NC-w18-18a06f",
|
||||
"TC-0045c6f3ed48",
|
||||
"TC-030fe0a0b69b",
|
||||
"TC-052b4fa41fd7",
|
||||
@ -16517,6 +16523,8 @@
|
||||
],
|
||||
"design_location": "文件设计/测试与夹具.md#file-33a0f3ae98d9755f",
|
||||
"test_case_refs": [
|
||||
"NC-w18-18b01a",
|
||||
"NC-w18-18b02b",
|
||||
"TC-2c9a90e4b1c1",
|
||||
"TC-43841d36dba4",
|
||||
"TC-49f968fe703b",
|
||||
@ -16667,6 +16675,9 @@
|
||||
"design_location": "文件设计/测试与夹具.md#file-2c4bcda0dfce8f6e",
|
||||
"legacy_sources": [],
|
||||
"test_case_refs": [
|
||||
"NC-w18-18b03c",
|
||||
"NC-w18-18b04d",
|
||||
"NC-w18-18b05e",
|
||||
"TC-00c1a8dabf0d",
|
||||
"TC-039b3827b252",
|
||||
"TC-068b740760eb",
|
||||
@ -21591,21 +21602,6 @@
|
||||
"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",
|
||||
"owner": "验证",
|
||||
@ -21620,7 +21616,8 @@
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/前端-应用与通用界面.md#file-0c134b40a9faae3a",
|
||||
"test_case_refs": [
|
||||
"NC-w17-browser"
|
||||
"NC-w17-browser",
|
||||
"NC-w18-browser"
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -21660,6 +21657,23 @@
|
||||
"NC-w17-w17b01",
|
||||
"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": {
|
||||
|
||||
@ -1305,8 +1305,7 @@ agent-example/
|
||||
│ │ │ ├── 拆书工作区.tsx # 分析进度与恢复
|
||||
│ │ │ ├── 数据.ts # 资料研究查询和业务命令
|
||||
│ │ │ ├── 榜单与选题页.tsx # 快照、比较和选书
|
||||
│ │ │ ├── 资料对照页.tsx # 来源与原文对照页面
|
||||
│ │ │ └── 资料页.tsx # 来源导入和版本
|
||||
│ │ │ └── 资料页.tsx # 来源与原文对照页面(W18 起并入研究入口)
|
||||
│ │ ├── 应用/
|
||||
│ │ │ ├── 作品会话.ts # 切作品屏障、取消和迟到隔离
|
||||
│ │ │ ├── 作者会话.ts # 作者登录与会话
|
||||
|
||||
@ -3246,3 +3246,15 @@
|
||||
- 环境:真实隔离库 + Chrome。
|
||||
- 合同:[权威定义](../模块设计/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)。
|
||||
|
||||
|
||||
@ -123223,6 +123223,198 @@
|
||||
"environment": "真实隔离库 + Chrome",
|
||||
"verification_state": "已实现未执行",
|
||||
"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/回合开发/整包/"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -3745,3 +3745,135 @@
|
||||
- 环境:隔离 PostgreSQL、真实 CLI 与 ASGI。
|
||||
- 合同:[权威定义](../接口契约/元数据投影与版本.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)。
|
||||
|
||||
|
||||
@ -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:
|
||||
"""登记并发布生成正文流程;计价由调用方注入,模板默认取仓内可审配置。"""
|
||||
|
||||
|
||||
@ -45,6 +45,12 @@ def _解析参数(argv: list[str] | None) -> argparse.Namespace:
|
||||
创作解析.add_argument("动作", choices=["发起", "报告"])
|
||||
创作解析.add_argument("目标", nargs="?", help="发起时为请求 JSON 文件;报告时为候选 ID")
|
||||
创作解析.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_argument("配置", help="应用配置 TOML 路径")
|
||||
结构解析.add_argument("动作", choices=["发布内置", "查看", "维护"])
|
||||
@ -207,6 +213,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
结果 = 运行创作命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标, 版本=参数.版本)
|
||||
print(json.dumps(结果, ensure_ascii=False, default=str))
|
||||
return _退出码_成功
|
||||
if 参数.命令 == "研究":
|
||||
from muse.接入.cli.研究命令 import 运行研究命令
|
||||
|
||||
结果 = 运行研究命令(构建(读取配置(参数.配置)), 参数.动作, 参数.目标)
|
||||
print(json.dumps(结果, ensure_ascii=False, default=str))
|
||||
return _退出码_成功
|
||||
if 参数.命令 == "自检":
|
||||
装配 = 构建(读取配置(参数.配置))
|
||||
for 键, 值 in 自检(装配).items():
|
||||
|
||||
98
src/muse/接入/cli/研究命令.py
Normal file
98
src/muse/接入/cli/研究命令.py
Normal 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__ = ["运行研究命令", "拆解请求"]
|
||||
@ -1,4 +1,4 @@
|
||||
"""资料研究的 HTTP 外壳:导入、来源版本对照、清理候选与榜单快照。"""
|
||||
"""资料研究的 HTTP 外壳:导入、来源版本对照、清理候选、榜单快照与研究拆书。"""
|
||||
|
||||
from typing import Any, Literal
|
||||
|
||||
@ -38,6 +38,15 @@ class 采集榜单请求(BaseModel):
|
||||
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):
|
||||
return request.app.state.装配
|
||||
|
||||
@ -157,3 +166,49 @@ def 列出快照(ranking_id: str, request: Request, 作者: 作者依赖) -> lis
|
||||
装配 = _装配(request)
|
||||
with 装配.要求数据库().连接(只读=True) as 连:
|
||||
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}
|
||||
|
||||
359
src/muse/编排/研究拆书.py
Normal file
359
src/muse/编排/研究拆书.py
Normal 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__ = [
|
||||
"登记研究拆书",
|
||||
"发起研究拆书",
|
||||
"查询拆书状态",
|
||||
"流程定义_研究拆书",
|
||||
"研究拆书步骤",
|
||||
"拆书错误",
|
||||
"流程身份",
|
||||
"流程版本",
|
||||
"处理器版本集",
|
||||
]
|
||||
101
src/muse/资料研究/分析版本.py
Normal file
101
src/muse/资料研究/分析版本.py
Normal 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__ = ["版本身份", "读取版本状态", "待重试窗口", "版本台账", "基准引用", "资料错误"]
|
||||
117
src/muse/资料研究/分章切窗.py
Normal file
117
src/muse/资料研究/分章切窗.py
Normal 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__ = [
|
||||
"窗口",
|
||||
"分章",
|
||||
"切窗",
|
||||
"窗计划哈希",
|
||||
"漂移检查",
|
||||
"覆盖核对",
|
||||
"窗口载荷",
|
||||
"默认窗口章数",
|
||||
"资料错误",
|
||||
]
|
||||
100
src/muse/资料研究/实体归并.py
Normal file
100
src/muse/资料研究/实体归并.py
Normal 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__ = ["收集实体", "归并", "归并哈希", "资料错误"]
|
||||
124
src/muse/资料研究/拆书分析.py
Normal file
124
src/muse/资料研究/拆书分析.py
Normal 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__ = ["窗口输出合同", "系统提示", "执行窗口提取", "资料错误"]
|
||||
19
src/muse/资料研究/提示词/分析参考作品.md
Normal file
19
src/muse/资料研究/提示词/分析参考作品.md
Normal file
@ -0,0 +1,19 @@
|
||||
# 分析参考作品 · 任务提示模板
|
||||
|
||||
本模板定义「分析参考作品」逐窗提取任务的可版本化表达;角色身份、字段结构、权限与预算从各自权威装配,本模板不重复。
|
||||
|
||||
## 任务
|
||||
|
||||
对给定窗口(来源版本 + 码点区间 + 内容哈希)输出拆书分析:
|
||||
|
||||
1. **章节与节拍**:窗口内每章的标题与情节节拍(beat,一句话)。
|
||||
2. **叙事阶段**:窗口覆盖的叙事阶段(如 开局/推进/转折/收束)。
|
||||
3. **实体**:出现的人物、地点、事物;每人附类型与原文引文证据;别名一并列出。
|
||||
4. **写法**:可借鉴的写作技法(视角、节奏、悬念、对话等),逐条一句话。
|
||||
|
||||
## 合同
|
||||
|
||||
- 只依据窗口原文;窗口外内容不补猜、不引用。
|
||||
- 实体证据必须是窗口原文中的字面引文。
|
||||
- 输出一个 JSON 对象,字段结构以任务输出合同为准(chapters/phases/entities/techniques)。
|
||||
- 窗口号是切分序号,不是真实章号;章号以窗口内章节标题为准。
|
||||
18
src/muse/资料研究/提示词/核对实体歧义.md
Normal file
18
src/muse/资料研究/提示词/核对实体歧义.md
Normal file
@ -0,0 +1,18 @@
|
||||
# 核对实体歧义 · 任务提示模板
|
||||
|
||||
本模板为「核对实体歧义」任务提供可版本化表达;W18 阶段歧义只登记不调用模型,
|
||||
本模板供后续语义核对阶段装配使用。
|
||||
|
||||
## 任务
|
||||
|
||||
对拆书归并中登记的歧义条目(同名不同类型的实体候选)逐项核对:
|
||||
|
||||
1. 列出各候选的出现窗与证据引文。
|
||||
2. 依据上下文判断各候选是否同一实体;判断必须给出引文级依据。
|
||||
3. 结论只能是三态之一:同一实体(合并并说明)、不同实体(分别命名区分)、证据不足(维持歧义)。
|
||||
|
||||
## 合同
|
||||
|
||||
- 不因为同名就直接合并;类型冲突的同名默认不同实体。
|
||||
- 只处理参考作品内部身份判定;不跨作品合并,不改写本书事实。
|
||||
- 证据不足时维持歧义,不得输出倾向性猜测。
|
||||
64
src/muse/资料研究/样本选题.py
Normal file
64
src/muse/资料研究/样本选题.py
Normal 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__ = ["样本比较", "选书交接单", "选题依据哈希", "资料错误"]
|
||||
@ -4,10 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -7,7 +7,6 @@ import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
AUTHOR_ROOT = ROOT / ".agent" / "作者"
|
||||
SCENE_ROOT = AUTHOR_ROOT / "场景"
|
||||
|
||||
@ -32,7 +32,6 @@ from framework.adapters.dsh.runner import ( # noqa: E402
|
||||
from framework.primitives.artifacts import read_jsonl # noqa: E402
|
||||
from framework.primitives.execution import FrameworkExecutionRequest # noqa: E402
|
||||
|
||||
|
||||
SESSION_LINES = [
|
||||
{
|
||||
"type": "session",
|
||||
|
||||
@ -7,7 +7,6 @@ import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -6,7 +6,6 @@ import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -4,8 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pathlib
|
||||
import tempfile
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
@ -31,7 +31,6 @@ from framework.primitives.execution import (
|
||||
FrameworkExecutionResult,
|
||||
)
|
||||
|
||||
|
||||
SCHEMA_DIR = ROOT / "framework" / "primitives" / "schemas"
|
||||
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
def _find_project_root(start: pathlib.Path) -> pathlib.Path:
|
||||
resolved = start.resolve()
|
||||
for directory in (resolved, *resolved.parents):
|
||||
|
||||
@ -7,7 +7,6 @@ import re
|
||||
import unittest
|
||||
import urllib.parse
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -6,7 +6,6 @@ import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -6,7 +6,6 @@ import pathlib
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -496,6 +496,7 @@ def 章后环境(应用测试库, tmp_path):
|
||||
"剧本": 剧本,
|
||||
"收到": 收到,
|
||||
"新作品": 新作品,
|
||||
"提供方端口": server.server_port,
|
||||
"处理器能力": sorted(set(生成处理器集) | set(章后处理器集)),
|
||||
}
|
||||
yield 环境
|
||||
@ -549,3 +550,129 @@ def 章后工具(章后环境):
|
||||
return {"类型": "文本", "文本": {"summary": 摘要, "facts": 事实}}
|
||||
|
||||
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(
|
||||
环境=环境,
|
||||
库=库,
|
||||
装配=装配,
|
||||
剧本=环境["剧本"],
|
||||
服务=服务,
|
||||
导入来源=导入来源,
|
||||
发起=发起,
|
||||
跑任务=跑任务,
|
||||
分析输出=分析输出,
|
||||
)
|
||||
|
||||
@ -11,7 +11,6 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
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 muse import replay as replay_mod # noqa: E402
|
||||
from muse.flow import dispatch as dispatch_mod # noqa: E402
|
||||
from muse.flow.adopt import adopt_candidate # 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 muse import replay as replay_mod # noqa: E402
|
||||
|
||||
|
||||
class CompoundingTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
|
||||
@ -7,7 +7,6 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -9,7 +9,6 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -8,7 +8,6 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = next(
|
||||
parent
|
||||
for parent in (pathlib.Path(__file__).resolve().parent, *pathlib.Path(__file__).resolve().parents)
|
||||
|
||||
@ -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(DIAGNOSE_SCRIPT_DIR))
|
||||
|
||||
import revise_ai_flavor as rev # 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
|
||||
|
||||
CLEAN_TEXT = "值得注意的是,门外已经下起了雨。"
|
||||
|
||||
@ -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 run_writer_pipeline import CasToken, PipelineError, run_writer_pipeline # 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 writer_contract import build_candidate_envelope, retrieval_identity # noqa: E402
|
||||
from writer_contract import retrieval_identity # noqa: E402
|
||||
|
||||
|
||||
class FakeCursor:
|
||||
|
||||
@ -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:
|
||||
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_writer_bridge import ( # noqa: E402
|
||||
DispatchWriterError,
|
||||
@ -31,7 +30,12 @@ from dispatch_writer_bridge import ( # noqa: E402
|
||||
)
|
||||
from read_tools import TOOL_REGISTRY # 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):
|
||||
|
||||
@ -4,7 +4,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "work" / "skills" / "generate" / "写下一章" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -21,11 +21,14 @@ from production_evidence_reassemble import ( # noqa: E402
|
||||
EvidenceReassembleError,
|
||||
reassemble_writer_context_for_gaps,
|
||||
)
|
||||
from run_writer_pipeline import InMemoryCasStateStore, PipelineError, run_writer_pipeline # noqa: E402
|
||||
from writer_contract import build_writer_creative_input, retrieval_identity # noqa: E402
|
||||
|
||||
from run_writer_pipeline import ( # noqa: E402
|
||||
InMemoryCasStateStore,
|
||||
PipelineError,
|
||||
run_writer_pipeline,
|
||||
)
|
||||
from test_check_writer_candidate import _requirements, _valid_pair # 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):
|
||||
|
||||
@ -24,8 +24,6 @@ sys.path.insert(0, str(SCRIPT_DIR))
|
||||
sys.path.insert(0, str(READ_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
|
||||
FIXED_OPUS_MODEL_ID,
|
||||
FIXED_OPUS_POLICY_ALIAS,
|
||||
@ -40,6 +38,12 @@ from run_writer import ( # noqa: E402
|
||||
calculate_dynamic_output_contract,
|
||||
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}
|
||||
ACTUAL_MODEL = FIXED_OPUS_MODEL_ID
|
||||
|
||||
@ -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):
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from test_check_writer_candidate import _candidate_body, _requirements, _valid_pair # noqa: E402
|
||||
from run_writer_pipeline import CasToken, InMemoryCasStateStore, PipelineError, atomic_write_json, run_writer_pipeline # noqa: E402
|
||||
from run_writer_pipeline import ( # noqa: E402
|
||||
InMemoryCasStateStore,
|
||||
PipelineError,
|
||||
atomic_write_json,
|
||||
run_writer_pipeline,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ if str(SCRIPT_DIR) not in sys.path:
|
||||
|
||||
import persist_writer_run as writer_persist # noqa: E402
|
||||
|
||||
|
||||
CANDIDATE_SHA = "a" * 64
|
||||
CONTEXT_SHA = "sha256:" + "b" * 64
|
||||
|
||||
|
||||
@ -20,8 +20,8 @@ for path in (SCRIPT_DIR,):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import two_phase_writer # noqa: E402
|
||||
import dispatch_writer_bridge as bridge # noqa: E402
|
||||
import two_phase_writer # noqa: E402
|
||||
from two_phase_writer import ( # noqa: E402
|
||||
EXPLORATION_MAX_MATERIALS,
|
||||
EXPLORATION_SCHEMA_VERSION,
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
@ -22,7 +21,10 @@ for path in (SCRIPT_DIR, PROJECT_ROOT / "muse" / "lifecycle" / "context" / "skil
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
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 = "林深把黑纹缠上手臂,异种核心在胸腔里低鸣。何岚站在舱门口没有说话。"
|
||||
|
||||
@ -30,9 +30,9 @@ for path in (SCRIPT_DIR,):
|
||||
if str(path) not in sys.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 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]
|
||||
WORK_TITLE = f"unittest-delta-{SUFFIX}"
|
||||
|
||||
@ -31,7 +31,10 @@ for path in (SCRIPT_DIR,):
|
||||
|
||||
from muse_db import connect # 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
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ import hashlib
|
||||
import pathlib
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
@ -31,8 +30,8 @@ for path in (SCRIPT_DIR,):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from muse_db import connect # noqa: E402
|
||||
import write_canonical # noqa: E402
|
||||
from muse_db import connect # noqa: E402
|
||||
from write_canonical import ConflictError, accept, discard # noqa: E402
|
||||
|
||||
SUFFIX = uuid.uuid4().hex[:10]
|
||||
|
||||
@ -25,8 +25,8 @@ for path in (SCRIPT_DIR,):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from persist_context_freeze import persist_freeze # noqa: E402
|
||||
from muse_db import connect # noqa: E402
|
||||
from persist_context_freeze import persist_freeze # noqa: E402
|
||||
|
||||
|
||||
class FreezeReplayIdempotencyDbTest(unittest.TestCase):
|
||||
|
||||
@ -20,7 +20,6 @@ for script_dir in (
|
||||
sys.path.insert(0, str(script_dir))
|
||||
|
||||
from load_reference_work import begin_read_snapshot # noqa: E402
|
||||
from search import search_cards # noqa: E402
|
||||
from retrieve_writer_sources import ( # noqa: E402
|
||||
ProductionCardIndexRepository,
|
||||
ReplayCardIndexRepository,
|
||||
@ -30,7 +29,7 @@ from retrieve_writer_sources import ( # noqa: E402
|
||||
retrieve_writer_sources,
|
||||
stable_sort_cards,
|
||||
)
|
||||
|
||||
from search import search_cards # noqa: E402
|
||||
|
||||
SOURCE_VERSION = "raw-file-v1:sha256:" + "a" * 64
|
||||
|
||||
|
||||
@ -465,7 +465,7 @@ class WriterContractTest(unittest.TestCase):
|
||||
self.assertEqual(output["candidateVersion"], 3)
|
||||
self.assertEqual(
|
||||
output["candidateSha256"],
|
||||
"sha256:" + hashlib.sha256("Café\n正文".encode("utf-8")).hexdigest(),
|
||||
"sha256:" + hashlib.sha256("Café\n正文".encode()).hexdigest(),
|
||||
)
|
||||
validate_writer_output(output)
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
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 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
|
||||
WriterReferenceWorkError,
|
||||
_requirement_hard_constraints,
|
||||
@ -51,20 +52,18 @@ from load_writer_reference_work import ( # noqa: E402
|
||||
validate_oracle_truth_pack,
|
||||
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
|
||||
MODEL_POLICY_VERSION,
|
||||
RUNTIME_ADAPTER,
|
||||
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"
|
||||
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"])
|
||||
self.assertIsNotNone(parsed.tzinfo)
|
||||
# (c) 解析后在未来、且距 now 不超过 24 小时(实际约等于 23 小时)
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
self.assertGreater(parsed, now)
|
||||
self.assertLessEqual(parsed - now, timedelta(hours=24))
|
||||
# (d) receiptSha256 == 去掉 receiptSha256 的整段规范自哈希(重签正确)
|
||||
|
||||
@ -39,7 +39,6 @@ for _path in (
|
||||
if str(_path) not in sys.path:
|
||||
sys.path.insert(0, str(_path))
|
||||
|
||||
import load_writer_reference_work as loader # noqa: E402
|
||||
from load_writer_reference_work import ( # noqa: E402
|
||||
PATTERN_CARD_TYPES,
|
||||
PATTERN_TOTAL_CAP,
|
||||
@ -52,19 +51,6 @@ from run_writer_replay.sample import ( # noqa: E402
|
||||
_build_arm_contexts,
|
||||
_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。
|
||||
from test_load_writer_reference_work import ( # noqa: E402
|
||||
@ -74,6 +60,19 @@ from test_load_writer_reference_work import ( # noqa: E402
|
||||
_assembly_rows,
|
||||
_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]:
|
||||
|
||||
@ -17,7 +17,7 @@ for _import_dir in (SCRIPT_DIR, TEST_DIR, QUALITY_GATE_DIR):
|
||||
if str(_import_dir) not in sys.path:
|
||||
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 writer_rubric import RUBRIC_POLICY_VERSION
|
||||
|
||||
|
||||
@ -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:
|
||||
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 muse_role import FIXED_OPUS_MODEL_ID, FIXED_OPUS_POLICY_ALIAS
|
||||
from writer_eval_preregister import build_balanced_preregistration
|
||||
from writer_gate import (
|
||||
decide_gate,
|
||||
@ -35,7 +35,6 @@ from writer_rubric import (
|
||||
adjudicate_structured_reviews,
|
||||
)
|
||||
|
||||
|
||||
SCENARIOS = (
|
||||
"battle",
|
||||
"character_dialogue",
|
||||
|
||||
@ -10,17 +10,16 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "flow" / "skills" / "book" / "制定作品规划" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from persist_planning import ( # noqa: E402
|
||||
check_field_coverage,
|
||||
write_section,
|
||||
FINE_OUTLINE_OWNER,
|
||||
_load_schema,
|
||||
assert_section_owner,
|
||||
FINE_OUTLINE_OWNER,
|
||||
check_field_coverage,
|
||||
write_section,
|
||||
)
|
||||
|
||||
REQUIRED = ["targetChapter", "sourceRef", "chapterGoal", "hardConstraints", "keyEvents",
|
||||
|
||||
@ -5,7 +5,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "flow" / "skills" / "book" / "制定作品规划" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -5,7 +5,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "flow" / "skills" / "book" / "制定作品规划" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -6,7 +6,6 @@ import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "flow" / "skills" / "book" / "制定作品规划" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -13,10 +13,10 @@ import signal
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
TEST_DIR = pathlib.Path(__file__).resolve().parent
|
||||
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.execute as execute_module # 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 file_cas import CasConflictError, FileCasStore # noqa: E402
|
||||
from gate_input_builder import GateInputBuilder, GateInputBuildError, canonical_sha256 # noqa: E402
|
||||
from muse_role import ( # noqa: E402
|
||||
FIXED_OPUS_MODEL_ID,
|
||||
FIXED_OPUS_POLICY_ALIAS,
|
||||
@ -64,18 +53,29 @@ from muse_role import ( # noqa: E402
|
||||
sha256_json,
|
||||
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 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_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
|
||||
SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA,
|
||||
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_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
|
||||
COMMON_DIMENSIONS,
|
||||
DIMENSIONS,
|
||||
@ -90,7 +90,7 @@ def _utc_stamp(delta: timedelta) -> str:
|
||||
"""相对当前时刻生成 UTC 时间戳。"""
|
||||
|
||||
return (
|
||||
(datetime.now(timezone.utc) + delta)
|
||||
(datetime.now(UTC) + delta)
|
||||
.replace(microsecond=0)
|
||||
.isoformat()
|
||||
.replace("+00:00", "Z")
|
||||
@ -702,7 +702,7 @@ def _test_execution_authorization() -> dict[str, object]:
|
||||
"status": "approved",
|
||||
"authorizationId": "raw-test-adapter-1",
|
||||
"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):
|
||||
item["receiptSha256"] = canonical_sha256(item)
|
||||
@ -1090,7 +1090,7 @@ def _production_config(*, budget_approved: bool = True) -> dict[str, object]:
|
||||
"status": "approved",
|
||||
"authorizationId": "raw-test-1",
|
||||
"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):
|
||||
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):
|
||||
"""固定时钟证明单次 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)
|
||||
|
||||
replay_module._assert_raw_call_window(
|
||||
@ -2459,7 +2459,7 @@ class WriterReplayProductionIntegrationTest(unittest.TestCase):
|
||||
adapters, writer, semantic, judge = _production_adapters(vault_factory=vault_factory)
|
||||
config = _production_config()
|
||||
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(
|
||||
{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)
|
||||
short = _production_config()
|
||||
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(
|
||||
{key: value for key, value in raw.items() if key != "receiptSha256"}
|
||||
)
|
||||
|
||||
@ -45,7 +45,7 @@ class WriterEvalPreregisterTest(unittest.TestCase):
|
||||
expected_order = sorted(
|
||||
self.sample_ids,
|
||||
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(),
|
||||
)
|
||||
self.assertEqual(
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
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:
|
||||
sys.path.insert(0, str(_import_dir))
|
||||
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 writer_gate import issue_gate_report_and_receipt # 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_VERSION = f"raw-file-v1:{SOURCE_HASH}"
|
||||
|
||||
@ -18,7 +18,6 @@ from build_snapshot import ( # noqa: E402
|
||||
remove_terminal_fields,
|
||||
)
|
||||
|
||||
|
||||
META = {
|
||||
"targetChapter": 489,
|
||||
"referenceWork": {"id": "deep-space", "version": "v1"},
|
||||
|
||||
@ -21,7 +21,6 @@ from check_snapshot import ( # noqa: E402
|
||||
check_target_sources,
|
||||
)
|
||||
|
||||
|
||||
AUTH = {
|
||||
"sourceStatus": "active",
|
||||
"copyrightStatus": "research_only",
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import inspect
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
@ -20,7 +20,6 @@ from load_reference_work import ( # noqa: E402
|
||||
project_card,
|
||||
)
|
||||
|
||||
|
||||
FILE_HASH = "02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4"
|
||||
SOURCE_HASH = f"sha256:{FILE_HASH}"
|
||||
SOURCE_VERSION = f"raw-file-v1:{SOURCE_HASH}"
|
||||
|
||||
@ -11,7 +11,6 @@ from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from unittest import mock
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "备份作品抽取结果" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
"""最终交付格式校验器的回归检查;不模拟或约束创作对话。"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse/lifecycle/flow/skills/book/完善故事基础设定/scripts"
|
||||
|
||||
@ -10,7 +10,6 @@ from decimal import Decimal
|
||||
from unittest import mock
|
||||
|
||||
import muse_llm
|
||||
|
||||
from muse_role import (
|
||||
CONTENT_POLICY_ALIAS,
|
||||
CONTENT_POLICY_VERSION,
|
||||
|
||||
@ -15,15 +15,14 @@
|
||||
|
||||
跑法:仓库根目录 `.venv/bin/python tests/skills/抽取作品知识/test_parse_upgrade_offline.py`
|
||||
"""
|
||||
import json
|
||||
import hashlib
|
||||
import inspect
|
||||
from io import StringIO
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
from contextlib import nullcontext
|
||||
from contextlib import redirect_stderr
|
||||
from contextlib import nullcontext, redirect_stderr
|
||||
from copy import deepcopy
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
@ -32,8 +31,8 @@ from click.testing import CliRunner
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "抽取作品知识" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
import upgrade as pu # noqa: E402
|
||||
import muse_embed as embed_drafts
|
||||
import upgrade as pu # noqa: E402
|
||||
|
||||
_passed = 0
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "抽取作品知识" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -6,8 +6,11 @@ import sys
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "extract" / "抽取章节知识" / "scripts"
|
||||
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 = "林深走进舰桥,何岚把黑色钥匙交给他。"
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import tempfile
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "ingest" / "拆解书稿" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -25,7 +25,8 @@ for path in (TEST_DIR, SCRIPT_DIR, CONTINUATION_DIR, READ_CONTEXT_DIR):
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -19,8 +19,8 @@ for path in (SCRIPT_DIR, CONTINUATION_DIR, READ_CONTEXT_DIR, WRITER_TEST_DIR):
|
||||
if str(path) not in sys.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 test_run_writer import _bound_context # noqa: E402
|
||||
from writer_contract import build_candidate_envelope, han_count # noqa: E402
|
||||
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@ import hashlib
|
||||
import pathlib
|
||||
import sys
|
||||
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]
|
||||
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_semantic_model_input,
|
||||
build_semantic_report_json_schema,
|
||||
canonical_sha256,
|
||||
calculate_semantic_metrics,
|
||||
canonical_sha256,
|
||||
is_semantic_schema_specialization,
|
||||
run_writer_semantic_detector,
|
||||
validate_semantic_detector_input,
|
||||
|
||||
@ -13,7 +13,6 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SKILL_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "dispatch" / "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:
|
||||
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
|
||||
OutputInvalidError,
|
||||
TaskSpecError,
|
||||
@ -29,14 +35,6 @@ from role_task import ( # noqa: E402
|
||||
load_spec,
|
||||
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
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ import json
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
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:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import read_tools # noqa: E402
|
||||
from read_tools import TOOL_REGISTRY, execute_tool # noqa: E402
|
||||
|
||||
READ_TOOLS_PY = SKILL_DIR / "read_tools.py"
|
||||
|
||||
@ -8,9 +8,8 @@ import pathlib
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
import muse_embed as embed
|
||||
from click.testing import CliRunner
|
||||
|
||||
# CLI 壳留在 Skill 里、不随包安装,只能按路径加载
|
||||
CLI_PATH = (pathlib.Path(__file__).resolve().parents[3]
|
||||
|
||||
@ -4,7 +4,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "entity" / "skills" / "sovereignty" / "确认知识草稿" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -5,9 +5,8 @@
|
||||
该 schema(字段权威)声明的必填/推荐字段一致,防止两者再次漂移。
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
SKILL = (
|
||||
|
||||
@ -14,26 +14,25 @@ SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humanization" /
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
import yaml
|
||||
|
||||
from capture_cases import (
|
||||
CaseCardError,
|
||||
CAPTURE_CLI_COMMANDS,
|
||||
LIFECYCLE_COMMANDS_FORBIDDEN,
|
||||
PATTERNS,
|
||||
CaseCardError,
|
||||
_parser,
|
||||
annotate_card,
|
||||
build_case_card,
|
||||
build_inventory,
|
||||
build_revalidation_report,
|
||||
capture_feedback,
|
||||
capture_file,
|
||||
build_inventory,
|
||||
build_case_card,
|
||||
confirm_card,
|
||||
project_sample,
|
||||
build_revalidation_report,
|
||||
load_bundle,
|
||||
main,
|
||||
project_sample,
|
||||
revalidate_card,
|
||||
text_sha256,
|
||||
validate_card,
|
||||
main,
|
||||
_parser,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -11,13 +11,11 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -27,11 +27,16 @@ SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记
|
||||
if str(SCRIPT_DIR) not in sys.path:
|
||||
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
|
||||
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]
|
||||
RUN_PREFIX = f"unittest-lesson-{SUFFIX}"
|
||||
|
||||
@ -10,7 +10,7 @@ import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest import mock
|
||||
|
||||
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:
|
||||
"""生成不超过 24 小时的 UTC 保留截止时间。"""
|
||||
|
||||
return (datetime.now(timezone.utc) + timedelta(hours=hours)).isoformat()
|
||||
return (datetime.now(UTC) + timedelta(hours=hours)).isoformat()
|
||||
|
||||
|
||||
class RawVaultTest(unittest.TestCase):
|
||||
@ -231,7 +231,7 @@ class RawVaultTest(unittest.TestCase):
|
||||
def test_create_rejects_insufficient_min_remaining_without_lease_or_vault(self):
|
||||
"""声明的最低剩余租期不足时必须在落 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:
|
||||
self.manager.create_vault(
|
||||
authorization_id="auth-min",
|
||||
@ -267,7 +267,7 @@ class RawVaultTest(unittest.TestCase):
|
||||
def test_write_rejected_after_expiry_but_cleanup_still_works(self):
|
||||
"""租约到期后 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}
|
||||
with mock.patch.object(raw_vault, "_utc_now", side_effect=lambda: clock["now"]):
|
||||
lease = self.manager.create_vault(
|
||||
@ -292,7 +292,7 @@ class RawVaultTest(unittest.TestCase):
|
||||
def test_recover_cleans_expired_open_lease(self):
|
||||
"""恢复扫描必须能收敛已过期的 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}
|
||||
with mock.patch.object(raw_vault, "_utc_now", side_effect=lambda: clock["now"]):
|
||||
lease = self.manager.create_vault(
|
||||
|
||||
@ -4,7 +4,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -4,7 +4,6 @@ import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "记录运行证据" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
@ -5,7 +5,6 @@ import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
DDL_PATH = PROJECT_ROOT / "muse" / "authority" / "db" / "ddl" / "96-example参考作品授权快照.sql"
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ SCRIPTS_DIR = PROJECT_ROOT / "muse" / "authority" / "evidence" / "skills" / "访
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
import click # noqa: E402
|
||||
|
||||
import db # noqa: E402
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""细纲 rubric 的离线回归测试。"""
|
||||
|
||||
import pathlib
|
||||
import inspect
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
@ -9,7 +9,8 @@ import json
|
||||
import pathlib
|
||||
import sys
|
||||
import unittest
|
||||
from typing import Any, Mapping
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "judge" / "评估内容质量" / "scripts"
|
||||
|
||||
@ -15,8 +15,8 @@ if str(SCRIPT_DIR) not in sys.path:
|
||||
from writer_rubric import ( # noqa: E402
|
||||
COMMON_DIMENSIONS,
|
||||
DIMENSIONS,
|
||||
RUBRIC_PROFILE,
|
||||
RUBRIC_POLICY_VERSION,
|
||||
RUBRIC_PROFILE,
|
||||
SCENARIO_DIMENSION,
|
||||
SCENARIO_TYPES,
|
||||
adjudicate_reviews,
|
||||
|
||||
@ -9,7 +9,6 @@ import pathlib
|
||||
import sys
|
||||
import types
|
||||
|
||||
|
||||
import muse_llm as llm
|
||||
|
||||
llm.TOKEN = "test-token"
|
||||
|
||||
@ -8,7 +8,6 @@ SCRIPTS = PROJECT_ROOT / "muse" / "content" / "work" / "skills" / "generate" / "
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
import click # noqa: E402
|
||||
|
||||
from assert_expected_revision import ( # noqa: E402
|
||||
RevisionConflict,
|
||||
assert_expected_revision,
|
||||
|
||||
@ -10,7 +10,6 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from click.testing import CliRunner
|
||||
|
||||
|
||||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||||
SKILLS_DIR = PROJECT_ROOT / ".agent" / "skills"
|
||||
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(EXTRACTION_SCRIPTS))
|
||||
|
||||
import upgrade as parse # noqa: E402
|
||||
import backup_upgrade_work as backup # noqa: E402
|
||||
import reset_upgrade_work as reset # noqa: E402
|
||||
import upgrade as parse # noqa: E402
|
||||
from upgrade_work_lock import UpgradeWorkLockUnavailable # noqa: E402
|
||||
|
||||
|
||||
BACKUP_ID = "11111111-1111-4111-8111-111111111111"
|
||||
CONFIRMATION_SHA = "c" * 64
|
||||
BACKUP_ARGS = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user