W14 受限上下文、工具与探索回放(收口):B01/B02 aiContext 真投影与用途绑定、V0020 冻结快照与实际消费、确定性回放与依赖复检、工作台材料清单与来源证据、收尾补测;整包兜底验证通过后置 verified(离线263/PG204/前端24;NC-ctx 54+前端4;W15 依赖解除)
This commit is contained in:
parent
de4820b595
commit
baf7d431a4
@ -747,6 +747,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/tasks/{task_id}/context-snapshots": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"上下文"
|
||||
],
|
||||
"summary": "列出任务快照",
|
||||
"operationId": "list_context_snapshots",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "task_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"title": "Task Id"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "work_id",
|
||||
"in": "query",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Work Id"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse______http_______________"
|
||||
},
|
||||
"title": "Response List Context Snapshots"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/HTTPValidationError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/schemas/commands": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -6908,6 +6970,71 @@
|
||||
],
|
||||
"title": "会话响应"
|
||||
},
|
||||
"muse______http_______________": {
|
||||
"properties": {
|
||||
"snapshot_id": {
|
||||
"type": "string",
|
||||
"title": "Snapshot Id"
|
||||
},
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"title": "Task Id"
|
||||
},
|
||||
"work_id": {
|
||||
"type": "string",
|
||||
"title": "Work Id"
|
||||
},
|
||||
"input_hash": {
|
||||
"type": "string",
|
||||
"title": "Input Hash"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"title": "State"
|
||||
},
|
||||
"invalidation": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Invalidation"
|
||||
},
|
||||
"changes": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Changes"
|
||||
},
|
||||
"assembly": {
|
||||
"$ref": "#/components/schemas/muse______http___________________2"
|
||||
},
|
||||
"sources": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse______http___________________3"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Sources"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"snapshot_id",
|
||||
"task_id",
|
||||
"work_id",
|
||||
"input_hash",
|
||||
"state",
|
||||
"invalidation",
|
||||
"changes",
|
||||
"assembly",
|
||||
"sources"
|
||||
],
|
||||
"title": "快照响应"
|
||||
},
|
||||
"muse______http__________________1": {
|
||||
"properties": {
|
||||
"step_id": {
|
||||
@ -6973,6 +7100,91 @@
|
||||
],
|
||||
"title": "任务响应"
|
||||
},
|
||||
"muse______http___________________1": {
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"title": "Source"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"title": "Reason"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source",
|
||||
"reason"
|
||||
],
|
||||
"title": "省略条目响应"
|
||||
},
|
||||
"muse______http___________________2": {
|
||||
"properties": {
|
||||
"included_sources": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Included Sources"
|
||||
},
|
||||
"text_hash": {
|
||||
"type": "string",
|
||||
"title": "Text Hash"
|
||||
},
|
||||
"byte_size": {
|
||||
"type": "integer",
|
||||
"title": "Byte Size"
|
||||
},
|
||||
"omissions": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/muse______http___________________1"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Omissions"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"included_sources",
|
||||
"text_hash",
|
||||
"byte_size",
|
||||
"omissions"
|
||||
],
|
||||
"title": "装配清单响应"
|
||||
},
|
||||
"muse______http___________________3": {
|
||||
"properties": {
|
||||
"source_id": {
|
||||
"type": "string",
|
||||
"title": "Source Id"
|
||||
},
|
||||
"data_version": {
|
||||
"type": "string",
|
||||
"title": "Data Version"
|
||||
},
|
||||
"schema_hash": {
|
||||
"type": "string",
|
||||
"title": "Schema Hash"
|
||||
},
|
||||
"projection_version": {
|
||||
"type": "string",
|
||||
"title": "Projection Version"
|
||||
},
|
||||
"current_state": {
|
||||
"type": "string",
|
||||
"title": "Current State"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"source_id",
|
||||
"data_version",
|
||||
"schema_hash",
|
||||
"projection_version",
|
||||
"current_state"
|
||||
],
|
||||
"title": "来源证据响应"
|
||||
},
|
||||
"muse______http____________________1": {
|
||||
"properties": {
|
||||
"command_id": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
# 前端-应用与通用界面:文件合同
|
||||
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 56 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 58 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
|
||||
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
|
||||
|
||||
@ -53,6 +53,7 @@ agent-example/
|
||||
│ └── 表单.tsx # 共用表单布局
|
||||
├── tests/
|
||||
│ ├── 交互/
|
||||
│ │ ├── 上下文材料.test.tsx # 任务材料组件渲染验证
|
||||
│ │ ├── 任务重连.test.tsx # 持久化游标续接
|
||||
│ │ ├── 候选审阅.test.tsx # 被审内容与批准范围
|
||||
│ │ ├── 切作品隔离.test.tsx # 迟到响应不写错作品
|
||||
@ -62,6 +63,7 @@ agent-example/
|
||||
│ ├── 单元/
|
||||
│ │ └── 中文显示与动作.test.tsx # 作者界面语义的逐例行为验证
|
||||
│ ├── 旅程/
|
||||
│ │ ├── 任务材料.spec.ts # 任务材料真实浏览器旅程验证
|
||||
│ │ ├── 创建作品到确认正文.spec.ts # 真后端创作旅程
|
||||
│ │ ├── 动态类型到事实确认.spec.ts # 新类型全链消费
|
||||
│ │ ├── 定稿导出与反馈.spec.ts # 固定稿件交付
|
||||
@ -698,3 +700,27 @@ agent-example/
|
||||
- 目标:中文含义与诊断信息可理解,数量来自数据;点击携带当前对象与审阅版本,不拼旧脚本。
|
||||
- 合同依据:专属合同。
|
||||
- 用例合同:[TC-158ceb91f1fe](../验证设计/工作台与工程用例.md#lc-298a3b05caa7);[TC-72fea2ff83a5](../验证设计/工作台与工程用例.md#lc-bc0603748375);[TC-210481806e32](../验证设计/工作台与工程用例.md#lc-a70ec3c6a681);[TC-b1bc2e61bdb3](../验证设计/工作台与工程用例.md#lc-9ba852d98924);[TC-532f19c48838](../验证设计/工作台与工程用例.md#lc-51915881202b);[TC-f66b7b4c00d2](../验证设计/工作台与工程用例.md#lc-386902e6377a);[TC-39ab793c3a80](../验证设计/工作台与工程用例.md#lc-de75fe53364c);[TC-178a47c1dbea](../验证设计/工作台与工程用例.md#lc-0f9021949197);[TC-d6ac7580979e](../验证设计/工作台与工程用例.md#lc-0ec05b04ee85);[TC-4dff767010c5](../验证设计/工作台与工程用例.md#lc-8f9aad46a4ef);[TC-b70927866815](../验证设计/工作台与工程用例.md#lc-fe5039853267);[TC-5a9dc17fdf74](../验证设计/工作台与工程用例.md#lc-8743149ab18c);[TC-043a74500daf](../验证设计/工作台与工程用例.md#lc-05f3251fa176);[TC-00f820aca9ee](../验证设计/工作台与工程用例.md#lc-9b2594d7f345);[TC-f9bc3a8d21cf](../验证设计/工作台与工程用例.md#lc-37ba23d12b25);[TC-ea9587d92a04](../验证设计/工作台与工程用例.md#lc-3cd785675a60);[TC-8098487cc24d](../验证设计/工作台与工程用例.md#lc-3d48e18a8727);[TC-6d0421af315b](../验证设计/工作台与工程用例.md#lc-96608088521d);[TC-6723f3b292d5](../验证设计/工作台与工程用例.md#lc-336b3f1724d9);[TC-b11d342a8dbc](../验证设计/工作台与工程用例.md#lc-6417e75da94f)。
|
||||
|
||||
<a id="file-5f44d8ea3d67a5a2"></a>
|
||||
|
||||
### `web/tests/交互/上下文材料.test.tsx`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护清单、省略原因与失效标注的渲染语义。
|
||||
- 职责:任务材料组件渲染验证。
|
||||
- 边界:离线组件,传输替身;真实后端旅程由 Playwright 承担。
|
||||
- 目标:渲染破坏时用例失败。
|
||||
- 合同依据:行为验证。
|
||||
- 用例合同:[NC-ui-context-snapshot-list](../验证设计/工作台与工程用例.md#nc-ui-context-snapshot-list);[NC-ui-context-stale-evidence](../验证设计/工作台与工程用例.md#nc-ui-context-stale-evidence);[NC-ui-context-empty-state](../验证设计/工作台与工程用例.md#nc-ui-context-empty-state)。
|
||||
|
||||
<a id="file-25a1cf70babc0097"></a>
|
||||
|
||||
### `web/tests/旅程/任务材料.spec.ts`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护工作台三层内容在真实浏览器可见。
|
||||
- 职责:任务材料真实浏览器旅程验证。
|
||||
- 边界:真实隔离库与真实浏览器;不 mock 后端。
|
||||
- 目标:三层内容可见且无页面错误。
|
||||
- 合同依据:行为验证。
|
||||
- 用例合同:[NC-browser-context-workbench](../验证设计/工作台与工程用例.md#nc-browser-context-workbench)。
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 数据库与迁移:文件合同
|
||||
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 63 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 64 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
|
||||
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
|
||||
|
||||
@ -77,7 +77,8 @@ agent-example/
|
||||
├── V0016__流程定义与版本.sql # 流程定义与版本结构迁移
|
||||
├── V0017__跨模块引用约束.sql # 跨模块引用约束结构迁移
|
||||
├── V0018__工作台查询视图.sql # 工作台查询视图结构迁移
|
||||
└── V0019__用途隔离与数据权限.sql # 用途隔离与数据权限结构迁移
|
||||
├── V0019__用途隔离与数据权限.sql # 用途隔离与数据权限结构迁移
|
||||
└── V0020__上下文快照与消费.sql # 上下文快照与消费记录结构迁移
|
||||
```
|
||||
|
||||
## 逐文件合同
|
||||
@ -811,3 +812,14 @@ agent-example/
|
||||
- 边界:显式管理命令执行,应用启动及迁移不创建角色。
|
||||
- 目标:后续迁移通过维护角色设定权限,生产拒读 oracle,评测不能写业务对象。
|
||||
- 合同依据:[存储与对象关系](../数据模型/存储与对象关系.md)。
|
||||
|
||||
<a id="file-f253b948325616ac"></a>
|
||||
|
||||
### `数据库/迁移/V0020__上下文快照与消费.sql`
|
||||
|
||||
- 归属与种类:数据演进;迁移。
|
||||
- 意图:为 上下文快照与消费记录 提供版本化数据库演进。
|
||||
- 职责:上下文快照与消费结构迁移。
|
||||
- 边界:只执行本版本声明的结构变化;不占用其他工作包预留序号,不以启动自动修表或复用旧序号覆盖历史。
|
||||
- 目标:迁移顺序与校验和可复验,失败不留下假完成版本。
|
||||
- 合同依据:结构迁移。
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 测试与夹具:文件合同
|
||||
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 130 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
本页从[目标文件清单](../目标文件清单.json)投影,共 137 个目标条目。测试用例的具体规格在独立用例清单;文件和目标合同不代表实现或验收已完成。
|
||||
|
||||
[返回目录](目录.md) · [完整项目树](../项目目录与文件职责.md) · [用例重写](../验证设计/测试用例重写.md)
|
||||
|
||||
@ -11,6 +11,9 @@ agent-example/
|
||||
├── tests/
|
||||
│ ├── conftest.py # 默认隔离、测试用途和夹具装配
|
||||
│ ├── 单元/
|
||||
│ │ ├── test_上下文任务范围形状.py # 任务范围新节与用途绑定的离线形状验证
|
||||
│ │ ├── test_上下文回放清单形状.py # 探索清单形状校验验证
|
||||
│ │ ├── test_上下文快照哈希.py # 快照哈希确定性与敏感性验证
|
||||
│ │ ├── test_上下文范围与预算.py # 上下文范围与预算行为验证
|
||||
│ │ ├── test_事实时点与有效性.py # 事实时点与有效性行为验证
|
||||
│ │ ├── test_修订保真与停止.py # 修订保真与停止行为验证
|
||||
@ -114,7 +117,11 @@ agent-example/
|
||||
│ └── 集成/
|
||||
│ ├── test_HTTP任务接入.py # HTTP 与 CLI 任务控制共用隔离库和具名回执
|
||||
│ ├── test_上下文历史正文.py # 受限历史正文读取与材料预算的真实任务验证
|
||||
│ ├── test_上下文回放与复检.py # 确定性回放与依赖复检的真实任务验证
|
||||
│ ├── test_上下文工作台.py # 上下文工作台 HTTP 合同验证
|
||||
│ ├── test_上下文快照与索引写入.py # 上下文快照与索引写入的逐例行为验证
|
||||
│ ├── test_上下文快照消费.py # 上下文快照冻结、幂等与漂移失效的真实任务验证
|
||||
│ ├── test_上下文投影读取.py # 目标章细纲与时点事实 aiContext 真投影的真实任务验证
|
||||
│ ├── test_上游依赖并发变更.py # 上游依赖并发变更行为验证
|
||||
│ ├── test_两阶段写手.py # 两阶段写手旅程的逐例行为验证
|
||||
│ ├── test_事实来源变更.py # 事实来源变更行为验证
|
||||
@ -1617,3 +1624,88 @@ agent-example/
|
||||
- 边界:隔离合成规划,不计模型质量。
|
||||
- 目标:确认追加正式版本且失败不覆盖。
|
||||
- 合同依据:[B01作品规划](../模块设计/B01-作品规划.md)。
|
||||
|
||||
<a id="file-7e3ebd48ed7bf7aa"></a>
|
||||
|
||||
### `tests/单元/test_上下文任务范围形状.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护冻结范围恢复对目标章细纲、事实系统版本与两种用途绑定的失败关闭。
|
||||
- 职责:任务范围新节与用途绑定的离线形状验证。
|
||||
- 边界:离线纯校验;不连数据库、不调用模型,不按字段名存在性冒充语义。
|
||||
- 目标:非法形状、空范围、缺用途绑定或身份清单不一致时恢复明确拒绝。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-scope-restore-full](../验证设计/创作与正式变更用例.md#NC-ctx-scope-restore-full);[NC-ctx-scope-restore-no-documents](../验证设计/创作与正式变更用例.md#NC-ctx-scope-restore-no-documents);[NC-ctx-scope-shape-invalid](../验证设计/创作与正式变更用例.md#NC-ctx-scope-shape-invalid);[NC-ctx-scope-plan-source-missing](../验证设计/创作与正式变更用例.md#NC-ctx-scope-plan-source-missing);[NC-ctx-scope-purpose-binding](../验证设计/创作与正式变更用例.md#NC-ctx-scope-purpose-binding);[NC-ctx-scope-legacy-shape](../验证设计/创作与正式变更用例.md#NC-ctx-scope-legacy-shape)。
|
||||
|
||||
<a id="file-3c5556857afbe108"></a>
|
||||
|
||||
### `tests/集成/test_上下文投影读取.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护按任务用途真实投影、冻结身份漂移失效与视角收窄的端到端行为。
|
||||
- 职责:目标章细纲与时点事实 aiContext 真投影的真实任务验证。
|
||||
- 边界:真实隔离 PostgreSQL 与 S02/B01/B02 公开接口;不调用模型,不用 uiVisible 顶替 aiContext。
|
||||
- 目标:细纲与事实经具名工具读取留痕进入预算;漂移、越界与用途不一致全部明确拒绝。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-projection-s02-trace](../验证设计/创作与正式变更用例.md#NC-ctx-projection-s02-trace);[NC-ctx-plan-projection](../验证设计/创作与正式变更用例.md#NC-ctx-plan-projection);[NC-ctx-facts-view-narrowing](../验证设计/创作与正式变更用例.md#NC-ctx-facts-view-narrowing);[NC-ctx-projection-drift](../验证设计/创作与正式变更用例.md#NC-ctx-projection-drift);[NC-ctx-aicontext-differs](../验证设计/创作与正式变更用例.md#NC-ctx-aicontext-differs);[NC-ctx-plan-target-boundary](../验证设计/创作与正式变更用例.md#NC-ctx-plan-target-boundary);[NC-ctx-purpose-mismatch](../验证设计/创作与正式变更用例.md#NC-ctx-purpose-mismatch)。
|
||||
|
||||
<a id="file-744d9d29107561eb"></a>
|
||||
|
||||
### `tests/单元/test_上下文快照哈希.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护同一载荷同哈希、键序无关与任一输入变化产生新身份。。
|
||||
- 职责:快照哈希确定性与敏感性验证。。
|
||||
- 边界:离线纯函数;不连数据库,不用实现自证哈希。。
|
||||
- 目标:哈希不稳定或身份复用时用例失败。。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-snapshot-hash-stable](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-hash-stable);[NC-ctx-snapshot-hash-sensitive](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-hash-sensitive)。
|
||||
|
||||
|
||||
<a id="file-c7ce9c555ac955df"></a>
|
||||
|
||||
### `tests/集成/test_上下文快照消费.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护冻结-回读-漂移链路与实际消费记录的端到端行为。。
|
||||
- 职责:上下文快照冻结、幂等与漂移失效的真实任务验证。。
|
||||
- 边界:真实隔离 PostgreSQL 与 S02/B09 公开接口;不调用模型,不改写历史快照。。
|
||||
- 目标:重复冻结一行、输入变化新快照、漂移回读明确失效且历史不变。。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-snapshot-lifecycle](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-lifecycle);[NC-ctx-snapshot-idempotent](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-idempotent);[NC-ctx-snapshot-new-input](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-new-input);[NC-ctx-snapshot-drift-invalid](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-drift-invalid);[NC-ctx-snapshot-reauthorize](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-reauthorize);[NC-ctx-snapshot-binding-denied](../验证设计/创作与正式变更用例.md#nc-ctx-snapshot-binding-denied)。
|
||||
|
||||
<a id="file-76e63ed0eae5b335"></a>
|
||||
|
||||
### `tests/单元/test_上下文回放清单形状.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护清单顺序、身份、类型与来源绑定的失败关闭。
|
||||
- 职责:探索清单形状校验验证。
|
||||
- 边界:离线纯函数;不连数据库。
|
||||
- 目标:非法清单在回放前被拒绝。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-replay-manifest-order](../验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-order);[NC-ctx-replay-manifest-duplicate](../验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-duplicate);[NC-ctx-replay-manifest-types](../验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-types);[NC-ctx-replay-manifest-binding](../验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-binding);[NC-ctx-replay-manifest-valid](../验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-valid)。
|
||||
|
||||
<a id="file-b9978d8a841fa217"></a>
|
||||
|
||||
### `tests/集成/test_上下文回放与复检.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护回放确定性、漂移失效与恢复检查阻断的端到端行为。
|
||||
- 职责:确定性回放与依赖复检的真实任务验证。
|
||||
- 边界:真实隔离 PostgreSQL 与 S02/B09 公开接口;回放不调模型不留新痕。
|
||||
- 目标:回放与首读逐项一致;漂移明确失效并指明项;依赖重验真实阻断。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-replay-deterministic](../验证设计/创作与正式变更用例.md#nc-ctx-replay-deterministic);[NC-ctx-replay-source-drift](../验证设计/创作与正式变更用例.md#nc-ctx-replay-source-drift);[NC-ctx-replay-out-of-scope](../验证设计/创作与正式变更用例.md#nc-ctx-replay-out-of-scope);[NC-ctx-deps-recheck-blocks](../验证设计/创作与正式变更用例.md#nc-ctx-deps-recheck-blocks);[NC-ctx-deps-lock-status](../验证设计/创作与正式变更用例.md#nc-ctx-deps-lock-status)。
|
||||
|
||||
<a id="file-74d961c624f0bcc3"></a>
|
||||
|
||||
### `tests/集成/test_上下文工作台.py`
|
||||
|
||||
- 归属与种类:验证;测试。
|
||||
- 意图:保护材料清单、来源证据与失效说明的只读合同。
|
||||
- 职责:上下文工作台 HTTP 合同验证。
|
||||
- 边界:真实隔离 PostgreSQL 与 TestClient;只读不写正式内容。
|
||||
- 目标:越权与不存在任务拒绝;漂移失效可见且历史原样。
|
||||
- 合同依据:[B09上下文](../模块设计/B09-上下文.md)。
|
||||
- 用例合同:[NC-ctx-workbench-author-view](../验证设计/创作与正式变更用例.md#nc-ctx-workbench-author-view);[NC-ctx-workbench-denied](../验证设计/创作与正式变更用例.md#nc-ctx-workbench-denied);[NC-ctx-workbench-drift-visible](../验证设计/创作与正式变更用例.md#nc-ctx-workbench-drift-visible)。
|
||||
|
||||
@ -15266,6 +15266,11 @@
|
||||
"design_location": "文件设计/测试与夹具.md#file-2eccf8c851b20c29",
|
||||
"legacy_sources": [],
|
||||
"test_case_refs": [
|
||||
"NC-ctx-replay-consumption-hash",
|
||||
"NC-ctx-replay-drift-compare",
|
||||
"NC-ctx-replay-ordered-tools",
|
||||
"NC-ctx-replay-scope-args",
|
||||
"NC-ctx-replay-unknown-tool",
|
||||
"TC-09f3bc25ef5f",
|
||||
"TC-0a2301cbed1d",
|
||||
"TC-1fca9b6bf74f",
|
||||
@ -21283,6 +21288,205 @@
|
||||
"NC-plan-stale-review-rejection",
|
||||
"TC-e1f9cb0ac406"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "tests/单元/test_上下文任务范围形状.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "任务范围新节与用途绑定的离线形状验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护冻结范围恢复对目标章细纲、事实系统版本与两种用途绑定的失败关闭",
|
||||
"boundary": "离线纯校验;不连数据库、不调用模型,不按字段名存在性冒充语义",
|
||||
"goal": "非法形状、空范围、缺用途绑定或身份清单不一致时恢复明确拒绝",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-7e3ebd48ed7bf7aa",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-scope-legacy-shape",
|
||||
"NC-ctx-scope-plan-source-missing",
|
||||
"NC-ctx-scope-purpose-binding",
|
||||
"NC-ctx-scope-restore-full",
|
||||
"NC-ctx-scope-restore-no-documents",
|
||||
"NC-ctx-scope-shape-invalid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "tests/集成/test_上下文投影读取.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "目标章细纲与时点事实 aiContext 真投影的真实任务验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护按任务用途真实投影、冻结身份漂移失效与视角收窄的端到端行为",
|
||||
"boundary": "真实隔离 PostgreSQL 与 S02/B01/B02 公开接口;不调用模型,不用 uiVisible 顶替 aiContext",
|
||||
"goal": "细纲与事实经具名工具读取留痕进入预算;漂移、越界与用途不一致全部明确拒绝",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-3c5556857afbe108",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-aicontext-differs",
|
||||
"NC-ctx-facts-view-narrowing",
|
||||
"NC-ctx-plan-projection",
|
||||
"NC-ctx-plan-target-boundary",
|
||||
"NC-ctx-projection-drift",
|
||||
"NC-ctx-projection-s02-trace",
|
||||
"NC-ctx-purpose-mismatch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "tests/单元/test_上下文快照哈希.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "快照哈希确定性与敏感性验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护同一载荷同哈希、键序无关与任一输入变化产生新身份",
|
||||
"boundary": "离线纯函数;不连数据库,不用实现自证哈希",
|
||||
"goal": "哈希不稳定或身份复用时用例失败",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-744d9d29107561eb",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-snapshot-hash-sensitive",
|
||||
"NC-ctx-snapshot-hash-stable"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "tests/集成/test_上下文快照消费.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "上下文快照冻结、幂等与漂移失效的真实任务验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护冻结-回读-漂移链路与实际消费记录的端到端行为",
|
||||
"boundary": "真实隔离 PostgreSQL 与 S02/B09 公开接口;不调用模型,不改写历史快照",
|
||||
"goal": "重复冻结一行、输入变化新快照、漂移回读明确失效且历史不变",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-c7ce9c555ac955df",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-snapshot-binding-denied",
|
||||
"NC-ctx-snapshot-conflict-row",
|
||||
"NC-ctx-snapshot-drift-invalid",
|
||||
"NC-ctx-snapshot-idempotent",
|
||||
"NC-ctx-snapshot-lifecycle",
|
||||
"NC-ctx-snapshot-new-input",
|
||||
"NC-ctx-snapshot-reauthorize"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "数据库/迁移/V0020__上下文快照与消费.sql",
|
||||
"owner": "数据演进",
|
||||
"kind": "迁移",
|
||||
"responsibility": "上下文快照与消费记录结构迁移",
|
||||
"project_part": "后端",
|
||||
"intent": "为不可变上下文快照与实际消费记录提供版本化演进",
|
||||
"boundary": "只执行本版本声明的结构变化;不占用其他工作包预留序号",
|
||||
"goal": "迁移顺序与校验和可复验,失败不留下假完成版本",
|
||||
"contract_basis": "结构迁移",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/数据库与迁移.md#file-f253b948325616ac",
|
||||
"test_case_refs": []
|
||||
},
|
||||
{
|
||||
"path": "tests/单元/test_上下文回放清单形状.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "探索清单形状校验验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护清单顺序、身份、类型与来源绑定的失败关闭",
|
||||
"boundary": "离线纯函数;不连数据库",
|
||||
"goal": "非法清单在回放前被拒绝",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-76e63ed0eae5b335",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-replay-manifest-binding",
|
||||
"NC-ctx-replay-manifest-duplicate",
|
||||
"NC-ctx-replay-manifest-order",
|
||||
"NC-ctx-replay-manifest-types",
|
||||
"NC-ctx-replay-manifest-valid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "tests/集成/test_上下文回放与复检.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "确定性回放与依赖复检的真实任务验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护回放确定性、漂移失效与恢复检查阻断的端到端行为",
|
||||
"boundary": "真实隔离 PostgreSQL 与 S02/B09 公开接口;回放不调模型不留新痕",
|
||||
"goal": "回放与首读逐项一致;漂移明确失效并指明项;依赖重验真实阻断",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-b9978d8a841fa217",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-deps-lock-status",
|
||||
"NC-ctx-deps-recheck-blocks",
|
||||
"NC-ctx-replay-auth-denied",
|
||||
"NC-ctx-replay-deterministic",
|
||||
"NC-ctx-replay-out-of-scope",
|
||||
"NC-ctx-replay-source-drift"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "tests/集成/test_上下文工作台.py",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "上下文工作台 HTTP 合同验证",
|
||||
"project_part": "测试",
|
||||
"intent": "保护材料清单、来源证据与失效说明的只读合同",
|
||||
"boundary": "真实隔离 PostgreSQL 与 TestClient;只读不写正式内容",
|
||||
"goal": "越权与不存在任务拒绝;漂移失效可见且历史原样",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/测试与夹具.md#file-74d961c624f0bcc3",
|
||||
"test_case_refs": [
|
||||
"NC-ctx-workbench-author-view",
|
||||
"NC-ctx-workbench-denied",
|
||||
"NC-ctx-workbench-drift-visible"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "web/tests/交互/上下文材料.test.tsx",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "任务材料组件渲染验证",
|
||||
"project_part": "前端",
|
||||
"intent": "保护清单、省略原因与失效标注的渲染语义",
|
||||
"boundary": "离线组件,传输替身;真实后端旅程由 Playwright 承担",
|
||||
"goal": "渲染破坏时用例失败",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/前端-应用与通用界面.md#file-5f44d8ea3d67a5a2",
|
||||
"test_case_refs": [
|
||||
"NC-ui-context-empty-state",
|
||||
"NC-ui-context-snapshot-list",
|
||||
"NC-ui-context-stale-evidence"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "web/tests/旅程/任务材料.spec.ts",
|
||||
"owner": "验证",
|
||||
"kind": "测试",
|
||||
"responsibility": "任务材料真实浏览器旅程验证",
|
||||
"project_part": "前端",
|
||||
"intent": "保护工作台三层内容在真实浏览器可见",
|
||||
"boundary": "真实隔离库与真实浏览器;不 mock 后端",
|
||||
"goal": "三层内容可见且无页面错误",
|
||||
"contract_basis": "行为验证",
|
||||
"status": "目标合同;不表示已实现或已验证",
|
||||
"legacy_sources": [],
|
||||
"design_location": "文件设计/前端-应用与通用界面.md#file-25a1cf70babc0097",
|
||||
"test_case_refs": [
|
||||
"NC-browser-context-workbench"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contract_fields": {
|
||||
|
||||
@ -1040,6 +1040,9 @@ agent-example/
|
||||
├── tests/
|
||||
│ ├── conftest.py # 默认隔离、测试用途和夹具装配
|
||||
│ ├── 单元/
|
||||
│ │ ├── test_上下文任务范围形状.py # 任务范围新节与用途绑定的离线形状验证
|
||||
│ │ ├── test_上下文回放清单形状.py # 探索清单形状校验验证
|
||||
│ │ ├── test_上下文快照哈希.py # 快照哈希确定性与敏感性验证
|
||||
│ │ ├── test_上下文范围与预算.py # 上下文范围与预算行为验证
|
||||
│ │ ├── test_事实时点与有效性.py # 事实时点与有效性行为验证
|
||||
│ │ ├── test_修订保真与停止.py # 修订保真与停止行为验证
|
||||
@ -1143,7 +1146,11 @@ agent-example/
|
||||
│ └── 集成/
|
||||
│ ├── test_HTTP任务接入.py # HTTP 与 CLI 任务控制共用隔离库和具名回执
|
||||
│ ├── test_上下文历史正文.py # 受限历史正文读取与材料预算的真实任务验证
|
||||
│ ├── test_上下文回放与复检.py # 确定性回放与依赖复检的真实任务验证
|
||||
│ ├── test_上下文工作台.py # 上下文工作台 HTTP 合同验证
|
||||
│ ├── test_上下文快照与索引写入.py # 上下文快照与索引写入的逐例行为验证
|
||||
│ ├── test_上下文快照消费.py # 上下文快照冻结、幂等与漂移失效的真实任务验证
|
||||
│ ├── test_上下文投影读取.py # 目标章细纲与时点事实 aiContext 真投影的真实任务验证
|
||||
│ ├── test_上游依赖并发变更.py # 上游依赖并发变更行为验证
|
||||
│ ├── test_两阶段写手.py # 两阶段写手旅程的逐例行为验证
|
||||
│ ├── test_事实来源变更.py # 事实来源变更行为验证
|
||||
@ -1331,6 +1338,7 @@ agent-example/
|
||||
│ │ └── 表单.tsx # 共用表单布局
|
||||
│ ├── tests/
|
||||
│ │ ├── 交互/
|
||||
│ │ │ ├── 上下文材料.test.tsx # 任务材料组件渲染验证
|
||||
│ │ │ ├── 任务重连.test.tsx # 持久化游标续接
|
||||
│ │ │ ├── 候选审阅.test.tsx # 被审内容与批准范围
|
||||
│ │ │ ├── 切作品隔离.test.tsx # 迟到响应不写错作品
|
||||
@ -1342,6 +1350,7 @@ agent-example/
|
||||
│ │ │ └── 中文显示与动作.test.tsx # 作者界面语义的逐例行为验证
|
||||
│ │ ├── 旅程/
|
||||
│ │ │ ├── 人工创作.spec.ts # 人工创作HTTP与作者编辑独立用例
|
||||
│ │ │ ├── 任务材料.spec.ts # 任务材料真实浏览器旅程验证
|
||||
│ │ │ ├── 候选审阅.spec.ts # 作者候选审阅和正式采纳浏览器旅程
|
||||
│ │ │ ├── 创建作品到确认正文.spec.ts # 真后端创作旅程
|
||||
│ │ │ ├── 动态类型到事实确认.spec.ts # 新类型全链消费
|
||||
@ -1436,7 +1445,8 @@ agent-example/
|
||||
│ ├── V0016__流程定义与版本.sql # 流程定义与版本结构迁移
|
||||
│ ├── V0017__跨模块引用约束.sql # 跨模块引用约束结构迁移
|
||||
│ ├── V0018__工作台查询视图.sql # 工作台查询视图结构迁移
|
||||
│ └── V0019__用途隔离与数据权限.sql # 用途隔离与数据权限结构迁移
|
||||
│ ├── V0019__用途隔离与数据权限.sql # 用途隔离与数据权限结构迁移
|
||||
│ └── V0020__上下文快照与消费.sql # 上下文快照与消费记录结构迁移
|
||||
├── 部署/
|
||||
│ ├── Dockerfile # 前端构建和后端发布镜像
|
||||
│ ├── compose.test.yaml # 隔离 PostgreSQL 和测试环境
|
||||
|
||||
@ -4468,3 +4468,501 @@
|
||||
- 参数身份:unknown-tool、work-id、missing-cutoff、wrong-author、missing-version。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-scope-restore-full"></a>
|
||||
|
||||
## NC-ctx-scope-restore-full · test_完整任务范围原样恢复__d1a2f3
|
||||
|
||||
- 位置:`tests/单元/test_上下文任务范围形状.py::test_完整任务范围原样恢复__d1a2f3`。
|
||||
- 给定:含历史文稿、目标章细纲、事实系统版本与两种用途绑定的冻结任务范围。
|
||||
- 动作:从任务快照恢复冻结范围。
|
||||
- 预期:范围形状、来源清单与用途绑定全部通过时原样返回。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-scope-restore-no-documents"></a>
|
||||
|
||||
## NC-ctx-scope-restore-no-documents · test_无历史文稿但绑定细纲与事实仍可恢复__e4b5c6
|
||||
|
||||
- 位置:`tests/单元/test_上下文任务范围形状.py::test_无历史文稿但绑定细纲与事实仍可恢复__e4b5c6`。
|
||||
- 给定:没有历史文稿但冻结了目标章细纲与事实系统版本。
|
||||
- 动作:从任务快照恢复冻结范围。
|
||||
- 预期:documents 为空不单独拒绝;空文稿且无细纲无事实才失败。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-scope-shape-invalid"></a>
|
||||
|
||||
## NC-ctx-scope-shape-invalid · test_非法细纲事实形状或空范围拒绝__f7a8b9
|
||||
|
||||
- 位置:`tests/单元/test_上下文任务范围形状.py::test_非法细纲事实形状或空范围拒绝__f7a8b9`。
|
||||
- 给定:细纲版本为零或布尔、章节为空串、事实系统版本为负或布尔或缺键、完全空范围。
|
||||
- 动作:从任务快照恢复冻结范围。
|
||||
- 预期:一律 CONTEXT_SCOPE_INVALID 失败关闭,不猜测补默认值。
|
||||
- 环境:离线纯函数。
|
||||
- 参数身份:zero-plan-revision、boolean-plan-revision、empty-chapter、negative-system-revision、boolean-system-revision、missing-system-revision、empty-scope。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-scope-plan-source-missing"></a>
|
||||
|
||||
## NC-ctx-scope-plan-source-missing · test_细纲身份不在来源清单拒绝__a3c4d5
|
||||
|
||||
- 位置:`tests/单元/test_上下文任务范围形状.py::test_细纲身份不在来源清单拒绝__a3c4d5`。
|
||||
- 给定:target_plan 的 plan_id 不在冻结 source_ids 中。
|
||||
- 动作:从任务快照恢复冻结范围。
|
||||
- 预期:CONTEXT_SCOPE_INVALID;来源清单必须覆盖文稿与细纲身份。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-scope-purpose-binding"></a>
|
||||
|
||||
## NC-ctx-scope-purpose-binding · test_新节必须绑定与任务一致的用途__b6c7d8
|
||||
|
||||
- 位置:`tests/单元/test_上下文任务范围形状.py::test_新节必须绑定与任务一致的用途__b6c7d8`。
|
||||
- 给定:target_plan 或 facts 存在但缺运行用途、或内容用途与任务不一致。
|
||||
- 动作:从任务快照恢复冻结范围。
|
||||
- 预期:CONTEXT_SCOPE_INVALID;新节必须绑定与任务一致的两种用途。
|
||||
- 环境:离线纯函数。
|
||||
- 参数身份:missing-run-purpose、content-mismatch、content-unbound。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-scope-legacy-shape"></a>
|
||||
|
||||
## NC-ctx-scope-legacy-shape · test_仅历史文稿的范围不要求用途键__c9d0e1
|
||||
|
||||
- 位置:`tests/单元/test_上下文任务范围形状.py::test_仅历史文稿的范围不要求用途键__c9d0e1`。
|
||||
- 给定:仅含历史文稿的旧形状范围(无用途键与新节)。
|
||||
- 动作:从任务快照恢复冻结范围。
|
||||
- 预期:原样返回;用途绑定只对细纲与事实新节强制。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-projection-s02-trace"></a>
|
||||
|
||||
## NC-ctx-projection-s02-trace · test_细纲与事实经真实任务读取留痕并进入预算__b09201
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_细纲与事实经真实任务读取留痕并进入预算__b09201`。
|
||||
- 给定:已确认目标章细纲、多条时点事实与历史正文的真实作品。
|
||||
- 动作:经 S02 任务执行装配处理器依次调用三种具名工具并装配材料。
|
||||
- 预期:三种工具读取均留工具读取事件与来源引用;细纲/事实/历史全部进入材料预算入选清单。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-plan-projection"></a>
|
||||
|
||||
## NC-ctx-plan-projection · test_目标细纲按任务用途返回真投影__b09202
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_目标细纲按任务用途返回真投影__b09202`。
|
||||
- 给定:任务冻结范围为 generation 用途。
|
||||
- 动作:调用 query_work_plan 工具。
|
||||
- 预期:返回细纲 aiContext 投影内容、剔除说明与两种用途;来源版本、结构哈希与投影版本完整。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-facts-view-narrowing"></a>
|
||||
|
||||
## NC-ctx-facts-view-narrowing · test_时点事实按冻结系统版本与视角收窄返回__b09203
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_时点事实按冻结系统版本与视角收窄返回__b09203`。
|
||||
- 给定:冻结事实系统版本;一条事实的正文依据位于截止之后;一个不存在的知情主体。
|
||||
- 动作:分别以作者、读者与角色视角调用 query_facts_as_of。
|
||||
- 预期:作者视角含全部事实;读者视角省略未来依据并给出 FUTURE_EVIDENCE;未知知情主体 SCOPE_DENIED。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-projection-drift"></a>
|
||||
|
||||
## NC-ctx-projection-drift · test_细纲或事实漂移后拒绝沿用原任务__b09204
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_细纲或事实漂移后拒绝沿用原任务__b09204`。
|
||||
- 给定:任务范围已冻结细纲身份与事实系统版本。
|
||||
- 动作:重新确认细纲新版本或确认新事实后再次调用工具。
|
||||
- 预期:SOURCE_STALE;漂移后不得沿用原任务范围。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 参数身份:plan-revised、facts-advanced。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-aicontext-differs"></a>
|
||||
|
||||
## NC-ctx-aicontext-differs · test_aiContext投影与uiVisible差异真实生效__b09205
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_aiContext投影与uiVisible差异真实生效__b09205`。
|
||||
- 给定:一个字段只声明 uiVisible、另一个字段两种用途都可见的动态类型事实。
|
||||
- 动作:同一对象分别经工作台查询与 query_facts_as_of 读取。
|
||||
- 预期:工作台可见秘密备注;工具投影剔除该字段并给出 FIELD_NOT_ALLOWED 剔除原因。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-plan-target-boundary"></a>
|
||||
|
||||
## NC-ctx-plan-target-boundary · test_目标章不在截止之后紧邻或无细纲拒绝授权__b09206
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_目标章不在截止之后紧邻或无细纲拒绝授权__b09206`。
|
||||
- 给定:历史章、截止位置章、跨作品章或无已确认细纲的紧邻章。
|
||||
- 动作:以目标章授权任务范围。
|
||||
- 预期:前三种 SCOPE_DENIED(含实际所属作品);无细纲 PRECONDITION_PENDING。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 参数身份:past-chapter、cutoff-chapter、cross-work、unconfirmed-outline。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-purpose-mismatch"></a>
|
||||
|
||||
## NC-ctx-purpose-mismatch · test_任务用途与冻结范围不一致拒绝__b09207
|
||||
|
||||
- 位置:`tests/集成/test_上下文投影读取.py::test_任务用途与冻结范围不一致拒绝__b09207`。
|
||||
- 给定:范围按 generation 授权,任务以 planning 内容用途创建。
|
||||
- 动作:领取只读工具装配。
|
||||
- 预期:CONTEXT_SCOPE_INVALID;任务用途与冻结范围用途不一致即失败关闭。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-hash-stable"></a>
|
||||
|
||||
## NC-ctx-snapshot-hash-stable · test_同载荷同哈希且键序无关__b09301
|
||||
|
||||
- 位置:`tests/单元/test_上下文快照哈希.py::test_同载荷同哈希且键序无关__b09301`。
|
||||
- 给定:同一冻结载荷以不同键序构造。
|
||||
- 动作:派生快照输入哈希与身份。
|
||||
- 预期:同;载;荷;同;哈;希;同;身;份;;;键;序;差;异;不;改;变;结;果。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-hash-sensitive"></a>
|
||||
|
||||
## NC-ctx-snapshot-hash-sensitive · test_任一输入变化产生新哈希与新身份__b09302
|
||||
|
||||
- 位置:`tests/单元/test_上下文快照哈希.py::test_任一输入变化产生新哈希与新身份__b09302`。
|
||||
- 给定:范围、装配入选、省略、来源版本或文本任一变化的载荷。
|
||||
- 动作:派生快照输入哈希与身份。
|
||||
- 预期:任;一;变;化;产;生;新;哈;希;与;新;身;份;,;不;复;用;旧;身;份。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-lifecycle"></a>
|
||||
|
||||
## NC-ctx-snapshot-lifecycle · test_完整链路冻结回读一致且任务可追溯__b09311
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_完整链路冻结回读一致且任务可追溯__b09311`。
|
||||
- 给定:授权任务范围并完成工具读取与材料装配的真实任务。
|
||||
- 动作:服务端冻结上下文并按任务回读快照。
|
||||
- 预期:回;读;内;容;与;冻;结;一;致;;;快;照;与;任;务;、;来;源;版;本;绑;定;可;追;溯。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-idempotent"></a>
|
||||
|
||||
## NC-ctx-snapshot-idempotent · test_重复冻结同一输入幂等为一行__b09312
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_重复冻结同一输入幂等为一行__b09312`。
|
||||
- 给定:同一任务同一输入的两次冻结。
|
||||
- 动作:重复调用冻结。
|
||||
- 预期:同;一;快;照;身;份;与;哈;希;,;表;内;仅;一;行;,;不;改;写。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-new-input"></a>
|
||||
|
||||
## NC-ctx-snapshot-new-input · test_装配输入变化产生新快照且不改写旧快照__b09313
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_装配输入变化产生新快照且不改写旧快照__b09313`。
|
||||
- 给定:装配输入(入选或省略)变化后再次冻结。
|
||||
- 动作:冻结并回读新旧快照。
|
||||
- 预期:产;生;新;快;照;新;身;份;;;旧;快;照;字;段;原;样;保;留。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-drift-invalid"></a>
|
||||
|
||||
## NC-ctx-snapshot-drift-invalid · test_来源漂移后回读明确失效且历史不改写__b09314
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_来源漂移后回读明确失效且历史不改写__b09314`。
|
||||
- 给定:快照冻结后细纲重确认、正文重存或事实系统版本前进。
|
||||
- 动作:回读快照。
|
||||
- 预期:当;前;状;态;标;注; ;S;O;U;R;C;E;_;S;T;A;L;E; ;与;失;效;说;明;;;历;史;快;照;字;段;不;被;改;写。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 参数身份:plan-revised、document-revised、facts-advanced。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-reauthorize"></a>
|
||||
|
||||
## NC-ctx-snapshot-reauthorize · test_来源版本变化经重新授权产生新快照__b09315
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_来源版本变化经重新授权产生新快照__b09315`。
|
||||
- 给定:来源版本变化后按新版本重新授权任务范围。
|
||||
- 动作:再次冻结。
|
||||
- 预期:经;重;新;授;权;形;成;新;快;照;;;不;静;默;续;用;旧;输;入。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-binding-denied"></a>
|
||||
|
||||
## NC-ctx-snapshot-binding-denied · test_绑定越出冻结范围拒绝冻结__b09316
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_绑定越出冻结范围拒绝冻结__b09316`。
|
||||
- 给定:携带冻结范围以外来源绑定的冻结请求。
|
||||
- 动作:调用冻结。
|
||||
- 预期:明;确;拒;绝;,;不;落;部;分;快;照。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-manifest-order"></a>
|
||||
|
||||
## NC-ctx-replay-manifest-order · test_顺序断裂拒绝__b7e0b1
|
||||
|
||||
- 位置:`tests/单元/test_上下文回放清单形状.py::test_顺序断裂拒绝__b7e0b1`。
|
||||
- 给定:顺序不连续的探索清单。
|
||||
- 动作:校验清单。
|
||||
- 预期:C;O;N;T;E;X;T;_;I;N;P;U;T;_;I;N;V;A;L;I;D; ;拒;绝;,;顺;序;必;须;连;续;可;核;对。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-manifest-duplicate"></a>
|
||||
|
||||
## NC-ctx-replay-manifest-duplicate · test_重复调用ID拒绝__b7e0b2
|
||||
|
||||
- 位置:`tests/单元/test_上下文回放清单形状.py::test_重复调用ID拒绝__b7e0b2`。
|
||||
- 给定:调用ID重复的探索清单。
|
||||
- 动作:校验清单。
|
||||
- 预期:明;确;拒;绝;,;不;猜;测;去;重。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-manifest-types"></a>
|
||||
|
||||
## NC-ctx-replay-manifest-types · test_布尔与类型混淆拒绝__b7e0b3
|
||||
|
||||
- 位置:`tests/单元/test_上下文回放清单形状.py::test_布尔与类型混淆拒绝__b7e0b3`。
|
||||
- 给定:顺序/版本等字段为布尔或类型混淆的清单。
|
||||
- 动作:校验清单。
|
||||
- 预期:明;确;拒;绝;,;不;用;宽;松;类;型;顶;替。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-manifest-binding"></a>
|
||||
|
||||
## NC-ctx-replay-manifest-binding · test_缺来源绑定与非法消费哈希拒绝__b7e0b4
|
||||
|
||||
- 位置:`tests/单元/test_上下文回放清单形状.py::test_缺来源绑定与非法消费哈希拒绝__b7e0b4`。
|
||||
- 给定:缺来源绑定或消费哈希非法的条目。
|
||||
- 动作:校验清单。
|
||||
- 预期:分;别;明;确;拒;绝。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-manifest-valid"></a>
|
||||
|
||||
## NC-ctx-replay-manifest-valid · test_合法清单按序通过且空清单拒绝__b7e0b5
|
||||
|
||||
- 位置:`tests/单元/test_上下文回放清单形状.py::test_合法清单按序通过且空清单拒绝__b7e0b5`。
|
||||
- 给定:合法清单与空清单。
|
||||
- 动作:校验清单。
|
||||
- 预期:合;法;按;序;通;过;;;空;清;单;拒;绝;(;探;索;必;须;非;空;)。
|
||||
- 环境:离线纯函数。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-unknown-tool"></a>
|
||||
|
||||
## NC-ctx-replay-unknown-tool · test_未知工具与未授权工具分别明确拒绝__b7e0a1
|
||||
|
||||
- 位置:`tests/契约/test_探索回放合同.py::test_未知工具与未授权工具分别明确拒绝__b7e0a1`。
|
||||
- 给定:未知工具名或范围未授权的工具。
|
||||
- 动作:校验并回放。
|
||||
- 预期:未;知;工;具; ;C;O;N;T;E;X;T;_;I;N;P;U;T;_;I;N;V;A;L;I;D;;;未;授;权; ;S;C;O;P;E;_;D;E;N;I;E;D。
|
||||
- 环境:离线契约。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-scope-args"></a>
|
||||
|
||||
## NC-ctx-replay-scope-args · test_参数越出冻结范围与扩权字段拒绝__b7e0a2
|
||||
|
||||
- 位置:`tests/契约/test_探索回放合同.py::test_参数越出冻结范围与扩权字段拒绝__b7e0a2`。
|
||||
- 给定:指定未冻结文稿版本或携带扩权字段的参数。
|
||||
- 动作:校验并回放。
|
||||
- 预期:S;C;O;P;E;_;D;E;N;I;E;D;/;C;O;N;T;E;X;T;_;I;N;P;U;T;_;I;N;V;A;L;I;D; ;拒;绝;,;不;静;默;收;窄。
|
||||
- 环境:离线契约。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-drift-compare"></a>
|
||||
|
||||
## NC-ctx-replay-drift-compare · test_回放比对指明版本哈希投影与内容漂移__b7e0a3
|
||||
|
||||
- 位置:`tests/契约/test_探索回放合同.py::test_回放比对指明版本哈希投影与内容漂移__b7e0a3`。
|
||||
- 给定:版本、哈希、投影版本或内容消费哈希任一漂移的条目。
|
||||
- 动作:按清单回放。
|
||||
- 预期:S;O;U;R;C;E;_;S;T;A;L;E; ;且;指;明;调;用;与;来;源。
|
||||
- 环境:离线契约。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-consumption-hash"></a>
|
||||
|
||||
## NC-ctx-replay-consumption-hash · test_记录探索消费哈希与来源绑定同键__b7e0a4
|
||||
|
||||
- 位置:`tests/契约/test_探索回放合同.py::test_记录探索消费哈希与来源绑定同键__b7e0a4`。
|
||||
- 给定:真实工具读取结果。
|
||||
- 动作:记录探索条目。
|
||||
- 预期:消;费;哈;希;与;来;源;绑;定;与; ;S;0;2; ;事;件; ;s;o;u;r;c;e;_;r;e;f;s; ;同;键;可;核;对。
|
||||
- 环境:离线契约。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-ordered-tools"></a>
|
||||
|
||||
## NC-ctx-replay-ordered-tools · test_三类工具清单按序通过且顺序可核对__b7e0a5
|
||||
|
||||
- 位置:`tests/契约/test_探索回放合同.py::test_三类工具清单按序通过且顺序可核对__b7e0a5`。
|
||||
- 给定:覆盖三类具名工具的合法清单。
|
||||
- 动作:校验并回放。
|
||||
- 预期:按;序;通;过;,;顺;序;可;核;对。
|
||||
- 环境:离线契约。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-deterministic"></a>
|
||||
|
||||
## NC-ctx-replay-deterministic · test_回放与首读逐项一致且不再留痕__b7e0c1
|
||||
|
||||
- 位置:`tests/集成/test_上下文回放与复检.py::test_回放与首读逐项一致且不再留痕__b7e0c1`。
|
||||
- 给定:已完成探索并冻结的真实任务。
|
||||
- 动作:按清单确定性回放。
|
||||
- 预期:回;放;与;首;读;逐;项;一;致;(;含;中;文;码;点;片;段;);;;回;放;不;追;加;工;具;读;取;留;痕。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-source-drift"></a>
|
||||
|
||||
## NC-ctx-replay-source-drift · test_来源漂移后回放明确失效并指明项__b7e0c2
|
||||
|
||||
- 位置:`tests/集成/test_上下文回放与复检.py::test_来源漂移后回放明确失效并指明项__b7e0c2`。
|
||||
- 给定:回放前正文重存、细纲重确认或事实系统版本前进。
|
||||
- 动作:按清单回放。
|
||||
- 预期:S;O;U;R;C;E;_;S;T;A;L;E; ;且;指;明;漂;移;调;用;与;来;源。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 参数身份:document-revised、plan-reconfirmed、facts-advanced。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-out-of-scope"></a>
|
||||
|
||||
## NC-ctx-replay-out-of-scope · test_越界参数清单回放拒绝__b7e0c3
|
||||
|
||||
- 位置:`tests/集成/test_上下文回放与复检.py::test_越界参数清单回放拒绝__b7e0c3`。
|
||||
- 给定:越界参数的清单。
|
||||
- 动作:经真实任务回放。
|
||||
- 预期:明;确;拒;绝;,;不;返;回;部;分;材;料。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-deps-recheck-blocks"></a>
|
||||
|
||||
## NC-ctx-deps-recheck-blocks · test_依赖重验检查漂移后阻断任务重验__b7e0c4
|
||||
|
||||
- 位置:`tests/集成/test_上下文回放与复检.py::test_依赖重验检查漂移后阻断任务重验__b7e0c4`。
|
||||
- 给定:注册真实依赖重验检查的任务在来源漂移后。
|
||||
- 动作:重验执行范围。
|
||||
- 预期:抛;错;阻;断;,;不;静;默;续;用;旧;输;入。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-deps-lock-status"></a>
|
||||
|
||||
## NC-ctx-deps-lock-status · test_核对依赖输出锁定依据与变化项__b7e0c5
|
||||
|
||||
- 位置:`tests/集成/test_上下文回放与复检.py::test_核对依赖输出锁定依据与变化项__b7e0c5`。
|
||||
- 给定:有效与漂移两种冻结范围。
|
||||
- 动作:核对依赖。
|
||||
- 预期:有;效;输;出;逐;来;源;当;前;版;本;锁;定;依;据;;;漂;移;输;出;具;体;变;化;项。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-workbench-author-view"></a>
|
||||
|
||||
## NC-ctx-workbench-author-view · test_作者可见任务材料清单与来源证据__dd023f
|
||||
|
||||
- 位置:`tests/集成/test_上下文工作台.py::test_作者可见任务材料清单与来源证据__dd023f`。
|
||||
- 给定:作者经会话查询已有快照任务。
|
||||
- 动作:GET /api/v1/tasks/{id}/context-snapshots。
|
||||
- 预期:未登录 401;作者可见快照的材料清单(入选/字节数/省略原因)与逐来源版本、哈希、投影版本。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-workbench-denied"></a>
|
||||
|
||||
## NC-ctx-workbench-denied · test_他人任务与不存在任务被拒绝__5f6302
|
||||
|
||||
- 位置:`tests/集成/test_上下文工作台.py::test_他人任务与不存在任务被拒绝__5f6302`。
|
||||
- 给定:他人作者会话或不存在的任务。
|
||||
- 动作:GET /api/v1/tasks/{id}/context-snapshots。
|
||||
- 预期:他人任务 403 SCOPE_DENIED;不存在任务 422;不泄露内容。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-workbench-drift-visible"></a>
|
||||
|
||||
## NC-ctx-workbench-drift-visible · test_来源漂移后失效说明可见且历史装配原样__18a1af
|
||||
|
||||
- 位置:`tests/集成/test_上下文工作台.py::test_来源漂移后失效说明可见且历史装配原样__18a1af`。
|
||||
- 给定:快照冻结后来源漂移。
|
||||
- 动作:再次 GET 上下文快照。
|
||||
- 预期:同一 snapshot_id 与 input_hash;失效说明与具体变化项可见;历史装配与来源绑定原样。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-replay-auth-denied"></a>
|
||||
|
||||
## NC-ctx-replay-auth-denied · test_回放授权重验失败拒绝回放__b7e0c6
|
||||
|
||||
- 位置:`tests/集成/test_上下文回放与复检.py::test_回放授权重验失败拒绝回放__b7e0c6`。
|
||||
- 给定:任务冻结范围的授权/身份不一致(恢复冻结范围失败)。
|
||||
- 动作:调用回放探索。
|
||||
- 预期:SCOPE_DENIED 拒绝回放,不沿用已撤回或不一致的授权。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ctx-snapshot-conflict-row"></a>
|
||||
|
||||
## NC-ctx-snapshot-conflict-row · test_存储级同身份不同输入哈希冲突拒绝且不改写原行__b09317
|
||||
|
||||
- 位置:`tests/集成/test_上下文快照消费.py::test_存储级同身份不同输入哈希冲突拒绝且不改写原行__b09317`。
|
||||
- 给定:存储级直插与既有 snapshot_id 相同但 input_hash/载荷不同的行。
|
||||
- 动作:再次以同身份冻结/读取。
|
||||
- 预期:CONTEXT_SNAPSHOT_CONFLICT 拒绝;原快照行不被改写。
|
||||
- 环境:真实隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
@ -3138,3 +3138,51 @@
|
||||
- 环境:真实Chrome与隔离PostgreSQL。
|
||||
- 合同:[权威定义](../模块设计/B01-作品规划.md)。
|
||||
|
||||
|
||||
<a id="nc-ui-context-snapshot-list"></a>
|
||||
|
||||
## NC-ui-context-snapshot-list · NC-ui-context-snapshot-list:材料清单与省略原因渲染
|
||||
|
||||
- 位置:`web/tests/交互/上下文材料.test.tsx::NC-ui-context-snapshot-list:材料清单与省略原因渲染`。
|
||||
- 给定:含省略条目的有效快照。
|
||||
- 动作:渲染任务上下文组件。
|
||||
- 预期:材料清单、字节数与省略原因渲染;取数走 /tasks/{id}/context-snapshots。
|
||||
- 环境:离线组件,传输替身。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ui-context-stale-evidence"></a>
|
||||
|
||||
## NC-ui-context-stale-evidence · NC-ui-context-stale-evidence:来源漂移后失效标注与具体变化可见
|
||||
|
||||
- 位置:`web/tests/交互/上下文材料.test.tsx::NC-ui-context-stale-evidence:来源漂移后失效标注与具体变化可见`。
|
||||
- 给定:漂移后的快照。
|
||||
- 动作:渲染任务上下文组件。
|
||||
- 预期:失效标注与具体变化项可见;来源证据仍显示版本与哈希。
|
||||
- 环境:离线组件,传输替身。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-ui-context-empty-state"></a>
|
||||
|
||||
## NC-ui-context-empty-state · NC-ui-context-empty-state:无快照任务如实显示空态
|
||||
|
||||
- 位置:`web/tests/交互/上下文材料.test.tsx::NC-ui-context-empty-state:无快照任务如实显示空态`。
|
||||
- 给定:没有快照的任务。
|
||||
- 动作:渲染任务上下文组件。
|
||||
- 预期:如实显示空态,不伪造材料。
|
||||
- 环境:离线组件,传输替身。
|
||||
- 合同:[权威定义](../模块设计/B09-上下文.md)。
|
||||
|
||||
|
||||
<a id="nc-browser-context-workbench"></a>
|
||||
|
||||
## NC-browser-context-workbench · NC-browser-context-workbench:任务材料清单、来源证据与失效说明
|
||||
|
||||
- 位置:`web/tests/旅程/任务材料.spec.ts::NC-browser-context-workbench:任务材料清单、来源证据与失效说明`。
|
||||
- 给定:真实隔离库上的工作台与一处真实漂移。
|
||||
- 动作:真实浏览器登录并打开任务详情。
|
||||
- 预期:材料清单/来源证据/失效说明可见(DOM 断言 24 条,页面与控制台错误 0);截图视觉判定四项清晰可读。
|
||||
- 环境:真实浏览器。
|
||||
- 合同:[权威定义](../接口契约/任务工具与事件.md)。
|
||||
|
||||
|
||||
@ -121800,6 +121800,847 @@
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-history-fail-closed",
|
||||
"reason": "W14 受限历史正文读取与材料预算切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-scope-restore-full",
|
||||
"file": "tests/单元/test_上下文任务范围形状.py",
|
||||
"symbol": "test_完整任务范围原样恢复__d1a2f3",
|
||||
"given": "含历史文稿、目标章细纲、事实系统版本与两种用途绑定的冻结任务范围",
|
||||
"when": "从任务快照恢复冻结范围",
|
||||
"then": [
|
||||
"范围形状、来源清单与用途绑定全部通过时原样返回"
|
||||
],
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证离线.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-scope-restore-full",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-scope-restore-no-documents",
|
||||
"file": "tests/单元/test_上下文任务范围形状.py",
|
||||
"symbol": "test_无历史文稿但绑定细纲与事实仍可恢复__e4b5c6",
|
||||
"given": "没有历史文稿但冻结了目标章细纲与事实系统版本",
|
||||
"when": "从任务快照恢复冻结范围",
|
||||
"then": [
|
||||
"documents 为空不单独拒绝;空文稿且无细纲无事实才失败"
|
||||
],
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证离线.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-scope-restore-no-documents",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-scope-shape-invalid",
|
||||
"file": "tests/单元/test_上下文任务范围形状.py",
|
||||
"symbol": "test_非法细纲事实形状或空范围拒绝__f7a8b9",
|
||||
"given": "细纲版本为零或布尔、章节为空串、事实系统版本为负或布尔或缺键、完全空范围",
|
||||
"when": "从任务快照恢复冻结范围",
|
||||
"then": [
|
||||
"一律 CONTEXT_SCOPE_INVALID 失败关闭,不猜测补默认值"
|
||||
],
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证离线.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-scope-shape-invalid",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"parameter_ids": [
|
||||
"zero-plan-revision",
|
||||
"boolean-plan-revision",
|
||||
"empty-chapter",
|
||||
"negative-system-revision",
|
||||
"boolean-system-revision",
|
||||
"missing-system-revision",
|
||||
"empty-scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-scope-plan-source-missing",
|
||||
"file": "tests/单元/test_上下文任务范围形状.py",
|
||||
"symbol": "test_细纲身份不在来源清单拒绝__a3c4d5",
|
||||
"given": "target_plan 的 plan_id 不在冻结 source_ids 中",
|
||||
"when": "从任务快照恢复冻结范围",
|
||||
"then": [
|
||||
"CONTEXT_SCOPE_INVALID;来源清单必须覆盖文稿与细纲身份"
|
||||
],
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证离线.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-scope-plan-source-missing",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-scope-purpose-binding",
|
||||
"file": "tests/单元/test_上下文任务范围形状.py",
|
||||
"symbol": "test_新节必须绑定与任务一致的用途__b6c7d8",
|
||||
"given": "target_plan 或 facts 存在但缺运行用途、或内容用途与任务不一致",
|
||||
"when": "从任务快照恢复冻结范围",
|
||||
"then": [
|
||||
"CONTEXT_SCOPE_INVALID;新节必须绑定与任务一致的两种用途"
|
||||
],
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证离线.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-scope-purpose-binding",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"parameter_ids": [
|
||||
"missing-run-purpose",
|
||||
"content-mismatch",
|
||||
"content-unbound"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-scope-legacy-shape",
|
||||
"file": "tests/单元/test_上下文任务范围形状.py",
|
||||
"symbol": "test_仅历史文稿的范围不要求用途键__c9d0e1",
|
||||
"given": "仅含历史文稿的旧形状范围(无用途键与新节)",
|
||||
"when": "从任务快照恢复冻结范围",
|
||||
"then": [
|
||||
"原样返回;用途绑定只对细纲与事实新节强制"
|
||||
],
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证离线.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-scope-legacy-shape",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-projection-s02-trace",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_细纲与事实经真实任务读取留痕并进入预算__b09201",
|
||||
"given": "已确认目标章细纲、多条时点事实与历史正文的真实作品",
|
||||
"when": "经 S02 任务执行装配处理器依次调用三种具名工具并装配材料",
|
||||
"then": [
|
||||
"三种工具读取均留工具读取事件与来源引用;细纲/事实/历史全部进入材料预算入选清单"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-projection-s02-trace",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-plan-projection",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_目标细纲按任务用途返回真投影__b09202",
|
||||
"given": "任务冻结范围为 generation 用途",
|
||||
"when": "调用 query_work_plan 工具",
|
||||
"then": [
|
||||
"返回细纲 aiContext 投影内容、剔除说明与两种用途;来源版本、结构哈希与投影版本完整"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-plan-projection",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-facts-view-narrowing",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_时点事实按冻结系统版本与视角收窄返回__b09203",
|
||||
"given": "冻结事实系统版本;一条事实的正文依据位于截止之后;一个不存在的知情主体",
|
||||
"when": "分别以作者、读者与角色视角调用 query_facts_as_of",
|
||||
"then": [
|
||||
"作者视角含全部事实;读者视角省略未来依据并给出 FUTURE_EVIDENCE;未知知情主体 SCOPE_DENIED"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-facts-view-narrowing",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-projection-drift",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_细纲或事实漂移后拒绝沿用原任务__b09204",
|
||||
"given": "任务范围已冻结细纲身份与事实系统版本",
|
||||
"when": "重新确认细纲新版本或确认新事实后再次调用工具",
|
||||
"then": [
|
||||
"SOURCE_STALE;漂移后不得沿用原任务范围"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-projection-drift",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"parameter_ids": [
|
||||
"plan-revised",
|
||||
"facts-advanced"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-aicontext-differs",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_aiContext投影与uiVisible差异真实生效__b09205",
|
||||
"given": "一个字段只声明 uiVisible、另一个字段两种用途都可见的动态类型事实",
|
||||
"when": "同一对象分别经工作台查询与 query_facts_as_of 读取",
|
||||
"then": [
|
||||
"工作台可见秘密备注;工具投影剔除该字段并给出 FIELD_NOT_ALLOWED 剔除原因"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-aicontext-differs",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-plan-target-boundary",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_目标章不在截止之后紧邻或无细纲拒绝授权__b09206",
|
||||
"given": "历史章、截止位置章、跨作品章或无已确认细纲的紧邻章",
|
||||
"when": "以目标章授权任务范围",
|
||||
"then": [
|
||||
"前三种 SCOPE_DENIED(含实际所属作品);无细纲 PRECONDITION_PENDING"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-plan-target-boundary",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"parameter_ids": [
|
||||
"past-chapter",
|
||||
"cutoff-chapter",
|
||||
"cross-work",
|
||||
"unconfirmed-outline"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-purpose-mismatch",
|
||||
"file": "tests/集成/test_上下文投影读取.py",
|
||||
"symbol": "test_任务用途与冻结范围不一致拒绝__b09207",
|
||||
"given": "范围按 generation 授权,任务以 planning 内容用途创建",
|
||||
"when": "领取只读工具装配",
|
||||
"then": [
|
||||
"CONTEXT_SCOPE_INVALID;任务用途与冻结范围用途不一致即失败关闭"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/aiContext投影/投影读取PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/aiContext投影/验证数据库.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-purpose-mismatch",
|
||||
"reason": "W14 目标章细纲与时点事实 aiContext 真投影切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-hash-stable",
|
||||
"file": "tests/单元/test_上下文快照哈希.py",
|
||||
"symbol": "test_同载荷同哈希且键序无关__b09301",
|
||||
"given": "同一冻结载荷以不同键序构造",
|
||||
"when": "派生快照输入哈希与身份",
|
||||
"then": "同载荷同哈希同身份;键序差异不改变结果",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照哈希单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证离线-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-hash-stable",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-hash-sensitive",
|
||||
"file": "tests/单元/test_上下文快照哈希.py",
|
||||
"symbol": "test_任一输入变化产生新哈希与新身份__b09302",
|
||||
"given": "范围、装配入选、省略、来源版本或文本任一变化的载荷",
|
||||
"when": "派生快照输入哈希与身份",
|
||||
"then": "任一变化产生新哈希与新身份,不复用旧身份",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照哈希单元.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证离线-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-hash-sensitive",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-lifecycle",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_完整链路冻结回读一致且任务可追溯__b09311",
|
||||
"given": "授权任务范围并完成工具读取与材料装配的真实任务",
|
||||
"when": "服务端冻结上下文并按任务回读快照",
|
||||
"then": "回读内容与冻结一致;快照与任务、来源版本绑定可追溯",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照消费PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证数据库-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-lifecycle",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-idempotent",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_重复冻结同一输入幂等为一行__b09312",
|
||||
"given": "同一任务同一输入的两次冻结",
|
||||
"when": "重复调用冻结",
|
||||
"then": "同一快照身份与哈希,表内仅一行,不改写",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照消费PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证数据库-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-idempotent",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-new-input",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_装配输入变化产生新快照且不改写旧快照__b09313",
|
||||
"given": "装配输入(入选或省略)变化后再次冻结",
|
||||
"when": "冻结并回读新旧快照",
|
||||
"then": "产生新快照新身份;旧快照字段原样保留",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照消费PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证数据库-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-new-input",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-drift-invalid",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_来源漂移后回读明确失效且历史不改写__b09314",
|
||||
"given": "快照冻结后细纲重确认、正文重存或事实系统版本前进",
|
||||
"when": "回读快照",
|
||||
"then": "当前状态标注 SOURCE_STALE 与失效说明;历史快照字段不被改写",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照消费PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证数据库-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-drift-invalid",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"parameter_ids": [
|
||||
"plan-revised",
|
||||
"document-revised",
|
||||
"facts-advanced"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-reauthorize",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_来源版本变化经重新授权产生新快照__b09315",
|
||||
"given": "来源版本变化后按新版本重新授权任务范围",
|
||||
"when": "再次冻结",
|
||||
"then": "经重新授权形成新快照;不静默续用旧输入",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照消费PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证数据库-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-reauthorize",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-binding-denied",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_绑定越出冻结范围拒绝冻结__b09316",
|
||||
"given": "携带冻结范围以外来源绑定的冻结请求",
|
||||
"when": "调用冻结",
|
||||
"then": "明确拒绝,不落部分快照",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/快照与消费/快照消费PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/快照与消费/验证数据库-复核.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-binding-denied",
|
||||
"reason": "W14 冻结快照与实际消费切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-manifest-order",
|
||||
"file": "tests/单元/test_上下文回放清单形状.py",
|
||||
"symbol": "test_顺序断裂拒绝__b7e0b1",
|
||||
"given": "顺序不连续的探索清单",
|
||||
"when": "校验清单",
|
||||
"then": "CONTEXT_INPUT_INVALID 拒绝,顺序必须连续可核对",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放离线新用例.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证离线.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-order",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-manifest-duplicate",
|
||||
"file": "tests/单元/test_上下文回放清单形状.py",
|
||||
"symbol": "test_重复调用ID拒绝__b7e0b2",
|
||||
"given": "调用ID重复的探索清单",
|
||||
"when": "校验清单",
|
||||
"then": "明确拒绝,不猜测去重",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放离线新用例.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证离线.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-duplicate",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-manifest-types",
|
||||
"file": "tests/单元/test_上下文回放清单形状.py",
|
||||
"symbol": "test_布尔与类型混淆拒绝__b7e0b3",
|
||||
"given": "顺序/版本等字段为布尔或类型混淆的清单",
|
||||
"when": "校验清单",
|
||||
"then": "明确拒绝,不用宽松类型顶替",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放离线新用例.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证离线.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-types",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-manifest-binding",
|
||||
"file": "tests/单元/test_上下文回放清单形状.py",
|
||||
"symbol": "test_缺来源绑定与非法消费哈希拒绝__b7e0b4",
|
||||
"given": "缺来源绑定或消费哈希非法的条目",
|
||||
"when": "校验清单",
|
||||
"then": "分别明确拒绝",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放离线新用例.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证离线.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-binding",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-manifest-valid",
|
||||
"file": "tests/单元/test_上下文回放清单形状.py",
|
||||
"symbol": "test_合法清单按序通过且空清单拒绝__b7e0b5",
|
||||
"given": "合法清单与空清单",
|
||||
"when": "校验清单",
|
||||
"then": "合法按序通过;空清单拒绝(探索必须非空)",
|
||||
"environment": "离线纯函数",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放离线新用例.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证离线.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-manifest-valid",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-unknown-tool",
|
||||
"file": "tests/契约/test_探索回放合同.py",
|
||||
"symbol": "test_未知工具与未授权工具分别明确拒绝__b7e0a1",
|
||||
"given": "未知工具名或范围未授权的工具",
|
||||
"when": "校验并回放",
|
||||
"then": "未知工具 CONTEXT_INPUT_INVALID;未授权 SCOPE_DENIED",
|
||||
"environment": "离线契约",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-unknown-tool",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-scope-args",
|
||||
"file": "tests/契约/test_探索回放合同.py",
|
||||
"symbol": "test_参数越出冻结范围与扩权字段拒绝__b7e0a2",
|
||||
"given": "指定未冻结文稿版本或携带扩权字段的参数",
|
||||
"when": "校验并回放",
|
||||
"then": "SCOPE_DENIED/CONTEXT_INPUT_INVALID 拒绝,不静默收窄",
|
||||
"environment": "离线契约",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-scope-args",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-drift-compare",
|
||||
"file": "tests/契约/test_探索回放合同.py",
|
||||
"symbol": "test_回放比对指明版本哈希投影与内容漂移__b7e0a3",
|
||||
"given": "版本、哈希、投影版本或内容消费哈希任一漂移的条目",
|
||||
"when": "按清单回放",
|
||||
"then": "SOURCE_STALE 且指明调用与来源",
|
||||
"environment": "离线契约",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-drift-compare",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-consumption-hash",
|
||||
"file": "tests/契约/test_探索回放合同.py",
|
||||
"symbol": "test_记录探索消费哈希与来源绑定同键__b7e0a4",
|
||||
"given": "真实工具读取结果",
|
||||
"when": "记录探索条目",
|
||||
"then": "消费哈希与来源绑定与 S02 事件 source_refs 同键可核对",
|
||||
"environment": "离线契约",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-consumption-hash",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-ordered-tools",
|
||||
"file": "tests/契约/test_探索回放合同.py",
|
||||
"symbol": "test_三类工具清单按序通过且顺序可核对__b7e0a5",
|
||||
"given": "覆盖三类具名工具的合法清单",
|
||||
"when": "校验并回放",
|
||||
"then": "按序通过,顺序可核对",
|
||||
"environment": "离线契约",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-ordered-tools",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-deterministic",
|
||||
"file": "tests/集成/test_上下文回放与复检.py",
|
||||
"symbol": "test_回放与首读逐项一致且不再留痕__b7e0c1",
|
||||
"given": "已完成探索并冻结的真实任务",
|
||||
"when": "按清单确定性回放",
|
||||
"then": "回放与首读逐项一致(含中文码点片段);回放不追加工具读取留痕",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-deterministic",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-source-drift",
|
||||
"file": "tests/集成/test_上下文回放与复检.py",
|
||||
"symbol": "test_来源漂移后回放明确失效并指明项__b7e0c2",
|
||||
"given": "回放前正文重存、细纲重确认或事实系统版本前进",
|
||||
"when": "按清单回放",
|
||||
"then": "SOURCE_STALE 且指明漂移调用与来源",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-source-drift",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"parameter_ids": [
|
||||
"document-revised",
|
||||
"plan-reconfirmed",
|
||||
"facts-advanced"
|
||||
]
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-out-of-scope",
|
||||
"file": "tests/集成/test_上下文回放与复检.py",
|
||||
"symbol": "test_越界参数清单回放拒绝__b7e0c3",
|
||||
"given": "越界参数的清单",
|
||||
"when": "经真实任务回放",
|
||||
"then": "明确拒绝,不返回部分材料",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-out-of-scope",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-deps-recheck-blocks",
|
||||
"file": "tests/集成/test_上下文回放与复检.py",
|
||||
"symbol": "test_依赖重验检查漂移后阻断任务重验__b7e0c4",
|
||||
"given": "注册真实依赖重验检查的任务在来源漂移后",
|
||||
"when": "重验执行范围",
|
||||
"then": "抛错阻断,不静默续用旧输入",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-deps-recheck-blocks",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-deps-lock-status",
|
||||
"file": "tests/集成/test_上下文回放与复检.py",
|
||||
"symbol": "test_核对依赖输出锁定依据与变化项__b7e0c5",
|
||||
"given": "有效与漂移两种冻结范围",
|
||||
"when": "核对依赖",
|
||||
"then": "有效输出逐来源当前版本锁定依据;漂移输出具体变化项",
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/回放与复检/回放与复检PG.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合验证/回放与复检/验证数据库.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-deps-lock-status",
|
||||
"reason": "W14 确定性回放与依赖复检切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-workbench-author-view",
|
||||
"file": "tests/集成/test_上下文工作台.py",
|
||||
"symbol": "test_作者可见任务材料清单与来源证据__dd023f",
|
||||
"given": "作者经会话查询已有快照任务",
|
||||
"when": "GET /api/v1/tasks/{id}/context-snapshots",
|
||||
"then": [
|
||||
"未登录 401;作者可见快照的材料清单(入选/字节数/省略原因)与逐来源版本、哈希、投影版本"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/上下文工作台PG.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-workbench-author-view",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-workbench-denied",
|
||||
"file": "tests/集成/test_上下文工作台.py",
|
||||
"symbol": "test_他人任务与不存在任务被拒绝__5f6302",
|
||||
"given": "他人作者会话或不存在的任务",
|
||||
"when": "GET /api/v1/tasks/{id}/context-snapshots",
|
||||
"then": [
|
||||
"他人任务 403 SCOPE_DENIED;不存在任务 422;不泄露内容"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/上下文工作台PG.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-workbench-denied",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-workbench-drift-visible",
|
||||
"file": "tests/集成/test_上下文工作台.py",
|
||||
"symbol": "test_来源漂移后失效说明可见且历史装配原样__18a1af",
|
||||
"given": "快照冻结后来源漂移",
|
||||
"when": "再次 GET 上下文快照",
|
||||
"then": [
|
||||
"同一 snapshot_id 与 input_hash;失效说明与具体变化项可见;历史装配与来源绑定原样"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/上下文工作台PG.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-workbench-drift-visible",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ui-context-snapshot-list",
|
||||
"file": "web/tests/交互/上下文材料.test.tsx",
|
||||
"symbol": "NC-ui-context-snapshot-list:材料清单与省略原因渲染",
|
||||
"given": "含省略条目的有效快照",
|
||||
"when": "渲染任务上下文组件",
|
||||
"then": [
|
||||
"材料清单、字节数与省略原因渲染;取数走 /tasks/{id}/context-snapshots"
|
||||
],
|
||||
"environment": "离线组件,传输替身",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/前端测试.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/工作台与工程用例.md#nc-ui-context-snapshot-list",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"code_sha256": "3c95c02ea0fa2efd395e35219a3ff879e2c70eb837de14a4953ef518f8e37442"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ui-context-stale-evidence",
|
||||
"file": "web/tests/交互/上下文材料.test.tsx",
|
||||
"symbol": "NC-ui-context-stale-evidence:来源漂移后失效标注与具体变化可见",
|
||||
"given": "漂移后的快照",
|
||||
"when": "渲染任务上下文组件",
|
||||
"then": [
|
||||
"失效标注与具体变化项可见;来源证据仍显示版本与哈希"
|
||||
],
|
||||
"environment": "离线组件,传输替身",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/前端测试.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/工作台与工程用例.md#nc-ui-context-stale-evidence",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"code_sha256": "3c95c02ea0fa2efd395e35219a3ff879e2c70eb837de14a4953ef518f8e37442"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ui-context-empty-state",
|
||||
"file": "web/tests/交互/上下文材料.test.tsx",
|
||||
"symbol": "NC-ui-context-empty-state:无快照任务如实显示空态",
|
||||
"given": "没有快照的任务",
|
||||
"when": "渲染任务上下文组件",
|
||||
"then": [
|
||||
"如实显示空态,不伪造材料"
|
||||
],
|
||||
"environment": "离线组件,传输替身",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/前端测试.log"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/工作台与工程用例.md#nc-ui-context-empty-state",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"code_sha256": "3c95c02ea0fa2efd395e35219a3ff879e2c70eb837de14a4953ef518f8e37442"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-browser-context-workbench",
|
||||
"file": "web/tests/旅程/任务材料.spec.ts",
|
||||
"symbol": "NC-browser-context-workbench:任务材料清单、来源证据与失效说明",
|
||||
"given": "真实隔离库上的工作台与一处真实漂移",
|
||||
"when": "真实浏览器登录并打开任务详情",
|
||||
"then": [
|
||||
"材料清单/来源证据/失效说明可见(DOM 断言 24 条,页面与控制台错误 0);截图视觉判定四项清晰可读"
|
||||
],
|
||||
"environment": "真实浏览器",
|
||||
"contract": "接口契约/任务工具与事件.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/任务材料浏览器.json",
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/工作台/视觉判定-任务材料与失效说明.md"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/工作台与工程用例.md#nc-browser-context-workbench",
|
||||
"reason": "W14 工作台材料清单/来源证据/失效说明切片的新版行为用例;按功能价值新写,不承接已退役旧用例身份。",
|
||||
"code_sha256": "adfea414ea636f7335675574533827ca815d6ad7262720461978656b65af2569"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-replay-auth-denied",
|
||||
"file": "tests/集成/test_上下文回放与复检.py",
|
||||
"symbol": "test_回放授权重验失败拒绝回放__b7e0c6",
|
||||
"given": "任务冻结范围的授权/身份不一致(恢复冻结范围失败)",
|
||||
"when": "调用回放探索",
|
||||
"then": [
|
||||
"SCOPE_DENIED 拒绝回放,不沿用已撤回或不一致的授权"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/W14收尾/补测绿证.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/W14收尾/数据库全量.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-replay-auth-denied",
|
||||
"reason": "W14 收尾补测:回合3/回合2 记录的覆盖缺口按功能价值补齐,不承接已退役旧用例身份。"
|
||||
},
|
||||
{
|
||||
"case_id": "NC-ctx-snapshot-conflict-row",
|
||||
"file": "tests/集成/test_上下文快照消费.py",
|
||||
"symbol": "test_存储级同身份不同输入哈希冲突拒绝且不改写原行__b09317",
|
||||
"given": "存储级直插与既有 snapshot_id 相同但 input_hash/载荷不同的行",
|
||||
"when": "再次以同身份冻结/读取",
|
||||
"then": [
|
||||
"CONTEXT_SNAPSHOT_CONFLICT 拒绝;原快照行不被改写"
|
||||
],
|
||||
"environment": "真实隔离PostgreSQL",
|
||||
"contract": "模块设计/B09-上下文.md",
|
||||
"execution_evidence": [
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/W14收尾/补测绿证.xml",
|
||||
".agents.local/改造/R2-20260909/W14/回合开发/W14收尾/数据库全量.xml"
|
||||
],
|
||||
"verification_state": "通过",
|
||||
"design_location": "验证设计/创作与正式变更用例.md#nc-ctx-snapshot-conflict-row",
|
||||
"reason": "W14 收尾补测:回合3/回合2 记录的覆盖缺口按功能价值补齐,不承接已退役旧用例身份。"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
"""服务端授权并恢复历史正文读取范围;执行阶段只从已保存任务取回。
|
||||
"""服务端授权并恢复任务读取范围;执行阶段只从已保存任务取回。
|
||||
|
||||
合同依据:模块设计/B09-上下文.md、接口契约/任务工具与事件.md。
|
||||
|
||||
- 授权只在服务端发生:作者、作品、叙事截止与文稿清单由作者动作传入,
|
||||
- 授权只在服务端发生:作者、作品、叙事截止、目标章与事实版本由作者动作传入,
|
||||
模型侧工具不接受其中任何一项。
|
||||
- 范围随任务冻结;恢复时校验形状与任务身份一致,缺失截止不是无限,
|
||||
- 范围随任务冻结;恢复时校验形状、身份与两种用途一致,缺失截止不是无限,
|
||||
也不静默改读最新版本。
|
||||
- 正文与目录只经 B01/B05 公开接口读取;本文件不写跨模块 SQL。
|
||||
- 正文、细纲与事实只经 B01/B02/B05 公开接口读取;本文件不写跨模块 SQL。
|
||||
- 目标章细纲的时间语义与历史正文相反:历史正文位于截止之前,
|
||||
目标章必须紧随截止之后,两者不能互换。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -15,11 +17,37 @@ from typing import Any
|
||||
|
||||
from psycopg import Connection
|
||||
|
||||
from muse.上下文.模型 import 上下文错误
|
||||
from muse.上下文.冻结快照 import (
|
||||
快照从行,
|
||||
快照身份,
|
||||
快照载荷,
|
||||
绑定载荷,
|
||||
装配记录,
|
||||
输入哈希,
|
||||
)
|
||||
from muse.上下文.存储 import 上下文存储
|
||||
from muse.上下文.探索回放 import 回放, 工具范围从冻结
|
||||
from muse.上下文.来源读取 import 范围漂移说明
|
||||
from muse.上下文.模型 import (
|
||||
上下文快照,
|
||||
上下文错误,
|
||||
冻结回执,
|
||||
回放结果,
|
||||
探索条目,
|
||||
材料装配结果,
|
||||
来源绑定,
|
||||
)
|
||||
from muse.任务运行.接口 import 任务快照, 任务服务
|
||||
from muse.作品规划.接口 import 作品错误, 读取作品范围, 读取章节归属
|
||||
from muse.作品规划.接口 import (
|
||||
作品错误,
|
||||
读取作品范围,
|
||||
读取章节归属,
|
||||
读取细纲投影,
|
||||
)
|
||||
from muse.共享.调用身份 import 调用身份
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
from muse.故事世界.接口 import 读取时点事实投影
|
||||
from muse.故事世界.模型 import 事实错误
|
||||
from muse.正文写作.接口 import 读取正文依据
|
||||
from muse.正文写作.模型 import 正文错误
|
||||
|
||||
@ -41,42 +69,213 @@ def _拒绝(错误: 作品错误, 前因: str) -> 上下文错误:
|
||||
|
||||
|
||||
class 上下文服务:
|
||||
"""授权历史范围并从已保存任务恢复冻结范围;读取范围失败即关闭。"""
|
||||
"""授权任务范围并从已保存任务恢复冻结范围;读取范围失败即关闭。"""
|
||||
|
||||
def __init__(self, 数据库: 数据库工厂, 任务: 任务服务) -> None:
|
||||
def __init__(self, 数据库: 数据库工厂, 任务: 任务服务, 规划引用解析=None) -> None:
|
||||
self.数据库 = 数据库
|
||||
self.任务 = 任务
|
||||
self.规划引用解析 = 规划引用解析
|
||||
|
||||
def 授权历史范围(
|
||||
self, 身份: 调用身份, 作品ID: str, 截止: int, 文稿查询: tuple[dict, ...]
|
||||
) -> dict:
|
||||
"""按作者、作品与叙事截止固定可读文稿、目录版本与正文哈希;只读不写入。"""
|
||||
if not 作品ID:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "历史范围必须绑定明确作品")
|
||||
if 身份.作者 is None or 身份.用途 is not self.数据库.用途:
|
||||
raise 上下文错误("SCOPE_DENIED", "历史范围授权需要与装配用途一致的作者身份")
|
||||
校验截止(截止)
|
||||
"""仅授权历史正文的读取范围;至少一个文稿来源。"""
|
||||
if not isinstance(文稿查询, tuple) or not 文稿查询:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "历史范围至少声明一个文稿来源")
|
||||
return self.授权任务范围(身份, 作品ID, 截止, 文稿查询)
|
||||
|
||||
def 授权任务范围(
|
||||
self,
|
||||
身份: 调用身份,
|
||||
作品ID: str,
|
||||
截止: int,
|
||||
文稿查询: tuple[dict, ...] = (),
|
||||
*,
|
||||
目标章ID: str | None = None,
|
||||
含事实: bool = False,
|
||||
) -> dict:
|
||||
"""按作者身份冻结历史文稿、目标章细纲与事实系统版本;两种用途随范围绑定。"""
|
||||
if not 作品ID:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务范围必须绑定明确作品")
|
||||
if 身份.作者 is None or 身份.用途 is not self.数据库.用途:
|
||||
raise 上下文错误("SCOPE_DENIED", "任务范围授权需要与装配用途一致的作者身份")
|
||||
校验截止(截止)
|
||||
if not isinstance(文稿查询, tuple):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"文稿来源必须以元组显式声明:{文稿查询!r}")
|
||||
if not 文稿查询 and 目标章ID is None and not 含事实:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务范围至少声明文稿、目标章细纲或事实之一")
|
||||
if 目标章ID is not None and (not isinstance(目标章ID, str) or not 目标章ID):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "目标章必须是非空字符串")
|
||||
运行用途 = self.数据库.用途.value
|
||||
内容用途 = 身份.内容用途.value
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
目录 = self._读取目录(连, 身份.作者, 作品ID)
|
||||
章节 = {c["chapter_id"]: c for c in 目录["chapters"]}
|
||||
文稿 = [self._授权文稿(连, 身份.作者, 作品ID, 截止, 章节, 查询) for 查询 in 文稿查询]
|
||||
if len({d["document_id"] for d in 文稿}) != len(文稿):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "同一文稿在历史范围内重复声明")
|
||||
return {
|
||||
细纲 = (
|
||||
self._授权细纲(
|
||||
连,
|
||||
身份.作者,
|
||||
作品ID,
|
||||
截止,
|
||||
章节,
|
||||
目标章ID,
|
||||
内容用途,
|
||||
运行用途,
|
||||
self.规划引用解析,
|
||||
)
|
||||
if 目标章ID is not None
|
||||
else None
|
||||
)
|
||||
事实 = (
|
||||
self._授权事实(连, 身份.作者, 作品ID, 截止, 内容用途, 运行用途) if 含事实 else None
|
||||
)
|
||||
来源集 = [d["document_id"] for d in 文稿]
|
||||
if 细纲 is not None:
|
||||
来源集.append(细纲["plan_id"])
|
||||
if 事实 is not None:
|
||||
来源集.extend(事实["object_ids"])
|
||||
if len({*来源集}) != len(来源集):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "同一来源在任务范围内重复声明")
|
||||
范围 = {
|
||||
"work_id": 作品ID,
|
||||
"as_of": 截止,
|
||||
"source_ids": [d["document_id"] for d in 文稿],
|
||||
"source_ids": 来源集,
|
||||
"documents": 文稿,
|
||||
"directory_revision": 目录["revision"],
|
||||
"authorized_by": 身份.作者,
|
||||
"run_purpose": 运行用途,
|
||||
"content_purpose": 内容用途,
|
||||
}
|
||||
if 细纲 is not None:
|
||||
范围["target_plan"] = 细纲
|
||||
if 事实 is not None:
|
||||
范围["facts"] = {"system_revision": 事实["system_revision"]}
|
||||
return 范围
|
||||
|
||||
def 读取范围(self, 任务ID: str) -> dict:
|
||||
"""从已保存任务恢复冻结范围;形状或身份不一致即失败关闭。"""
|
||||
return self.恢复冻结范围(self.任务.读取任务(任务ID))
|
||||
|
||||
def 冻结上下文(self, 任务ID: str, 装配: 材料装配结果, 绑定: tuple[来源绑定, ...]) -> 冻结回执:
|
||||
"""把实际装配与每来源版本绑定冻结为不可变快照;同一输入幂等为一行。
|
||||
|
||||
绑定在服务端装配路径发生:任务身份与范围取自已保存任务,
|
||||
模型与工具侧不提供快照身份;任一输入变化产生新快照,不改写旧快照。
|
||||
"""
|
||||
if not isinstance(任务ID, str) or not 任务ID:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "冻结上下文必须绑定明确任务")
|
||||
任务 = self.任务.读取任务(任务ID)
|
||||
范围 = self.恢复冻结范围(任务)
|
||||
self._校验冻结输入(任务, 范围, 装配, 绑定)
|
||||
载荷 = 快照载荷(任务ID, 范围, 装配, 绑定)
|
||||
哈希 = 输入哈希(载荷)
|
||||
身份 = 快照身份(任务ID, 哈希)
|
||||
with self.数据库.连接() as 连, 连.transaction():
|
||||
新建 = 上下文存储(连, self.数据库.用途).写入快照(
|
||||
{
|
||||
"snapshot_id": 身份,
|
||||
"run_purpose": self.数据库.用途.value,
|
||||
"task_id": 任务ID,
|
||||
"work_id": 范围["work_id"],
|
||||
"input_hash": 哈希,
|
||||
"frozen_scope": 范围,
|
||||
"assembly": 装配记录(装配, 含文本=True),
|
||||
"source_bindings": 绑定载荷(绑定),
|
||||
"created_by": 范围["authorized_by"],
|
||||
}
|
||||
)
|
||||
return 冻结回执(身份, 哈希, 新建)
|
||||
|
||||
def 读取快照(self, 快照ID: str) -> 上下文快照:
|
||||
"""按快照身份只读回读;对照当前来源标注有效性,不改写历史。"""
|
||||
if not isinstance(快照ID, str) or not 快照ID:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "回读快照必须提供明确身份")
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
行 = 上下文存储(连, self.数据库.用途).按身份读取(快照ID)
|
||||
if 行 is None:
|
||||
raise 上下文错误("SOURCE_NOT_FOUND", f"上下文快照 {快照ID} 不存在")
|
||||
return 快照从行(行, 范围漂移说明(连, 行["frozen_scope"], self.规划引用解析))
|
||||
|
||||
def 读取任务快照(self, 任务ID: str) -> tuple[上下文快照, ...]:
|
||||
"""按任务回读全部消费快照(按创建时间有序);消费与任务可互相关联追溯。"""
|
||||
if not isinstance(任务ID, str) or not 任务ID:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "按任务回读快照必须提供明确任务")
|
||||
with self.数据库.连接(只读=True) as 连:
|
||||
行集 = 上下文存储(连, self.数据库.用途).按任务读取(任务ID)
|
||||
return tuple(
|
||||
快照从行(行, 范围漂移说明(连, 行["frozen_scope"], self.规划引用解析)) for 行 in 行集
|
||||
)
|
||||
|
||||
def 回放探索(self, 任务ID: str, 清单: tuple[探索条目, ...]) -> 回放结果:
|
||||
"""按当前授权恢复的范围确定性回放清单读取;授权失败即 SCOPE_DENIED。
|
||||
|
||||
回放输入是任务内冻结范围加清单;重读以相同输入版本执行,任何漂移
|
||||
抛 SOURCE_STALE 并指明项,不采纳内容、不追加探索、不调用模型。
|
||||
"""
|
||||
if not isinstance(任务ID, str) or not 任务ID:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "回放探索必须提供明确任务")
|
||||
任务 = self.任务.读取任务(任务ID)
|
||||
try:
|
||||
冻结 = self.恢复冻结范围(任务)
|
||||
except 上下文错误 as 错误:
|
||||
raise 上下文错误("SCOPE_DENIED", f"任务授权重验未通过,不能回放:{错误.说明}") from None
|
||||
return 回放(
|
||||
self.数据库,
|
||||
冻结,
|
||||
工具范围从冻结(任务ID, 冻结),
|
||||
清单,
|
||||
self.规划引用解析,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _校验冻结输入(
|
||||
任务: 任务快照, 范围: dict, 装配: 材料装配结果, 绑定: tuple[来源绑定, ...]
|
||||
) -> None:
|
||||
"""冻结前校验装配自洽、预算不超限且每个绑定来源都在授权范围内。"""
|
||||
if len(set(装配.入选来源)) != len(装配.入选来源):
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"装配入选来源重复:{装配.入选来源}")
|
||||
if 装配.字节数 != len(装配.文本.encode("utf-8")):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID",
|
||||
f"装配字节数 {装配.字节数} 与文本实际 UTF-8 字节不符",
|
||||
)
|
||||
for 项 in 装配.省略:
|
||||
if len(项) != 2 or not all(isinstance(片段, str) and 片段 for 片段 in 项):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"省略条目必须是(来源,原因)非空字符串对:{项!r}"
|
||||
)
|
||||
预算 = (任务.冻结输入.get("冻结上下文") or {}).get("budget") or {}
|
||||
上限 = 预算.get("context_bytes")
|
||||
if isinstance(上限, bool) or not isinstance(上限, int) or 上限 <= 0:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"任务上下文字节预算必须是正整数,收到 {上限!r}"
|
||||
)
|
||||
if 装配.字节数 > 上限:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_BUDGET_EXCEEDED", f"装配材料 {装配.字节数} 字节超出任务预算 {上限}"
|
||||
)
|
||||
if not isinstance(绑定, tuple) or not 绑定:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "冻结至少绑定一个实际消费来源")
|
||||
允许 = set(范围["source_ids"])
|
||||
已见: set[str] = set()
|
||||
for 项 in 绑定:
|
||||
for 名, 值 in (
|
||||
("来源ID", 项.来源ID),
|
||||
("数据版本", 项.数据版本),
|
||||
("结构哈希", 项.结构哈希),
|
||||
("投影版本", 项.投影版本),
|
||||
):
|
||||
if not isinstance(值, str) or not 值:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"来源绑定的 {名} 必须是非空字符串")
|
||||
if 项.来源ID in 已见:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"来源绑定重复:{项.来源ID}")
|
||||
已见.add(项.来源ID)
|
||||
if 项.来源ID not in 允许:
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED", f"来源 {项.来源ID} 不在任务冻结范围内,不能记入消费"
|
||||
)
|
||||
|
||||
def 恢复冻结范围(self, 任务: 任务快照) -> dict:
|
||||
"""校验任务内冻结范围完整且与任务、装配用途一致,然后原样返回。"""
|
||||
固定 = 任务.冻结输入
|
||||
@ -96,8 +295,8 @@ class 上下文服务:
|
||||
"CONTEXT_SCOPE_INVALID", f"冻结目录版本必须是非负整数,收到 {目录版本!r}"
|
||||
)
|
||||
文稿 = 范围.get("documents")
|
||||
if not isinstance(文稿, list) or not 文稿:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结范围没有已授权文稿")
|
||||
if not isinstance(文稿, list):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结文稿清单必须是列表")
|
||||
for 项 in 文稿:
|
||||
if not isinstance(项, dict) or not _文稿必需键 <= 项.keys():
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结文稿缺少身份、版本或哈希绑定")
|
||||
@ -114,17 +313,143 @@ class 上下文服务:
|
||||
for 键 in ("chapter_id", "branch_id", "document_id", "document_hash"):
|
||||
if not isinstance(项[键], str) or not 项[键]:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"冻结文稿的 {键} 必须是非空字符串")
|
||||
目标 = 范围.get("target_plan")
|
||||
if 目标 is not None:
|
||||
self._校验细纲节(目标)
|
||||
事实 = 范围.get("facts")
|
||||
if 事实 is not None:
|
||||
self._校验事实节(事实)
|
||||
if not 文稿 and 目标 is None and 事实 is None:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结范围没有已授权来源")
|
||||
if 目标 is not None or 事实 is not None:
|
||||
self._校验用途绑定(范围, 固定)
|
||||
身份集 = 范围.get("source_ids")
|
||||
if (
|
||||
not isinstance(身份集, list)
|
||||
or not 身份集
|
||||
or not all(isinstance(i, str) and i for i in 身份集)
|
||||
or len(set(身份集)) != len(身份集)
|
||||
or set(身份集) != {项["document_id"] for 项 in 文稿}
|
||||
):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结来源身份与文稿清单不一致")
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结来源身份必须是唯一的非空字符串清单")
|
||||
基础 = {项["document_id"] for 项 in 文稿}
|
||||
if 目标 is not None:
|
||||
基础.add(目标["plan_id"])
|
||||
附加 = 目标 is not None or 事实 is not None
|
||||
if (not 附加 and set(身份集) != 基础) or not 基础 <= set(身份集):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结来源身份与文稿、细纲清单不一致")
|
||||
return 范围
|
||||
|
||||
@staticmethod
|
||||
def _校验细纲节(目标: Any) -> None:
|
||||
if not isinstance(目标, dict):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结目标章细纲必须是对象")
|
||||
for 键 in ("chapter_id", "plan_id", "content_hash"):
|
||||
值 = 目标.get(键)
|
||||
if not isinstance(值, str) or not 值:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"冻结细纲的 {键} 必须是非空字符串")
|
||||
版本 = 目标.get("revision")
|
||||
if isinstance(版本, bool) or not isinstance(版本, int) or 版本 < 1:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"冻结细纲版本必须是正整数,收到 {版本!r}")
|
||||
|
||||
@staticmethod
|
||||
def _校验事实节(事实: Any) -> None:
|
||||
if not isinstance(事实, dict):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结事实节必须是对象")
|
||||
版本 = 事实.get("system_revision")
|
||||
if isinstance(版本, bool) or not isinstance(版本, int) or 版本 < 0:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_SCOPE_INVALID", f"冻结事实系统版本必须是非负整数,收到 {版本!r}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _校验用途绑定(范围: dict, 固定: dict) -> None:
|
||||
运行 = 范围.get("run_purpose")
|
||||
内容 = 范围.get("content_purpose")
|
||||
if 运行 not in {"production", "evaluation", "maintenance"} or 内容 not in {
|
||||
"planning",
|
||||
"generation",
|
||||
"detection",
|
||||
"extraction",
|
||||
}:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务范围必须绑定执行与内容用途")
|
||||
if str(固定.get("执行用途")) != 运行 or str(固定.get("内容用途")) != 内容:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务用途与冻结范围用途不一致")
|
||||
|
||||
@staticmethod
|
||||
def _授权细纲(
|
||||
连: Connection,
|
||||
作者: str,
|
||||
作品ID: str,
|
||||
截止: int,
|
||||
章节: dict,
|
||||
目标章ID: str,
|
||||
内容用途: str,
|
||||
运行用途: str,
|
||||
引用解析,
|
||||
) -> dict:
|
||||
"""目标章必须紧随叙事截止之后;细纲按任务用途真实读取并冻结身份。"""
|
||||
行 = 章节.get(目标章ID)
|
||||
if 行 is None:
|
||||
归属 = None
|
||||
try:
|
||||
归属 = 读取章节归属(连, 作者, 目标章ID)
|
||||
except 作品错误 as 错误:
|
||||
raise _拒绝(错误, f"目标章 {目标章ID} 无法授权") from None
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED",
|
||||
f"目标章 {目标章ID} 不属于作品 {作品ID}(实际属于 {归属['work_id']})",
|
||||
)
|
||||
if 行["position"] != 截止 + 1:
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED",
|
||||
f"目标章 {目标章ID} 必须紧随叙事截止 {截止} 之后(目录位置 {行['position']});"
|
||||
"历史正文与目标章规划的时间语义不能互换",
|
||||
)
|
||||
try:
|
||||
记录 = 读取细纲投影(
|
||||
连,
|
||||
作者,
|
||||
作品ID,
|
||||
目标章ID,
|
||||
内容用途=内容用途,
|
||||
运行用途=运行用途,
|
||||
引用解析=引用解析,
|
||||
)
|
||||
except 作品错误 as 错误:
|
||||
raise _拒绝(错误, f"目标章 {目标章ID} 的细纲授权被拒绝") from None
|
||||
return {
|
||||
"chapter_id": 目标章ID,
|
||||
"plan_id": 记录["plan_id"],
|
||||
"revision": 记录["revision"],
|
||||
"content_hash": 记录["content_hash"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _授权事实(
|
||||
连: Connection, 作者: str, 作品ID: str, 截止: int, 内容用途: str, 运行用途: str
|
||||
) -> dict:
|
||||
"""按作者视角枚举时点事实并冻结当前事实系统版本与对象身份。"""
|
||||
try:
|
||||
结果 = 读取时点事实投影(
|
||||
连,
|
||||
作者,
|
||||
作品ID,
|
||||
截止,
|
||||
视角="author",
|
||||
内容用途=内容用途,
|
||||
运行用途=运行用途,
|
||||
)
|
||||
except 事实错误 as 错误:
|
||||
raise 上下文错误(
|
||||
错误.错误码, f"作品 {作品ID} 的事实范围授权被拒绝:{错误.说明}"
|
||||
) from None
|
||||
return {
|
||||
"system_revision": 结果["system_revision"],
|
||||
"object_ids": [
|
||||
项["object_id"] for 键 in ("facts", "beliefs", "plans") for 项 in 结果[键]
|
||||
],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _读取目录(连: Connection, 作者: str, 作品ID: str) -> dict:
|
||||
try:
|
||||
|
||||
206
src/muse/上下文/依赖校验.py
Normal file
206
src/muse/上下文/依赖校验.py
Normal file
@ -0,0 +1,206 @@
|
||||
"""冻结来源的授权、撤回与版本变化核对;恢复重验以真实检查阻断旧任务。
|
||||
|
||||
合同依据:模块设计/B09-上下文.md、接口契约/任务工具与事件.md「恢复与停机」。
|
||||
|
||||
- 输入冻结范围(或快照)加事务连接:核对授权仍在(作者/作品/用途)、来源未撤回
|
||||
(目录版本、文稿版本与哈希、细纲身份三元组、事实系统版本)、结构与投影版本未变。
|
||||
- 输出当前有效性及锁定依据:有效附各来源当前版本绑定,无效附具体变化项;
|
||||
只判断有效性,不修改任何模块内容。
|
||||
- 恢复重验检查供 S02 流程登记挂接:漂移即抛错阻断任务重验,不自报已重验。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from psycopg import Connection
|
||||
|
||||
from muse.上下文.任务范围 import 上下文服务
|
||||
from muse.上下文.来源读取 import 投影版本 as 可见投影版本
|
||||
from muse.上下文.模型 import (
|
||||
上下文错误,
|
||||
依赖状态,
|
||||
依赖锁定,
|
||||
允许内容用途,
|
||||
允许执行用途,
|
||||
来源绑定,
|
||||
)
|
||||
from muse.任务运行.接口 import 任务快照
|
||||
from muse.作品规划.接口 import 作品错误, 读取作品范围, 读取细纲投影
|
||||
from muse.故事世界.接口 import 读取当前事实系统版本
|
||||
from muse.故事世界.模型 import 事实错误
|
||||
from muse.正文写作.接口 import 读取正文依据
|
||||
from muse.正文写作.模型 import 正文错误
|
||||
|
||||
|
||||
def 核对依赖(
|
||||
连: Connection,
|
||||
冻结范围: dict,
|
||||
引用解析=None,
|
||||
基准绑定: tuple[来源绑定, ...] = (),
|
||||
) -> 依赖状态:
|
||||
"""在调用方事务内重查冻结来源;返回当前有效性与逐来源锁定依据。"""
|
||||
if not isinstance(冻结范围, dict):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "依赖核对需要任务冻结范围")
|
||||
基准: dict[str, 来源绑定] = {}
|
||||
for 绑 in 基准绑定:
|
||||
if not isinstance(绑, 来源绑定) or 绑.来源ID in 基准:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "基准来源绑定必须是未重复的来源绑定对象")
|
||||
基准[绑.来源ID] = 绑
|
||||
作者 = 冻结范围.get("authorized_by")
|
||||
作品 = 冻结范围.get("work_id")
|
||||
if not isinstance(作者, str) or not 作者 or not isinstance(作品, str) or not 作品:
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "依赖核对需要冻结范围的作者与作品绑定")
|
||||
变化项: list[str] = []
|
||||
锁定: list[依赖锁定] = []
|
||||
try:
|
||||
目录 = 读取作品范围(连, 作者, 作品)
|
||||
except 作品错误 as 错误:
|
||||
return 依赖状态(False, (), (f"作品或授权已不可用:{错误.说明}",))
|
||||
运行 = 冻结范围.get("run_purpose")
|
||||
内容 = 冻结范围.get("content_purpose")
|
||||
用途有效 = (
|
||||
isinstance(运行, str)
|
||||
and 运行 in 允许执行用途
|
||||
and isinstance(内容, str)
|
||||
and 内容 in 允许内容用途
|
||||
)
|
||||
if not 用途有效:
|
||||
变化项.append(f"冻结范围的执行用途 {运行!r} 或内容用途 {内容!r} 已不被接受;来源锁定失效")
|
||||
运行 = 运行 if isinstance(运行, str) else ""
|
||||
内容 = 内容 if isinstance(内容, str) else ""
|
||||
投影 = 可见投影版本(运行, 内容) if 用途有效 else ""
|
||||
if 目录["revision"] != 冻结范围["directory_revision"]:
|
||||
变化项.append(
|
||||
f"作品目录已从版本 {冻结范围['directory_revision']} 前进到 {目录['revision']};"
|
||||
"来源锁定失效"
|
||||
)
|
||||
else:
|
||||
锁定.append(依赖锁定(f"{作品}:目录", "directory", str(目录["revision"]), ""))
|
||||
for 目标 in 冻结范围.get("documents", ()):
|
||||
章节 = next((c for c in 目录["chapters"] if c["chapter_id"] == 目标["chapter_id"]), None)
|
||||
if 章节 is None or 章节["position"] != 目标["position"]:
|
||||
变化项.append(f"章节 {目标['chapter_id']} 的目录位置已漂移或已被移除;来源锁定失效")
|
||||
continue
|
||||
try:
|
||||
依据 = 读取正文依据(
|
||||
连, 作者, 目标["chapter_id"], 目标["revision"], 分支=目标["branch_id"]
|
||||
)
|
||||
except 正文错误 as 错误:
|
||||
变化项.append(
|
||||
f"文稿 {目标['chapter_id']} 版本 {目标['revision']} 已不可读取:{错误.说明}"
|
||||
)
|
||||
continue
|
||||
if 依据["current_revision"] != 目标["revision"]:
|
||||
变化项.append(
|
||||
f"文稿 {目标['chapter_id']} 已从版本 {目标['revision']} 前进到 "
|
||||
f"{依据['current_revision']};来源锁定失效"
|
||||
)
|
||||
continue
|
||||
if 依据["document_hash"] != 目标["document_hash"]:
|
||||
变化项.append(f"文稿 {目标['chapter_id']} 的结构哈希与冻结绑定不一致;来源锁定失效")
|
||||
continue
|
||||
基 = 基准.get(依据["document_id"])
|
||||
if 基 is not None and (基.数据版本, 基.结构哈希, 基.投影版本) != (
|
||||
str(目标["revision"]),
|
||||
目标["document_hash"],
|
||||
投影,
|
||||
):
|
||||
变化项.append(
|
||||
f"来源 {依据['document_id']} 与冻结消费绑定的版本、结构或投影版本不一致;"
|
||||
"来源锁定失效"
|
||||
)
|
||||
continue
|
||||
锁定.append(
|
||||
依赖锁定(
|
||||
依据["document_id"],
|
||||
"document",
|
||||
str(目标["revision"]),
|
||||
依据["document_hash"],
|
||||
投影,
|
||||
)
|
||||
)
|
||||
目标章 = 冻结范围.get("target_plan")
|
||||
if isinstance(目标章, dict):
|
||||
try:
|
||||
记录 = 读取细纲投影(
|
||||
连,
|
||||
作者,
|
||||
作品,
|
||||
目标章["chapter_id"],
|
||||
内容用途=内容,
|
||||
运行用途=运行,
|
||||
引用解析=引用解析,
|
||||
)
|
||||
except (作品错误, 事实错误) as 错误:
|
||||
# 细纲不可用、已撤回或其本书引用失去当前依据都算锁定失效,不向外泄漏
|
||||
变化项.append(f"目标章 {目标章['chapter_id']} 细纲已不可用或引用失效:{错误.说明}")
|
||||
else:
|
||||
if (
|
||||
记录["plan_id"],
|
||||
记录["revision"],
|
||||
记录["content_hash"],
|
||||
) != (
|
||||
目标章["plan_id"],
|
||||
目标章["revision"],
|
||||
目标章["content_hash"],
|
||||
):
|
||||
变化项.append(
|
||||
f"目标章 {目标章['chapter_id']} 细纲已从版本 {目标章['revision']} 漂移到 "
|
||||
f"{记录['revision']};来源锁定失效"
|
||||
)
|
||||
else:
|
||||
基 = 基准.get(记录["plan_id"])
|
||||
if 基 is not None and (
|
||||
基.数据版本,
|
||||
基.结构哈希,
|
||||
基.投影版本,
|
||||
) != (
|
||||
str(记录["revision"]),
|
||||
记录["content_hash"],
|
||||
记录["projection_version"],
|
||||
):
|
||||
变化项.append(
|
||||
f"来源 {记录['plan_id']} 与冻结消费绑定的版本、结构或投影版本不一致;"
|
||||
"来源锁定失效"
|
||||
)
|
||||
else:
|
||||
锁定.append(
|
||||
依赖锁定(
|
||||
记录["plan_id"],
|
||||
"plan",
|
||||
str(记录["revision"]),
|
||||
记录["content_hash"],
|
||||
记录["projection_version"],
|
||||
)
|
||||
)
|
||||
事实节 = 冻结范围.get("facts")
|
||||
if isinstance(事实节, dict):
|
||||
try:
|
||||
当前版本 = 读取当前事实系统版本(连, 作者, 作品)
|
||||
except 事实错误 as 错误:
|
||||
变化项.append(f"事实系统已不可读取:{错误.说明}")
|
||||
else:
|
||||
if 当前版本 != 事实节["system_revision"]:
|
||||
变化项.append(
|
||||
f"事实系统版本已从 {事实节['system_revision']} 前进到 {当前版本};来源锁定失效"
|
||||
)
|
||||
else:
|
||||
锁定.append(依赖锁定(f"{作品}:事实系统", "facts", str(当前版本), "", 投影))
|
||||
return 依赖状态(有效=not 变化项, 锁定依据=tuple(锁定), 变化项=tuple(变化项))
|
||||
|
||||
|
||||
def 依赖重验检查(服务: 上下文服务) -> Callable[[任务快照], None]:
|
||||
"""S02 恢复检查的真实实现:授权、来源、结构与版本重验;漂移即阻断。"""
|
||||
|
||||
def 检查(任务: 任务快照) -> None:
|
||||
范围 = 服务.恢复冻结范围(任务)
|
||||
with 服务.数据库.连接(只读=True) as 连:
|
||||
状态 = 核对依赖(连, 范围, 服务.规划引用解析)
|
||||
if not 状态.有效:
|
||||
raise 上下文错误("SOURCE_STALE", "恢复重验未通过:" + ";".join(状态.变化项))
|
||||
|
||||
return 检查
|
||||
|
||||
|
||||
__all__ = ["依赖重验检查", "核对依赖"]
|
||||
102
src/muse/上下文/冻结快照.py
Normal file
102
src/muse/上下文/冻结快照.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""上下文快照的确定性身份派生与行映射;纯函数不访问数据库。
|
||||
|
||||
合同依据:模块设计/B09-上下文.md、接口契约/任务工具与事件.md。
|
||||
|
||||
- 快照身份与输入哈希由载荷确定性派生(固定哈希 + uuid5 惯例):
|
||||
同一任务同一输入重复冻结得到同一身份;任一输入变化产生新身份,不改写旧快照。
|
||||
- 载荷哈希使用键排序的稳定 JSON 序列化,与字典构造顺序无关。
|
||||
- 装配文本按 UTF-8 字节取哈希,与材料字节数同口径。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
from muse.上下文.模型 import 上下文快照, 材料装配结果, 来源绑定
|
||||
from muse.正式变更.接口 import 固定哈希
|
||||
|
||||
快照身份前缀 = "muse:B09:context-snapshot:"
|
||||
|
||||
|
||||
def 文本哈希(文本: str) -> str:
|
||||
"""装配文本按 UTF-8 字节取 SHA-256;与材料字节数同口径。"""
|
||||
return hashlib.sha256(文本.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def 绑定载荷(绑定: tuple[来源绑定, ...]) -> list[dict]:
|
||||
"""来源绑定按工具读取事件 source_refs 的固定键序列化。"""
|
||||
return [
|
||||
{
|
||||
"source_id": 项.来源ID,
|
||||
"data_version": 项.数据版本,
|
||||
"schema_hash": 项.结构哈希,
|
||||
"projection_version": 项.投影版本,
|
||||
}
|
||||
for 项 in 绑定
|
||||
]
|
||||
|
||||
|
||||
def 装配记录(装配: 材料装配结果, *, 含文本: bool) -> dict:
|
||||
"""装配取舍的稳定记录;哈希载荷不含全文,落库记录保留实际消费文本。"""
|
||||
记录: dict = {
|
||||
"入选": list(装配.入选来源),
|
||||
"省略": [[项[0], 项[1]] for 项 in 装配.省略],
|
||||
"字节数": 装配.字节数,
|
||||
"文本哈希": 文本哈希(装配.文本),
|
||||
}
|
||||
if 含文本:
|
||||
记录["文本"] = 装配.文本
|
||||
return 记录
|
||||
|
||||
|
||||
def 快照载荷(任务ID: str, 范围: dict, 装配: 材料装配结果, 绑定: tuple[来源绑定, ...]) -> dict:
|
||||
"""冻结哈希覆盖任务身份、冻结范围、装配取舍与每来源版本绑定。"""
|
||||
return {
|
||||
"任务ID": 任务ID,
|
||||
"范围": 范围,
|
||||
"装配": 装配记录(装配, 含文本=False),
|
||||
"来源绑定": 绑定载荷(绑定),
|
||||
}
|
||||
|
||||
|
||||
def 输入哈希(载荷: dict) -> str:
|
||||
"""载荷的稳定哈希;键排序序列化,与字典构造顺序无关。"""
|
||||
return 固定哈希(载荷)
|
||||
|
||||
|
||||
def 快照身份(任务ID: str, 哈希: str) -> str:
|
||||
"""由任务身份与输入哈希确定性派生快照身份;同一输入永远同一身份。"""
|
||||
return str(uuid5(NAMESPACE_URL, 快照身份前缀 + 固定哈希([任务ID, 哈希])))
|
||||
|
||||
|
||||
def 快照从行(行: dict, 漂移: str | None) -> 上下文快照:
|
||||
"""把快照行映射为只读对象;漂移说明只标注当前状态,不改写历史字段。"""
|
||||
return 上下文快照(
|
||||
快照ID=str(行["snapshot_id"]),
|
||||
任务ID=str(行["task_id"]),
|
||||
作品ID=行["work_id"],
|
||||
输入哈希=行["input_hash"],
|
||||
冻结范围=行["frozen_scope"],
|
||||
装配=行["assembly"],
|
||||
来源绑定=tuple(
|
||||
来源绑定(
|
||||
项["source_id"], 项["data_version"], 项["schema_hash"], 项["projection_version"]
|
||||
)
|
||||
for 项 in 行["source_bindings"]
|
||||
),
|
||||
当前状态="SOURCE_STALE" if 漂移 else "有效",
|
||||
失效说明=漂移,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"快照身份",
|
||||
"快照从行",
|
||||
"快照载荷",
|
||||
"快照身份前缀",
|
||||
"文本哈希",
|
||||
"绑定载荷",
|
||||
"装配记录",
|
||||
"输入哈希",
|
||||
]
|
||||
80
src/muse/上下文/存储.py
Normal file
80
src/muse/上下文/存储.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""上下文快照数据的 SQL 读写;只操作本模块记录并使用调用方事务。
|
||||
|
||||
合同依据:模块设计/B09-上下文.md、文件设计/后端-B09-上下文.md。
|
||||
- 快照身份由载荷确定性派生,写入按 ON CONFLICT DO NOTHING 幂等;
|
||||
已存在行与输入哈希不一致视为身份冲突,立即失败而不是改写历史。
|
||||
- 读取只返回本模块行,不做来源重查;漂移判定由冻结快照读取路径另行标注。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, LiteralString
|
||||
|
||||
import psycopg
|
||||
from psycopg import sql
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
from muse.上下文.模型 import 上下文错误
|
||||
from muse.共享.调用身份 import 用途
|
||||
|
||||
|
||||
class 上下文存储:
|
||||
"""muse_context_snapshot 的版本读写;不跨 owner、不调用模型。"""
|
||||
|
||||
def __init__(self, 连接: psycopg.Connection, 执行用途: 用途) -> None:
|
||||
self.连接 = 连接
|
||||
self.用途 = 执行用途
|
||||
self.命名空间 = "evaluation" if 执行用途 is 用途.评测 else "public"
|
||||
|
||||
def 查询(self, 语句: LiteralString, 参数: tuple = ()) -> Any:
|
||||
"""仅本模块固定 SQL 使用 schema 占位,不接收外部 SQL 或表名。"""
|
||||
游标 = self.连接.cursor(row_factory=dict_row)
|
||||
return 游标.execute(sql.SQL(语句).format(s=sql.Identifier(self.命名空间)), 参数)
|
||||
|
||||
def 写入快照(self, 行: dict) -> bool:
|
||||
"""按快照身份幂等写入;返回是否新建行,已存在行核对哈希一致。"""
|
||||
新建 = self.查询(
|
||||
"INSERT INTO {s}.muse_context_snapshot (snapshot_id,run_purpose,task_id,work_id,"
|
||||
"input_hash,frozen_scope,assembly,source_bindings,created_by) "
|
||||
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (snapshot_id) DO NOTHING "
|
||||
"RETURNING snapshot_id",
|
||||
(
|
||||
行["snapshot_id"],
|
||||
行["run_purpose"],
|
||||
行["task_id"],
|
||||
行["work_id"],
|
||||
行["input_hash"],
|
||||
Jsonb(行["frozen_scope"]),
|
||||
Jsonb(行["assembly"]),
|
||||
Jsonb(行["source_bindings"]),
|
||||
行["created_by"],
|
||||
),
|
||||
).fetchone()
|
||||
if 新建 is not None:
|
||||
return True
|
||||
既有 = self.查询(
|
||||
"SELECT input_hash FROM {s}.muse_context_snapshot WHERE snapshot_id=%s",
|
||||
(行["snapshot_id"],),
|
||||
).fetchone()
|
||||
if 既有 is None or 既有["input_hash"] != 行["input_hash"]:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_SNAPSHOT_CONFLICT",
|
||||
f"快照 {行['snapshot_id']} 已对应不同输入哈希;快照不可改写",
|
||||
)
|
||||
return False
|
||||
|
||||
def 按身份读取(self, 快照ID: str) -> dict | None:
|
||||
return self.查询(
|
||||
"SELECT * FROM {s}.muse_context_snapshot WHERE snapshot_id=%s", (快照ID,)
|
||||
).fetchone()
|
||||
|
||||
def 按任务读取(self, 任务ID: str) -> list[dict]:
|
||||
return self.查询(
|
||||
"SELECT * FROM {s}.muse_context_snapshot WHERE task_id=%s "
|
||||
"ORDER BY created_at,snapshot_id",
|
||||
(任务ID,),
|
||||
).fetchall()
|
||||
|
||||
|
||||
__all__ = ["上下文存储"]
|
||||
332
src/muse/上下文/探索回放.py
Normal file
332
src/muse/上下文/探索回放.py
Normal file
@ -0,0 +1,332 @@
|
||||
"""具名只读探索的清单校验与确定性回放;漂移即失效,不触发新探索。
|
||||
|
||||
合同依据:模块设计/B09-上下文.md、接口契约/任务工具与事件.md。
|
||||
|
||||
- 清单是一次任务具名只读调用的有序记录:调用ID、工具名、参数、顺序、
|
||||
每来源引用与消费哈希;未知工具、参数越界、顺序断裂、缺来源绑定分别明确拒绝。
|
||||
- 回放按清单顺序以相同输入版本重新执行工具读取,逐项与清单来源绑定比对:
|
||||
数据版本、结构哈希、投影版本与内容消费哈希一致才算成功;任何漂移
|
||||
明确失效(SOURCE_STALE)并指明项,不采纳内容、不沿用已撤回权限。
|
||||
- 回放只用固定材料:直接调用来源读取函数,不追加工具读取留痕、不调用模型。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
from jsonschema.exceptions import ValidationError
|
||||
|
||||
from muse.上下文.来源读取 import (
|
||||
事实参数合同,
|
||||
历史正文参数合同,
|
||||
细纲参数合同,
|
||||
读取历史正文,
|
||||
读取时点事实,
|
||||
读取目标细纲,
|
||||
)
|
||||
from muse.上下文.模型 import (
|
||||
上下文错误,
|
||||
允许内容用途,
|
||||
允许执行用途,
|
||||
回放条目,
|
||||
回放结果,
|
||||
探索条目,
|
||||
来源绑定,
|
||||
)
|
||||
from muse.任务运行.接口 import 内容哈希, 工具结果, 工具范围
|
||||
from muse.共享.错误 import Muse错误
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
|
||||
已知工具 = frozenset({"read_history_document", "query_work_plan", "query_facts_as_of"})
|
||||
扩权字段 = frozenset(
|
||||
{
|
||||
"work_id",
|
||||
"as_of",
|
||||
"run_purpose",
|
||||
"content_purpose",
|
||||
"source_scope",
|
||||
"author_id",
|
||||
"decided_by",
|
||||
}
|
||||
)
|
||||
工具合同 = {
|
||||
"read_history_document": 历史正文参数合同,
|
||||
"query_work_plan": 细纲参数合同,
|
||||
"query_facts_as_of": 事实参数合同,
|
||||
}
|
||||
|
||||
|
||||
def 记录探索(
|
||||
清单: tuple[探索条目, ...], 调用ID: str, 工具名: str, 参数: dict, 结果: 工具结果
|
||||
) -> tuple[探索条目, ...]:
|
||||
"""把一次成功读取追加为清单条目;顺序按追加次序指派,消费哈希按结果内容派生。"""
|
||||
if not isinstance(调用ID, str) or not 调用ID:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "清单条目必须携带非空调用ID")
|
||||
if 工具名 not in 已知工具:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"未知工具 {工具名!r},不能记入探索清单")
|
||||
if not isinstance(参数, dict):
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"清单条目参数必须是对象:{参数!r}")
|
||||
if not isinstance(结果, 工具结果) or not 结果.来源:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"调用 {调用ID} 没有实际来源绑定,不能记入探索清单"
|
||||
)
|
||||
条目 = 探索条目(
|
||||
调用ID=调用ID,
|
||||
工具名=工具名,
|
||||
顺序=len(清单) + 1,
|
||||
参数=参数,
|
||||
来源引用=tuple(
|
||||
来源绑定(源.来源ID, 源.数据版本, 源.结构哈希, 源.投影版本) for 源 in 结果.来源
|
||||
),
|
||||
消费哈希=内容哈希(结果.内容),
|
||||
)
|
||||
return 清单 + (条目,)
|
||||
|
||||
|
||||
def 校验清单(清单: tuple[探索条目, ...], 冻结范围: dict) -> None:
|
||||
"""清单形状与参数范围的确定性校验;不含字符串存在性冒充。"""
|
||||
if not isinstance(清单, (tuple, list)) or not 清单:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "探索清单必须是非空条目序列")
|
||||
if not isinstance(冻结范围, dict):
|
||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "清单校验需要任务冻结范围")
|
||||
已见: set[str] = set()
|
||||
for 位, 项 in enumerate(清单, start=1):
|
||||
if not isinstance(项, 探索条目):
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"清单第 {位} 项不是探索条目对象:{项!r}")
|
||||
顺序 = 项.顺序
|
||||
if isinstance(顺序, bool) or not isinstance(顺序, int):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"调用 {项.调用ID} 的顺序必须是整数,收到 {顺序!r}"
|
||||
)
|
||||
if 顺序 != 位:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID",
|
||||
f"清单顺序断裂:第 {位} 项的顺序是 {顺序};顺序必须从 1 起连续",
|
||||
)
|
||||
if not isinstance(项.调用ID, str) or not 项.调用ID:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"清单第 {位} 项缺少非空调用ID")
|
||||
if 项.调用ID in 已见:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"调用ID 重复:{项.调用ID}")
|
||||
已见.add(项.调用ID)
|
||||
if 项.工具名 not in 已知工具:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"未知工具 {项.工具名!r},调用 {项.调用ID} 不能回放"
|
||||
)
|
||||
if not isinstance(项.参数, dict):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"调用 {项.调用ID} 的参数必须是对象:{项.参数!r}"
|
||||
)
|
||||
_校验来源绑定(项)
|
||||
for 项 in 清单:
|
||||
_校验参数(项, 冻结范围)
|
||||
|
||||
|
||||
def _校验来源绑定(项: 探索条目) -> None:
|
||||
if not isinstance(项.来源引用, tuple) or not 项.来源引用:
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", f"调用 {项.调用ID} 缺少来源绑定,清单不可回放")
|
||||
for 绑 in 项.来源引用:
|
||||
if not isinstance(绑, 来源绑定):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"调用 {项.调用ID} 的来源引用不是来源绑定对象:{绑!r}"
|
||||
)
|
||||
for 名, 值 in (
|
||||
("来源ID", 绑.来源ID),
|
||||
("数据版本", 绑.数据版本),
|
||||
("结构哈希", 绑.结构哈希),
|
||||
("投影版本", 绑.投影版本),
|
||||
):
|
||||
if not isinstance(值, str) or not 值:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID", f"调用 {项.调用ID} 来源绑定的 {名} 必须是非空字符串"
|
||||
)
|
||||
消费哈希 = 项.消费哈希
|
||||
if (
|
||||
not isinstance(消费哈希, str)
|
||||
or len(消费哈希) != 64
|
||||
or any(c not in "0123456789abcdef" for c in 消费哈希)
|
||||
):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID",
|
||||
f"调用 {项.调用ID} 的消费哈希必须是 64 位十六进制,收到 {消费哈希!r}",
|
||||
)
|
||||
|
||||
|
||||
def _校验参数(项: 探索条目, 冻结范围: dict) -> None:
|
||||
未授权 = (
|
||||
(项.工具名 == "read_history_document" and not 冻结范围.get("documents"))
|
||||
or (项.工具名 == "query_work_plan" and not isinstance(冻结范围.get("target_plan"), dict))
|
||||
or (项.工具名 == "query_facts_as_of" and not isinstance(冻结范围.get("facts"), dict))
|
||||
)
|
||||
if 未授权:
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED",
|
||||
f"工具 {项.工具名} 不在任务冻结范围内(对应来源未冻结),调用 {项.调用ID} 不能回放",
|
||||
)
|
||||
扩权 = 扩权字段 & 项.参数.keys()
|
||||
if 扩权:
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED",
|
||||
f"调用 {项.调用ID} 携带扩权字段 {sorted(扩权)};工具参数不得改变任务身份或范围",
|
||||
)
|
||||
try:
|
||||
Draft202012Validator(工具合同[项.工具名]).validate(项.参数)
|
||||
except ValidationError as 错误:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID",
|
||||
f"调用 {项.调用ID} 的参数不符合固定查询合同:{错误.message}",
|
||||
) from None
|
||||
if 项.工具名 == "read_history_document":
|
||||
键 = (项.参数["chapter_id"], 项.参数.get("branch_id", "main"), 项.参数["revision"])
|
||||
冻结 = {
|
||||
(d["chapter_id"], d["branch_id"], d["revision"]) for d in 冻结范围.get("documents", ())
|
||||
}
|
||||
if 键 not in 冻结:
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED",
|
||||
f"文稿(章 {键[0]}、分支 {键[1]}、版本 {键[2]})不在冻结范围内,"
|
||||
f"调用 {项.调用ID} 不得指定未冻结文稿或版本",
|
||||
)
|
||||
elif 项.工具名 == "query_facts_as_of":
|
||||
视角 = 项.参数.get("view", "author")
|
||||
知情主体 = 项.参数.get("knower_id")
|
||||
if (视角 == "character") != bool(知情主体):
|
||||
raise 上下文错误(
|
||||
"CONTEXT_INPUT_INVALID",
|
||||
f"调用 {项.调用ID} 的角色视角必须携带知情主体,其他视角不得携带",
|
||||
)
|
||||
|
||||
|
||||
def 工具范围从冻结(任务ID: str, 冻结范围: dict) -> 工具范围:
|
||||
"""从冻结范围派生回放使用的工具范围;用途缺失或非法即失败关闭。"""
|
||||
运行 = 冻结范围.get("run_purpose")
|
||||
内容 = 冻结范围.get("content_purpose")
|
||||
if 运行 not in 允许执行用途 or 内容 not in 允许内容用途:
|
||||
raise 上下文错误(
|
||||
"CONTEXT_SCOPE_INVALID",
|
||||
f"冻结范围必须绑定有效执行与内容用途,收到 {运行!r}/{内容!r}",
|
||||
)
|
||||
return 工具范围(
|
||||
任务ID,
|
||||
冻结范围["work_id"],
|
||||
tuple(冻结范围["source_ids"]),
|
||||
冻结范围["as_of"],
|
||||
运行,
|
||||
内容,
|
||||
)
|
||||
|
||||
|
||||
def 逐项比对(条目: 探索条目, 结果: 工具结果) -> tuple[str, ...]:
|
||||
"""比对一次重读与清单条目;返回指明调用与来源字段的具体失配说明,空即一致。"""
|
||||
前缀 = f"调用 {条目.调用ID}({条目.工具名})"
|
||||
当前 = {
|
||||
源.来源ID: 来源绑定(源.来源ID, 源.数据版本, 源.结构哈希, 源.投影版本) for 源 in 结果.来源
|
||||
}
|
||||
失配: list[str] = []
|
||||
for 基 in 条目.来源引用:
|
||||
现 = 当前.get(基.来源ID)
|
||||
if 现 is None:
|
||||
失配.append(f"{前缀}:来源 {基.来源ID} 已不在重读结果中")
|
||||
continue
|
||||
for 名, 旧, 新 in (
|
||||
("数据版本", 基.数据版本, 现.数据版本),
|
||||
("结构哈希", 基.结构哈希, 现.结构哈希),
|
||||
("投影版本", 基.投影版本, 现.投影版本),
|
||||
):
|
||||
if 旧 != 新:
|
||||
失配.append(f"{前缀}:来源 {基.来源ID} 的{名}已从 {旧} 变为 {新}")
|
||||
for 身 in sorted(set(当前) - {基.来源ID for 基 in 条目.来源引用}):
|
||||
失配.append(f"{前缀}:重读出现清单外来源 {身}")
|
||||
重读哈希 = 内容哈希(结果.内容)
|
||||
if 重读哈希 != 条目.消费哈希:
|
||||
失配.append(
|
||||
f"{前缀}:内容片段与清单消费哈希不一致(清单 {条目.消费哈希[:8]},重读 {重读哈希[:8]})"
|
||||
)
|
||||
return tuple(失配)
|
||||
|
||||
|
||||
def 装配读取器(
|
||||
数据库: 数据库工厂, 冻结范围: dict, 引用解析=None
|
||||
) -> dict[str, Callable[[工具范围, dict], 工具结果]]:
|
||||
"""按冻结范围实际授权装配具名读取器;未授权来源不伪装可读。"""
|
||||
读取器: dict[str, Callable[[工具范围, dict], 工具结果]] = {}
|
||||
if 冻结范围.get("documents"):
|
||||
|
||||
def 读历史(范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
return 读取历史正文(数据库, 冻结范围, 范围, 参数)
|
||||
|
||||
读取器["read_history_document"] = 读历史
|
||||
if isinstance(冻结范围.get("target_plan"), dict):
|
||||
|
||||
def 读细纲(范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
return 读取目标细纲(数据库, 冻结范围, 范围, 参数, 引用解析)
|
||||
|
||||
读取器["query_work_plan"] = 读细纲
|
||||
if isinstance(冻结范围.get("facts"), dict):
|
||||
|
||||
def 读事实(范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
return 读取时点事实(数据库, 冻结范围, 范围, 参数)
|
||||
|
||||
读取器["query_facts_as_of"] = 读事实
|
||||
return 读取器
|
||||
|
||||
|
||||
def 回放(
|
||||
数据库: 数据库工厂,
|
||||
冻结范围: dict,
|
||||
范围: 工具范围,
|
||||
清单: tuple[探索条目, ...],
|
||||
引用解析=None,
|
||||
) -> 回放结果:
|
||||
"""按清单顺序以相同输入版本确定性重读;任何漂移抛 SOURCE_STALE 并指明项。
|
||||
|
||||
输入是冻结快照或任务内冻结范围加当前授权恢复的工具范围;重读经各来源
|
||||
公开接口以冻结版本执行,不采纳内容、不追加留痕、不调用模型。
|
||||
"""
|
||||
校验清单(清单, 冻结范围)
|
||||
允许 = set(冻结范围["source_ids"])
|
||||
for 项 in 清单:
|
||||
越界 = {绑.来源ID for 绑 in 项.来源引用} - 允许
|
||||
if 越界:
|
||||
raise 上下文错误(
|
||||
"SCOPE_DENIED",
|
||||
f"调用 {项.调用ID} 的来源绑定越出冻结范围:{sorted(越界)}",
|
||||
)
|
||||
读取器 = 装配读取器(数据库, 冻结范围, 引用解析)
|
||||
失效项: list[str] = []
|
||||
重读: list[回放条目] = []
|
||||
for 条目 in 清单:
|
||||
try:
|
||||
结果 = 读取器[条目.工具名](范围, 条目.参数)
|
||||
except Muse错误 as 错误:
|
||||
# 重读途中的业务失败(漂移、撤回或引用失效)都算该项不可按原输入回读
|
||||
失效项.append(f"调用 {条目.调用ID}({条目.工具名})重读失败:{错误.说明}")
|
||||
continue
|
||||
失效项.extend(逐项比对(条目, 结果))
|
||||
重读.append(
|
||||
回放条目(
|
||||
调用ID=条目.调用ID,
|
||||
工具名=条目.工具名,
|
||||
顺序=条目.顺序,
|
||||
内容=结果.内容,
|
||||
来源=tuple(
|
||||
来源绑定(源.来源ID, 源.数据版本, 源.结构哈希, 源.投影版本) for 源 in 结果.来源
|
||||
),
|
||||
消费哈希=内容哈希(结果.内容),
|
||||
)
|
||||
)
|
||||
if 失效项:
|
||||
raise 上下文错误("SOURCE_STALE", "回放失效,以下项已漂移或不可回读:" + ";".join(失效项))
|
||||
return 回放结果(逐项=tuple(重读))
|
||||
|
||||
|
||||
__all__ = [
|
||||
"已知工具",
|
||||
"回放",
|
||||
"工具范围从冻结",
|
||||
"扩权字段",
|
||||
"校验清单",
|
||||
"记录探索",
|
||||
"逐项比对",
|
||||
"装配读取器",
|
||||
]
|
||||
@ -1,25 +1,85 @@
|
||||
"""上下文模块唯一公开入口;跨模块只经本文件导入。"""
|
||||
|
||||
from muse.上下文.任务范围 import 上下文服务, 校验截止
|
||||
from muse.上下文.依赖校验 import 依赖重验检查, 核对依赖
|
||||
from muse.上下文.冻结快照 import (
|
||||
快照身份,
|
||||
快照载荷,
|
||||
文本哈希,
|
||||
输入哈希,
|
||||
)
|
||||
from muse.上下文.探索回放 import (
|
||||
回放,
|
||||
工具范围从冻结,
|
||||
已知工具,
|
||||
校验清单,
|
||||
记录探索,
|
||||
逐项比对,
|
||||
)
|
||||
from muse.上下文.材料装配 import 装配材料
|
||||
from muse.上下文.来源读取 import (
|
||||
事实参数合同,
|
||||
事实工具,
|
||||
历史正文参数合同,
|
||||
历史正文工具,
|
||||
投影合同,
|
||||
投影版本,
|
||||
细纲参数合同,
|
||||
细纲工具,
|
||||
范围漂移说明,
|
||||
读取历史正文,
|
||||
读取时点事实,
|
||||
读取目标细纲,
|
||||
)
|
||||
from muse.上下文.模型 import (
|
||||
上下文快照,
|
||||
上下文错误,
|
||||
依赖状态,
|
||||
依赖锁定,
|
||||
冻结回执,
|
||||
回放条目,
|
||||
回放结果,
|
||||
探索条目,
|
||||
材料装配结果,
|
||||
来源绑定,
|
||||
)
|
||||
from muse.上下文.模型 import 上下文错误, 材料装配结果
|
||||
|
||||
__all__ = [
|
||||
"上下文错误",
|
||||
"上下文服务",
|
||||
"上下文快照",
|
||||
"上下文错误",
|
||||
"依赖重验检查",
|
||||
"依赖锁定",
|
||||
"依赖状态",
|
||||
"事实参数合同",
|
||||
"事实工具",
|
||||
"冻结回执",
|
||||
"已知工具",
|
||||
"回放",
|
||||
"回放条目",
|
||||
"回放结果",
|
||||
"探索条目",
|
||||
"来源绑定",
|
||||
"材料装配结果",
|
||||
"装配材料",
|
||||
"工具范围从冻结",
|
||||
"核对依赖",
|
||||
"历史正文参数合同",
|
||||
"历史正文工具",
|
||||
"范围漂移说明",
|
||||
"投影合同",
|
||||
"投影版本",
|
||||
"快照身份",
|
||||
"快照载荷",
|
||||
"文本哈希",
|
||||
"输入哈希",
|
||||
"读取历史正文",
|
||||
"读取时点事实",
|
||||
"读取目标细纲",
|
||||
"校验截止",
|
||||
"校验清单",
|
||||
"细纲参数合同",
|
||||
"细纲工具",
|
||||
"记录探索",
|
||||
"逐项比对",
|
||||
]
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
"""按 B05/B01 公开接口读取冻结范围内的历史正文;码点切片不做规范化。
|
||||
"""按 B01/B02/B05 公开接口读取冻结范围内的来源;漂移即失效。
|
||||
|
||||
合同依据:模块设计/B09-上下文.md、接口契约/正文结构与中文选区.md。
|
||||
合同依据:模块设计/B09-上下文.md、接口契约/任务工具与事件.md。
|
||||
|
||||
- 只返回冻结文稿、目录版本、正文版本与哈希全部匹配的可见文本;
|
||||
任一漂移返回 SOURCE_STALE,不静默改读最新版。
|
||||
- 投影版本由固定 visible-text-codepoints/v1 合同与两种用途确定性派生,
|
||||
不伪造 S04 动态结构绑定。
|
||||
- 目标章细纲按任务两种用途取 aiContext 真投影,身份漂移即失效;
|
||||
时点事实按冻结系统版本查询,视角只作为收窄查询条件。
|
||||
- 投影版本由固定投影合同与两种用途确定性派生,不伪造 S04 动态结构绑定。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -14,8 +15,10 @@ from psycopg import Connection
|
||||
|
||||
from muse.上下文.模型 import 上下文错误
|
||||
from muse.任务运行.接口 import 工具定义, 工具来源, 工具结果, 工具范围
|
||||
from muse.作品规划.接口 import 作品错误, 读取作品范围
|
||||
from muse.作品规划.接口 import 作品错误, 读取作品范围, 读取细纲投影
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
from muse.故事世界.接口 import 读取当前事实系统版本, 读取时点事实投影
|
||||
from muse.故事世界.模型 import 事实错误
|
||||
from muse.正文写作.接口 import 可见文本, 可见文本哈希, 读取正文依据
|
||||
from muse.正文写作.模型 import 正文错误
|
||||
|
||||
@ -34,12 +37,41 @@ from muse.正文写作.模型 import 正文错误
|
||||
},
|
||||
}
|
||||
|
||||
细纲参数合同 = {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {},
|
||||
}
|
||||
|
||||
事实参数合同 = {
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"properties": {
|
||||
"view": {"enum": ["author", "reader", "character"]},
|
||||
"knower_id": {"type": "string", "minLength": 1},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def 投影版本(运行用途: str, 内容用途: str) -> str:
|
||||
"""由固定投影合同与两种用途确定性派生;同一输入永远同一版本。"""
|
||||
return f"{投影合同}:{运行用途}:{内容用途}"
|
||||
|
||||
|
||||
def _核对目录(连: Connection, 作者: str, 冻结范围: dict) -> dict:
|
||||
"""目录漂移使整个任务范围失效;不静默采用新目录。"""
|
||||
try:
|
||||
目录 = 读取作品范围(连, 作者, 冻结范围["work_id"])
|
||||
except 作品错误 as 错误:
|
||||
raise 上下文错误("SOURCE_STALE", f"作品目录已不可用:{错误.说明}") from None
|
||||
if 目录["revision"] != 冻结范围["directory_revision"]:
|
||||
raise 上下文错误(
|
||||
"SOURCE_STALE",
|
||||
f"作品目录已从版本 {冻结范围['directory_revision']} 漂移;任务范围失效,需重新授权",
|
||||
)
|
||||
return 目录
|
||||
|
||||
|
||||
def 读取历史正文(数据库: 数据库工厂, 冻结范围: dict, 范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
"""只读冻结文稿匹配且目录、版本与哈希一致的可见文本码点片段。"""
|
||||
键 = (参数["chapter_id"], 参数.get("branch_id", "main"), 参数["revision"])
|
||||
@ -90,22 +122,195 @@ def 读取历史正文(数据库: 数据库工厂, 冻结范围: dict, 范围:
|
||||
)
|
||||
|
||||
|
||||
def _核对并读取(
|
||||
连: Connection, 作者: str, 冻结范围: dict, 键: tuple[str, str, int], 目标: dict
|
||||
) -> dict:
|
||||
def 读取目标细纲(
|
||||
数据库: 数据库工厂, 冻结范围: dict, 范围: 工具范围, 参数: dict, 引用解析=None
|
||||
) -> 工具结果:
|
||||
"""按任务用途读取冻结目标章细纲的 aiContext 真投影;身份漂移即失效。"""
|
||||
目标 = 冻结范围.get("target_plan")
|
||||
if not isinstance(目标, dict):
|
||||
raise 上下文错误("SCOPE_DENIED", "任务范围没有冻结目标章细纲")
|
||||
作者 = 冻结范围["authorized_by"]
|
||||
with 数据库.连接(只读=True) as 连:
|
||||
_核对目录(连, 作者, 冻结范围)
|
||||
try:
|
||||
记录 = 读取细纲投影(
|
||||
连,
|
||||
作者,
|
||||
冻结范围["work_id"],
|
||||
目标["chapter_id"],
|
||||
内容用途=范围.内容用途,
|
||||
运行用途=范围.运行用途,
|
||||
引用解析=引用解析,
|
||||
)
|
||||
except 作品错误 as 错误:
|
||||
raise 上下文错误("SOURCE_STALE", f"目标章细纲已不可用:{错误.说明}") from None
|
||||
if (
|
||||
记录["plan_id"],
|
||||
记录["revision"],
|
||||
记录["content_hash"],
|
||||
) != (目标["plan_id"], 目标["revision"], 目标["content_hash"]):
|
||||
raise 上下文错误(
|
||||
"SOURCE_STALE",
|
||||
f"目标章 {目标['chapter_id']} 细纲已从版本 {目标['revision']} 漂移"
|
||||
f"(当前版本 {记录['revision']});任务范围失效,需重新授权",
|
||||
)
|
||||
return 工具结果(
|
||||
内容={
|
||||
"chapter_id": 目标["chapter_id"],
|
||||
"plan_id": 记录["plan_id"],
|
||||
"revision": 记录["revision"],
|
||||
"content": 记录["content"],
|
||||
"omitted_fields": 记录["omitted"],
|
||||
"run_purpose": 范围.运行用途,
|
||||
"content_purpose": 范围.内容用途,
|
||||
},
|
||||
来源=(
|
||||
工具来源(
|
||||
来源ID=记录["plan_id"],
|
||||
数据版本=str(记录["revision"]),
|
||||
结构哈希=记录["content_hash"],
|
||||
投影版本=记录["projection_version"],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def 读取时点事实(数据库: 数据库工厂, 冻结范围: dict, 范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
"""按冻结事实系统版本查询时点事实;视角只收窄不扩权。"""
|
||||
事实 = 冻结范围.get("facts")
|
||||
if not isinstance(事实, dict) or "system_revision" not in 事实:
|
||||
raise 上下文错误("SCOPE_DENIED", "任务范围没有冻结事实系统版本")
|
||||
视角 = 参数.get("view", "author")
|
||||
知情主体 = 参数.get("knower_id")
|
||||
if (视角 == "character") != bool(知情主体):
|
||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "角色视角必须携带知情主体,其他视角不得携带")
|
||||
作者 = 冻结范围["authorized_by"]
|
||||
with 数据库.连接(只读=True) as 连:
|
||||
_核对目录(连, 作者, 冻结范围)
|
||||
当前 = 读取当前事实系统版本(连, 作者, 冻结范围["work_id"])
|
||||
if 当前 != 事实["system_revision"]:
|
||||
raise 上下文错误(
|
||||
"SOURCE_STALE",
|
||||
f"事实系统版本已从 {事实['system_revision']} 前进到 {当前};"
|
||||
"任务范围失效,需重新授权",
|
||||
)
|
||||
try:
|
||||
结果 = 读取时点事实投影(
|
||||
连,
|
||||
作者,
|
||||
冻结范围["work_id"],
|
||||
冻结范围["as_of"],
|
||||
系统版本=事实["system_revision"],
|
||||
视角=视角,
|
||||
知情主体ID=知情主体,
|
||||
内容用途=范围.内容用途,
|
||||
运行用途=范围.运行用途,
|
||||
)
|
||||
except 事实错误 as 错误:
|
||||
raise 上下文错误(错误.错误码, f"时点事实查询被拒绝:{错误.说明}") from None
|
||||
来源 = tuple(
|
||||
工具来源(
|
||||
来源ID=项["object_id"],
|
||||
数据版本=str(项["revision"]),
|
||||
结构哈希=项["content_hash"],
|
||||
投影版本=项["projection_version"],
|
||||
)
|
||||
for 键 in ("facts", "beliefs", "plans")
|
||||
for 项 in 结果[键]
|
||||
)
|
||||
return 工具结果(
|
||||
内容={
|
||||
"as_of": 冻结范围["as_of"],
|
||||
"system_revision": 结果["system_revision"],
|
||||
"view": 视角,
|
||||
"facts": 结果["facts"],
|
||||
"beliefs": 结果["beliefs"],
|
||||
"plans": 结果["plans"],
|
||||
"omitted": 结果["omitted"],
|
||||
"run_purpose": 范围.运行用途,
|
||||
"content_purpose": 范围.内容用途,
|
||||
},
|
||||
来源=来源,
|
||||
)
|
||||
|
||||
|
||||
def 范围漂移说明(连: Connection, 冻结范围: dict, 引用解析=None) -> str | None:
|
||||
"""对照当前来源重查冻结范围;返回首个漂移或不可用说明,未漂移返回 None。
|
||||
|
||||
漂移语义与各读取路径一致:目录、文稿版本与哈希、目标章细纲身份、
|
||||
事实系统版本任一不一致即失效;只描述差异,不改写任何记录。
|
||||
"""
|
||||
作者 = 冻结范围["authorized_by"]
|
||||
try:
|
||||
目录 = 读取作品范围(连, 作者, 冻结范围["work_id"])
|
||||
except 作品错误 as 错误:
|
||||
raise 上下文错误("SOURCE_STALE", f"作品目录已不可用:{错误.说明}") from None
|
||||
return f"作品目录已不可用:{错误.说明}"
|
||||
if 目录["revision"] != 冻结范围["directory_revision"]:
|
||||
return (
|
||||
f"作品目录已从版本 {冻结范围['directory_revision']} 前进到 {目录['revision']};"
|
||||
"冻结范围当前失效,需重新授权"
|
||||
)
|
||||
for 目标 in 冻结范围["documents"]:
|
||||
当前 = next((c for c in 目录["chapters"] if c["chapter_id"] == 目标["chapter_id"]), None)
|
||||
if 当前 is None or 当前["position"] != 目标["position"]:
|
||||
return f"章节 {目标['chapter_id']} 的目录位置已漂移;冻结范围当前失效,需重新授权"
|
||||
try:
|
||||
依据 = 读取正文依据(
|
||||
连, 作者, 目标["chapter_id"], 目标["revision"], 分支=目标["branch_id"]
|
||||
)
|
||||
except 正文错误 as 错误:
|
||||
return f"冻结正文版本已不存在:{错误.说明}"
|
||||
if (
|
||||
依据["current_revision"] != 目标["revision"]
|
||||
or 依据["document_hash"] != 目标["document_hash"]
|
||||
):
|
||||
return (
|
||||
f"文稿 {目标['chapter_id']} 已漂移(当前版本 {依据['current_revision']},"
|
||||
f"冻结版本 {目标['revision']});冻结范围当前失效,需重新授权"
|
||||
)
|
||||
目标章 = 冻结范围.get("target_plan")
|
||||
if isinstance(目标章, dict):
|
||||
try:
|
||||
记录 = 读取细纲投影(
|
||||
连,
|
||||
作者,
|
||||
冻结范围["work_id"],
|
||||
目标章["chapter_id"],
|
||||
内容用途=冻结范围["content_purpose"],
|
||||
运行用途=冻结范围["run_purpose"],
|
||||
引用解析=引用解析,
|
||||
)
|
||||
except 作品错误 as 错误:
|
||||
return f"目标章细纲已不可用:{错误.说明}"
|
||||
if (
|
||||
记录["plan_id"],
|
||||
记录["revision"],
|
||||
记录["content_hash"],
|
||||
) != (目标章["plan_id"], 目标章["revision"], 目标章["content_hash"]):
|
||||
return (
|
||||
f"目标章 {目标章['chapter_id']} 细纲已从版本 {目标章['revision']} 漂移"
|
||||
f"(当前版本 {记录['revision']});冻结范围当前失效,需重新授权"
|
||||
)
|
||||
事实 = 冻结范围.get("facts")
|
||||
if isinstance(事实, dict):
|
||||
当前版本 = 读取当前事实系统版本(连, 作者, 冻结范围["work_id"])
|
||||
if 当前版本 != 事实["system_revision"]:
|
||||
return (
|
||||
f"事实系统版本已从 {事实['system_revision']} 前进到 {当前版本};"
|
||||
"冻结范围当前失效,需重新授权"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _核对并读取(
|
||||
连: Connection, 作者: str, 冻结范围: dict, 键: tuple[str, str, int], 目标: dict
|
||||
) -> dict:
|
||||
目录 = _核对目录(连, 作者, 冻结范围)
|
||||
当前 = next((c for c in 目录["chapters"] if c["chapter_id"] == 键[0]), None)
|
||||
if (
|
||||
目录["revision"] != 冻结范围["directory_revision"]
|
||||
or 当前 is None
|
||||
or 当前["position"] != 目标["position"]
|
||||
):
|
||||
if 当前 is None or 当前["position"] != 目标["position"]:
|
||||
raise 上下文错误(
|
||||
"SOURCE_STALE",
|
||||
f"作品目录已从版本 {冻结范围['directory_revision']} 漂移;任务范围失效,需重新授权",
|
||||
f"章节 {键[0]} 的目录位置已漂移;任务范围失效,需重新授权",
|
||||
)
|
||||
try:
|
||||
依据 = 读取正文依据(连, 作者, 键[0], 键[2], 分支=键[1])
|
||||
@ -120,7 +325,7 @@ def _核对并读取(
|
||||
|
||||
|
||||
def 历史正文工具(数据库: 数据库工厂, 冻结范围: dict) -> 工具定义:
|
||||
"""为已冻结范围构建本轮唯一具名只读工具;参数只含查询条件。"""
|
||||
"""为已冻结范围构建历史正文具名只读工具;参数只含查询条件。"""
|
||||
|
||||
def 读取(范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
return 读取历史正文(数据库, 冻结范围, 范围, 参数)
|
||||
@ -133,4 +338,46 @@ def 历史正文工具(数据库: 数据库工厂, 冻结范围: dict) -> 工具
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["历史正文参数合同", "历史正文工具", "投影合同", "投影版本", "读取历史正文"]
|
||||
def 细纲工具(数据库: 数据库工厂, 冻结范围: dict, 引用解析=None) -> 工具定义:
|
||||
"""为冻结目标章构建细纲具名只读工具;无参数,身份只来自冻结范围。"""
|
||||
|
||||
def 读取(范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
return 读取目标细纲(数据库, 冻结范围, 范围, 参数, 引用解析)
|
||||
|
||||
return 工具定义(
|
||||
"query_work_plan",
|
||||
"读取任务冻结的目标章细纲 aiContext 投影;目标章身份与版本只来自任务范围",
|
||||
细纲参数合同,
|
||||
读取,
|
||||
)
|
||||
|
||||
|
||||
def 事实工具(数据库: 数据库工厂, 冻结范围: dict) -> 工具定义:
|
||||
"""为冻结事实版本构建时点事实具名只读工具;视角只收窄查询。"""
|
||||
|
||||
def 读取(范围: 工具范围, 参数: dict) -> 工具结果:
|
||||
return 读取时点事实(数据库, 冻结范围, 范围, 参数)
|
||||
|
||||
return 工具定义(
|
||||
"query_facts_as_of",
|
||||
"按任务冻结的叙事截止与事实系统版本查询时点事实 aiContext 投影;"
|
||||
"view 可选 author/reader/character,character 需 knower_id",
|
||||
事实参数合同,
|
||||
读取,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"历史正文参数合同",
|
||||
"历史正文工具",
|
||||
"范围漂移说明",
|
||||
"细纲参数合同",
|
||||
"细纲工具",
|
||||
"事实参数合同",
|
||||
"事实工具",
|
||||
"投影合同",
|
||||
"投影版本",
|
||||
"读取历史正文",
|
||||
"读取目标细纲",
|
||||
"读取时点事实",
|
||||
]
|
||||
|
||||
@ -3,9 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from muse.共享.错误 import Muse错误
|
||||
|
||||
# 两种用途的固定取值;授权、回放与依赖核对共用同一口径。
|
||||
允许执行用途 = frozenset({"production", "evaluation", "maintenance"})
|
||||
允许内容用途 = frozenset({"planning", "generation", "detection", "extraction"})
|
||||
|
||||
|
||||
class 上下文错误(Muse错误):
|
||||
"""上下文输入非法、必读缺项或必读超预算的稳定错误。
|
||||
@ -29,4 +34,102 @@ class 材料装配结果:
|
||||
省略: tuple[tuple[str, str], ...]
|
||||
|
||||
|
||||
__all__ = ["上下文错误", "材料装配结果"]
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 来源绑定:
|
||||
"""一次实际消费中单个来源的身份与版本四元组;与工具读取留痕同键。"""
|
||||
|
||||
来源ID: str
|
||||
数据版本: str
|
||||
结构哈希: str
|
||||
投影版本: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 冻结回执:
|
||||
"""冻结上下文快照的回执;同一任务同一输入重复冻结返回同一身份。"""
|
||||
|
||||
快照ID: str
|
||||
输入哈希: str
|
||||
新建: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 上下文快照:
|
||||
"""按身份或任务回读的不可变消费快照;当前状态只标注、不改写历史。"""
|
||||
|
||||
快照ID: str
|
||||
任务ID: str
|
||||
作品ID: str
|
||||
输入哈希: str
|
||||
冻结范围: dict
|
||||
装配: dict
|
||||
来源绑定: tuple[来源绑定, ...]
|
||||
当前状态: str
|
||||
失效说明: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 探索条目:
|
||||
"""清单单条:一次具名只读调用的身份、参数、顺序、来源绑定与消费哈希。"""
|
||||
|
||||
调用ID: str
|
||||
工具名: str
|
||||
顺序: int
|
||||
参数: dict
|
||||
来源引用: tuple[来源绑定, ...]
|
||||
消费哈希: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 回放条目:
|
||||
"""回放单条重读结果;与清单条目同序,内容与哈希可比对。"""
|
||||
|
||||
调用ID: str
|
||||
工具名: str
|
||||
顺序: int
|
||||
内容: Any
|
||||
来源: tuple[来源绑定, ...]
|
||||
消费哈希: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 回放结果:
|
||||
"""一次确定性回放的逐项重读;任何漂移在产生处抛 SOURCE_STALE 并指明项。"""
|
||||
|
||||
逐项: tuple[回放条目, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 依赖锁定:
|
||||
"""依赖核对中单个来源的当前版本绑定;哈希与投影按来源种类可有空串。"""
|
||||
|
||||
身份: str
|
||||
种类: str
|
||||
版本: str
|
||||
哈希: str
|
||||
投影版本: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class 依赖状态:
|
||||
"""冻结来源当前有效性核对结果;有效附各来源当前版本绑定,无效附具体变化项。"""
|
||||
|
||||
有效: bool
|
||||
锁定依据: tuple[依赖锁定, ...]
|
||||
变化项: tuple[str, ...]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"上下文错误",
|
||||
"上下文快照",
|
||||
"依赖锁定",
|
||||
"依赖状态",
|
||||
"允许执行用途",
|
||||
"允许内容用途",
|
||||
"回放条目",
|
||||
"回放结果",
|
||||
"探索条目",
|
||||
"材料装配结果",
|
||||
"冻结回执",
|
||||
"来源绑定",
|
||||
]
|
||||
|
||||
@ -197,6 +197,7 @@ def 核对可用规划(
|
||||
严格: bool = True,
|
||||
引用解析=None,
|
||||
消费用途: str = "planning",
|
||||
运行用途: str = "production",
|
||||
) -> tuple:
|
||||
id_ = 记录["plan_id"]
|
||||
if id_ in 祖先:
|
||||
@ -234,7 +235,7 @@ def 核对可用规划(
|
||||
规划授权(作者, req.work_id),
|
||||
字段用途名="aiContext",
|
||||
内容用途=消费用途,
|
||||
运行用途="production",
|
||||
运行用途=运行用途,
|
||||
)
|
||||
try:
|
||||
校验值(schema, 投影值(projection, 记录["content"]))
|
||||
|
||||
@ -26,6 +26,7 @@ from muse.作品规划.模型 import (
|
||||
)
|
||||
from muse.作品规划.目录调整 import 目录依赖, 目录入口, 目录参与者, 节点目录入口, 读取作者目录
|
||||
from muse.作品规划.规划版本 import 登记规划参与者, 规划服务
|
||||
from muse.作品规划.近期细化 import 读取细纲投影
|
||||
from muse.作品规划.进度查询 import 创作进度服务
|
||||
from muse.元数据.接口 import 元数据服务, 投影值, 投影字段, 生成结构合同
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
@ -282,6 +283,7 @@ __all__ = [
|
||||
"规划保存",
|
||||
"规划定位",
|
||||
"规划服务",
|
||||
"读取细纲投影",
|
||||
"目录节点",
|
||||
"节点目录保存",
|
||||
]
|
||||
|
||||
@ -1,17 +1,85 @@
|
||||
"""近期细纲只复用当前有效的已确认规划;新细化由持久任务执行。"""
|
||||
"""近期细纲只复用当前有效的已确认规划;新细化由持久任务执行。
|
||||
|
||||
from muse.作品规划.分层规划 import 核对可用规划
|
||||
from muse.作品规划.存储 import 规划存储
|
||||
from muse.作品规划.模型 import 作品错误
|
||||
读取细纲投影是 B01 对 B09 的任务用途真投影读取:aiContext 字段按
|
||||
任务的内容用途与运行用途裁剪,不用 uiVisible 结果顶替。
|
||||
"""
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from muse.作品规划.分层规划 import 核对可用规划, 规划授权
|
||||
from muse.作品规划.存储 import 规划存储, 解析规划载荷
|
||||
from muse.作品规划.模型 import 作品错误, 规划保存
|
||||
from muse.作品规划.目录调整 import 读取作者目录
|
||||
from muse.元数据.接口 import 元数据服务, 投影值, 投影字段
|
||||
|
||||
|
||||
def 读取已确认细纲(连, 作者: str, work_id: str, chapter_id: str, 引用解析=None) -> dict:
|
||||
def 读取已确认细纲(
|
||||
连,
|
||||
作者: str,
|
||||
work_id: str,
|
||||
chapter_id: str,
|
||||
引用解析=None,
|
||||
*,
|
||||
消费用途: str = "generation",
|
||||
运行用途: str = "production",
|
||||
) -> dict:
|
||||
目录 = 读取作者目录(连, 作者, work_id)
|
||||
if chapter_id not in {c["chapter_id"] for c in 目录["chapters"]}:
|
||||
raise 作品错误("SCOPE_DENIED", "目标章节不属于本书")
|
||||
item = 规划存储(连).查章规划(work_id, chapter_id, "fine_outline")
|
||||
if item is None:
|
||||
raise 作品错误("PRECONDITION_PENDING", "目标章节还没有已确认细纲")
|
||||
核对可用规划(连, 作者, item, 引用解析=引用解析, 消费用途="generation")
|
||||
核对可用规划(连, 作者, item, 引用解析=引用解析, 消费用途=消费用途, 运行用途=运行用途)
|
||||
return item
|
||||
|
||||
|
||||
def 读取细纲投影(
|
||||
连,
|
||||
作者: str,
|
||||
work_id: str,
|
||||
chapter_id: str,
|
||||
*,
|
||||
内容用途: str,
|
||||
运行用途: str,
|
||||
引用解析=None,
|
||||
) -> dict:
|
||||
"""按任务两种用途返回目标章已确认细纲的 aiContext 真投影与身份。
|
||||
|
||||
可用性核对与投影使用同一组用途;必需字段被用途裁剪时明确拒绝,
|
||||
不返回缺失必填的半份细纲。
|
||||
"""
|
||||
记录 = 读取已确认细纲(
|
||||
连,
|
||||
作者,
|
||||
work_id,
|
||||
chapter_id,
|
||||
引用解析=引用解析,
|
||||
消费用途=内容用途,
|
||||
运行用途=运行用途,
|
||||
)
|
||||
请求 = TypeAdapter(规划保存).validate_python(记录["request"])
|
||||
元数据 = 元数据服务(连)
|
||||
结构 = 元数据.读取有效结构(
|
||||
请求.schema.schema_id,
|
||||
请求.schema.base_version,
|
||||
work_id=work_id,
|
||||
extension_version=请求.schema.extension_version,
|
||||
)
|
||||
投影 = 投影字段(
|
||||
结构,
|
||||
元数据.当前策略(记录["type_id"]),
|
||||
规划授权(作者, work_id),
|
||||
字段用途名="aiContext",
|
||||
内容用途=内容用途,
|
||||
运行用途=运行用途,
|
||||
)
|
||||
return {
|
||||
"chapter_id": chapter_id,
|
||||
"plan_id": 记录["plan_id"],
|
||||
"revision": 记录["revision"],
|
||||
"content_hash": 记录["content_hash"],
|
||||
"schema_binding": 记录["schema_binding"],
|
||||
"projection_version": 投影.projection_version,
|
||||
"content": 投影值(投影, 解析规划载荷(记录["content"])),
|
||||
"omitted": dict(投影.剔除原因),
|
||||
}
|
||||
|
||||
@ -196,7 +196,7 @@ def 构建(配置: 应用配置, *, 时钟: 时钟 | None = None, 流程: 流程
|
||||
作品进度=创作进度服务(作品, 正文, 任务),
|
||||
故事世界=故事服务(数据库, 正式),
|
||||
规划=规划服务(数据库, 正式, 解析本书规划引用),
|
||||
上下文=上下文服务(数据库, 任务),
|
||||
上下文=上下文服务(数据库, 任务, 解析本书规划引用),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ from muse.基础设施.可观测性 import 记录运行
|
||||
from muse.接入.http.事件订阅 import 路由 as 事件路由
|
||||
from muse.接入.http.作者会话 import 会话路由
|
||||
from muse.接入.http.健康检查 import 路由 as 健康路由
|
||||
from muse.接入.http.路由.上下文 import 路由 as 上下文路由
|
||||
from muse.接入.http.路由.任务运行 import 路由 as 任务路由
|
||||
from muse.接入.http.路由.作品规划 import 路由 as 作品路由
|
||||
from muse.接入.http.路由.作者审阅 import 路由 as 审阅路由
|
||||
@ -77,6 +78,7 @@ def 创建应用(配置: 应用配置) -> FastAPI:
|
||||
def 注册API(应用: FastAPI) -> None:
|
||||
"""运行和合同生成共享同一组路由;不创建连接或加载凭据。"""
|
||||
应用.include_router(任务路由)
|
||||
应用.include_router(上下文路由)
|
||||
应用.include_router(元数据路由)
|
||||
应用.include_router(作品路由)
|
||||
应用.include_router(正文路由)
|
||||
|
||||
113
src/muse/接入/http/路由/上下文.py
Normal file
113
src/muse/接入/http/路由/上下文.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""任务上下文快照的只读 HTTP 外壳:材料清单、来源证据与失效说明。
|
||||
|
||||
合同依据:文件设计/后端-装配与接入.md、模块设计/B09-上下文.md。
|
||||
|
||||
- 只读消费回合1–3 的上下文服务:不新建、不改写快照,不暴露装配全文。
|
||||
- 整体状态与失效说明来自快照自身标注;具体变化项与逐来源当前状态
|
||||
来自依赖核对,历史装配与绑定原样回读。
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from pydantic import BaseModel
|
||||
|
||||
from muse.上下文.接口 import 上下文快照, 依赖状态, 核对依赖
|
||||
from muse.接入.http.作者会话 import 作者依赖
|
||||
from muse.接入.http.依赖 import 读取作者任务
|
||||
|
||||
路由 = APIRouter(prefix="/api/v1/tasks", tags=["上下文"])
|
||||
|
||||
|
||||
class 省略条目响应(BaseModel):
|
||||
source: str
|
||||
reason: str
|
||||
|
||||
|
||||
class 装配清单响应(BaseModel):
|
||||
included_sources: list[str]
|
||||
text_hash: str
|
||||
byte_size: int
|
||||
omissions: list[省略条目响应]
|
||||
|
||||
|
||||
class 来源证据响应(BaseModel):
|
||||
source_id: str
|
||||
data_version: str
|
||||
schema_hash: str
|
||||
projection_version: str
|
||||
current_state: str
|
||||
|
||||
|
||||
class 快照响应(BaseModel):
|
||||
snapshot_id: str
|
||||
task_id: str
|
||||
work_id: str
|
||||
input_hash: str
|
||||
state: str
|
||||
invalidation: str | None
|
||||
changes: list[str]
|
||||
assembly: 装配清单响应
|
||||
sources: list[来源证据响应]
|
||||
|
||||
|
||||
def 呈现快照(快照: 上下文快照, 状态: 依赖状态) -> 快照响应:
|
||||
锁定身份 = {锁.身份 for 锁 in 状态.锁定依据}
|
||||
文稿身份 = {项["document_id"] for 项 in 快照.冻结范围.get("documents", ())}
|
||||
计划身份 = (快照.冻结范围.get("target_plan") or {}).get("plan_id")
|
||||
事实锁定 = f"{快照.作品ID}:事实系统" in 锁定身份
|
||||
来源: list[来源证据响应] = []
|
||||
for 绑定 in 快照.来源绑定:
|
||||
if 状态.有效:
|
||||
当前 = "有效"
|
||||
elif 绑定.来源ID == 计划身份 or 绑定.来源ID in 文稿身份:
|
||||
当前 = "有效" if 绑定.来源ID in 锁定身份 else "SOURCE_STALE"
|
||||
else:
|
||||
# 事实对象没有逐对象锁定;随事实系统版本锁定整体标注。
|
||||
当前 = "有效" if 事实锁定 else "SOURCE_STALE"
|
||||
来源.append(
|
||||
来源证据响应(
|
||||
source_id=绑定.来源ID,
|
||||
data_version=绑定.数据版本,
|
||||
schema_hash=绑定.结构哈希,
|
||||
projection_version=绑定.投影版本,
|
||||
current_state=当前,
|
||||
)
|
||||
)
|
||||
return 快照响应(
|
||||
snapshot_id=快照.快照ID,
|
||||
task_id=快照.任务ID,
|
||||
work_id=快照.作品ID,
|
||||
input_hash=快照.输入哈希,
|
||||
state=快照.当前状态,
|
||||
invalidation=快照.失效说明,
|
||||
changes=list(状态.变化项),
|
||||
assembly=装配清单响应(
|
||||
included_sources=list(快照.装配.get("入选", ())),
|
||||
text_hash=快照.装配.get("文本哈希", ""),
|
||||
byte_size=快照.装配.get("字节数", 0),
|
||||
omissions=[
|
||||
省略条目响应(source=项[0], reason=项[1]) for 项 in 快照.装配.get("省略", ())
|
||||
],
|
||||
),
|
||||
sources=来源,
|
||||
)
|
||||
|
||||
|
||||
@路由.get(
|
||||
"/{task_id}/context-snapshots",
|
||||
response_model=list[快照响应],
|
||||
operation_id="list_context_snapshots",
|
||||
)
|
||||
def 列出任务快照(
|
||||
task_id: str, request: Request, 作者: 作者依赖, work_id: str | None = None
|
||||
) -> list[快照响应]:
|
||||
读取作者任务(request, task_id, work_id)
|
||||
服务 = request.app.state.装配.要求上下文()
|
||||
快照集 = 服务.读取任务快照(task_id)
|
||||
with 服务.数据库.连接(只读=True) as 连:
|
||||
return [
|
||||
呈现快照(
|
||||
快照,
|
||||
核对依赖(连, 快照.冻结范围, 服务.规划引用解析, 基准绑定=快照.来源绑定),
|
||||
)
|
||||
for 快照 in 快照集
|
||||
]
|
||||
@ -2,29 +2,37 @@
|
||||
|
||||
合同依据:文件设计/后端-装配与接入.md、接口契约/任务工具与事件.md。
|
||||
- 工具范围完全取自任务冻结输入;本文件不重新解释作者、作品或截止。
|
||||
- 只安装已实现的历史正文工具;未实现的方法、资料等工具不伪返回成功。
|
||||
- 只安装任务范围实际授权的工具:历史文稿、目标章细纲与时点事实;
|
||||
未实现的方法、资料等工具不伪返回成功。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from muse.上下文.接口 import 上下文服务, 历史正文工具
|
||||
from muse.任务运行.接口 import 事件类型, 任务服务, 只读工具集, 工具范围, 领取凭证
|
||||
from muse.上下文.接口 import 上下文服务, 事实工具, 历史正文工具, 细纲工具
|
||||
from muse.任务运行.接口 import 事件类型, 任务服务, 只读工具集, 工具定义, 工具范围, 领取凭证
|
||||
|
||||
|
||||
def 领取只读工具(上下文: 上下文服务, 运行: 任务服务, 领取: 领取凭证) -> 只读工具集:
|
||||
"""按任务冻结范围安装已实现工具;成功读取复用 S02 工具读取事件留痕。"""
|
||||
"""按任务冻结范围安装已授权工具;成功读取复用 S02 工具读取事件留痕。"""
|
||||
任务 = 运行.读取任务(领取.任务ID)
|
||||
冻结 = 上下文.恢复冻结范围(任务)
|
||||
步骤 = next(s for s in 任务.流程.步骤 if s.步骤ID == 领取.步骤ID)
|
||||
固定 = 任务.冻结输入
|
||||
登记: tuple[工具定义, ...] = ()
|
||||
if 冻结["documents"]:
|
||||
登记 += (历史正文工具(上下文.数据库, 冻结),)
|
||||
if isinstance(冻结.get("target_plan"), dict):
|
||||
登记 += (细纲工具(上下文.数据库, 冻结, 上下文.规划引用解析),)
|
||||
if isinstance(冻结.get("facts"), dict):
|
||||
登记 += (事实工具(上下文.数据库, 冻结),)
|
||||
|
||||
def 记录(载荷: dict) -> None:
|
||||
运行.追加事件(领取, 事件类型.工具读取, 载荷, 事件ID=str(uuid4()))
|
||||
|
||||
return 只读工具集(
|
||||
(历史正文工具(上下文.数据库, 冻结),),
|
||||
登记,
|
||||
步骤.工具,
|
||||
工具范围(
|
||||
任务.任务ID,
|
||||
|
||||
@ -18,7 +18,7 @@ from muse.故事世界.事实提案 import (
|
||||
解析类型选择,
|
||||
)
|
||||
from muse.故事世界.存储 import 故事存储
|
||||
from muse.故事世界.时点查询 import 查询世界状态
|
||||
from muse.故事世界.时点查询 import 按位置查询世界, 查询世界状态
|
||||
from muse.故事世界.来源有效性 import 当前依据可用, 读取依据状态
|
||||
from muse.故事世界.模型 import (
|
||||
事实定位,
|
||||
@ -218,10 +218,51 @@ __all__ = [
|
||||
"知情记录",
|
||||
"故事服务",
|
||||
"登记故事参与者",
|
||||
"读取时点事实投影",
|
||||
"读取当前事实系统版本",
|
||||
"解析本书规划引用",
|
||||
]
|
||||
|
||||
|
||||
def 读取时点事实投影(
|
||||
连,
|
||||
作者: str,
|
||||
work_id: str,
|
||||
截止位置: int,
|
||||
*,
|
||||
系统版本: int | None = None,
|
||||
视角: str = "author",
|
||||
知情主体ID: str | None = None,
|
||||
内容用途: str,
|
||||
运行用途: str,
|
||||
) -> dict:
|
||||
"""上下文按任务用途读取时点事实的 aiContext 真投影;跨模块只走本入口。"""
|
||||
if isinstance(截止位置, bool) or not isinstance(截止位置, int) or 截止位置 < 0:
|
||||
raise 事实错误("FACT_TIME_INVALID", f"事实查询截止位置必须是非负整数,收到 {截止位置!r}")
|
||||
目录 = 读取作品范围(连, 作者, work_id)
|
||||
章序 = {c["chapter_id"]: c["position"] for c in 目录["chapters"]}
|
||||
return 按位置查询世界(
|
||||
连,
|
||||
作者,
|
||||
work_id,
|
||||
章序,
|
||||
目录["revision"],
|
||||
截止位置,
|
||||
系统版本=系统版本,
|
||||
视角=视角,
|
||||
知情主体ID=知情主体ID,
|
||||
字段用途名="aiContext",
|
||||
内容用途=内容用途,
|
||||
运行用途=运行用途,
|
||||
)
|
||||
|
||||
|
||||
def 读取当前事实系统版本(连, 作者: str, work_id: str) -> int:
|
||||
"""供上下文校验冻结事实版本是否仍是当前确认版本。"""
|
||||
读取作品范围(连, 作者, work_id)
|
||||
return 故事存储(连).当前系统版本(work_id)
|
||||
|
||||
|
||||
def 解析本书规划引用(连, 作者: str, work_id: str, 需求, *, 必须有效: bool) -> tuple:
|
||||
"""规划只取得本书对象身份和必要依赖,不越权读取参考实例或自行确认实体。"""
|
||||
读取作品范围(连, 作者, work_id)
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
"""事件生效、披露时点与确认版本分开;不改变事实的保存状态。"""
|
||||
"""事件生效、披露时点与确认版本分开;不改变事实的保存状态。
|
||||
|
||||
查询用途参数化:工作台沿用 uiVisible 投影,任务上下文按冻结用途
|
||||
取 aiContext 真投影;两种读取共用同一套时点与来源有效性判定。
|
||||
"""
|
||||
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
@ -22,13 +26,45 @@ def 查询世界状态(
|
||||
视角: str = "author",
|
||||
知情主体ID: str | None = None,
|
||||
) -> dict:
|
||||
if 视角 not in {"author", "reader", "character"} or ((视角 == "character") != bool(知情主体ID)):
|
||||
raise 事实错误("FACT_SCOPE_INVALID", "查询需要明确的作者、读者或角色视角")
|
||||
目录 = 读取作品范围(连, 作者, work_id)
|
||||
章序 = {c["chapter_id"]: c["position"] for c in 目录["chapters"]}
|
||||
if 截止章节 is not None and 截止章节 not in 章序:
|
||||
raise 事实错误("FACT_TIME_INVALID", "查询截止位置不属于本书")
|
||||
截止 = 章序[截止章节] if 截止章节 is not None else max(章序.values(), default=0)
|
||||
return 按位置查询世界(
|
||||
连,
|
||||
作者,
|
||||
work_id,
|
||||
章序,
|
||||
目录["revision"],
|
||||
截止,
|
||||
系统版本=系统版本,
|
||||
视角=视角,
|
||||
知情主体ID=知情主体ID,
|
||||
字段用途名="uiVisible",
|
||||
内容用途="planning",
|
||||
运行用途="production",
|
||||
)
|
||||
|
||||
|
||||
def 按位置查询世界(
|
||||
连,
|
||||
作者: str,
|
||||
work_id: str,
|
||||
章序: dict,
|
||||
目录版本: int,
|
||||
截止: int,
|
||||
*,
|
||||
系统版本: int | None = None,
|
||||
视角: str = "author",
|
||||
知情主体ID: str | None = None,
|
||||
字段用途名: str = "uiVisible",
|
||||
内容用途: str = "planning",
|
||||
运行用途: str = "production",
|
||||
) -> dict:
|
||||
"""按目录位置截止查询时点记录;投影按传入用途真实裁剪。"""
|
||||
if 视角 not in {"author", "reader", "character"} or ((视角 == "character") != bool(知情主体ID)):
|
||||
raise 事实错误("FACT_SCOPE_INVALID", "查询需要明确的作者、读者或角色视角")
|
||||
存储 = 故事存储(连)
|
||||
当前系统版 = 存储.当前系统版本(work_id)
|
||||
系统版 = 当前系统版 if 系统版本 is None else 系统版本
|
||||
@ -45,7 +81,7 @@ def 查询世界状态(
|
||||
raise 事实错误("SCOPE_DENIED", "知情主体在查询时点不可用")
|
||||
结果 = {
|
||||
"work_id": work_id,
|
||||
"directory_revision": 目录["revision"],
|
||||
"directory_revision": 目录版本,
|
||||
"system_revision": 系统版,
|
||||
"facts": [],
|
||||
"beliefs": [],
|
||||
@ -89,9 +125,9 @@ def 查询世界状态(
|
||||
结构,
|
||||
元数据.当前策略(行["type_id"]),
|
||||
事实授权(作者, work_id),
|
||||
字段用途名="uiVisible",
|
||||
内容用途="planning",
|
||||
运行用途="production",
|
||||
字段用途名=字段用途名,
|
||||
内容用途=内容用途,
|
||||
运行用途=运行用途,
|
||||
)
|
||||
item = {
|
||||
"object_id": id_,
|
||||
@ -104,6 +140,7 @@ def 查询世界状态(
|
||||
"independent": 行["independent"],
|
||||
"content_hash": 行["content_hash"],
|
||||
"projection_version": 投影.projection_version,
|
||||
"omitted_fields": dict(投影.剔除原因),
|
||||
"schema_binding": 行["schema_binding"],
|
||||
"sources": [
|
||||
{k: s[k] for k in ("chapter_id", "branch_id", "revision", "document_hash")}
|
||||
|
||||
138
tests/单元/test_上下文任务范围形状.py
Normal file
138
tests/单元/test_上下文任务范围形状.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""任务范围新节(目标章细纲、事实系统版本与用途绑定)的离线形状校验。
|
||||
|
||||
只测恢复冻结范围对冻结输入的判定:形状与身份不一致必须失败关闭,
|
||||
不连数据库、不调用模型。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from muse.上下文.接口 import 上下文服务, 上下文错误
|
||||
from muse.任务运行.模型 import 任务快照, 任务状态, 执行计划
|
||||
from muse.共享.调用身份 import 用途
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
from muse.配置 import 数据库引用
|
||||
|
||||
|
||||
def 服务() -> 上下文服务:
|
||||
工厂 = 数据库工厂(数据库引用("环境变量", "MUSE_TEST_DATABASE_URL"), 用途.生产)
|
||||
return 上下文服务(工厂, None)
|
||||
|
||||
|
||||
def 快照(范围: dict) -> 任务快照:
|
||||
return 任务快照(
|
||||
"task-shape",
|
||||
任务状态.待运行,
|
||||
"ctx-author",
|
||||
"hash",
|
||||
{
|
||||
"作品ID": 范围["work_id"],
|
||||
"执行用途": "production",
|
||||
"内容用途": "generation",
|
||||
"冻结上下文": {"source_scope": 范围},
|
||||
},
|
||||
执行计划("shape-flow", "1", ()),
|
||||
(),
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def 文稿(编号: str) -> dict:
|
||||
return {
|
||||
"chapter_id": 编号,
|
||||
"branch_id": "main",
|
||||
"revision": 1,
|
||||
"document_id": "doc-" + 编号,
|
||||
"document_hash": "hash-" + 编号,
|
||||
"position": 1,
|
||||
}
|
||||
|
||||
|
||||
def 任务范围() -> dict:
|
||||
"""含历史文稿、目标章细纲与事实系统版本的完整任务范围。"""
|
||||
return {
|
||||
"work_id": "ctx-work",
|
||||
"as_of": 2,
|
||||
"source_ids": ["doc-ch-1", "plan-target", "obj-fact"],
|
||||
"documents": [文稿("ch-1")],
|
||||
"directory_revision": 3,
|
||||
"authorized_by": "ctx-author",
|
||||
"run_purpose": "production",
|
||||
"content_purpose": "generation",
|
||||
"target_plan": {
|
||||
"chapter_id": "ch-3",
|
||||
"plan_id": "plan-target",
|
||||
"revision": 1,
|
||||
"content_hash": "plan-hash",
|
||||
},
|
||||
"facts": {"system_revision": 2},
|
||||
}
|
||||
|
||||
|
||||
def test_完整任务范围原样恢复__d1a2f3() -> None:
|
||||
范围 = 任务范围()
|
||||
assert 服务().恢复冻结范围(快照(范围)) == 范围
|
||||
|
||||
|
||||
def test_无历史文稿但绑定细纲与事实仍可恢复__e4b5c6() -> None:
|
||||
范围 = {**任务范围(), "documents": [], "source_ids": ["plan-target", "obj-fact"]}
|
||||
assert 服务().恢复冻结范围(快照(范围)) == 范围
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"改动",
|
||||
[
|
||||
{"target_plan": {"chapter_id": "ch-3", "plan_id": "plan-target", "revision": 0}},
|
||||
{"target_plan": {"chapter_id": "ch-3", "plan_id": "plan-target", "revision": True}},
|
||||
{"target_plan": {"chapter_id": "", "plan_id": "plan-target", "revision": 1}},
|
||||
{"facts": {"system_revision": -1}},
|
||||
{"facts": {"system_revision": True}},
|
||||
{"facts": {}},
|
||||
{"documents": [], "target_plan": None, "facts": None},
|
||||
],
|
||||
ids=[
|
||||
"zero-plan-revision",
|
||||
"boolean-plan-revision",
|
||||
"empty-chapter",
|
||||
"negative-system-revision",
|
||||
"boolean-system-revision",
|
||||
"missing-system-revision",
|
||||
"empty-scope",
|
||||
],
|
||||
)
|
||||
def test_非法细纲事实形状或空范围拒绝__f7a8b9(改动: dict) -> None:
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务().恢复冻结范围(快照({**任务范围(), **改动}))
|
||||
assert 异常.value.错误码 == "CONTEXT_SCOPE_INVALID"
|
||||
|
||||
|
||||
def test_细纲身份不在来源清单拒绝__a3c4d5() -> None:
|
||||
范围 = {**任务范围(), "source_ids": ["doc-ch-1", "obj-fact"]}
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务().恢复冻结范围(快照(范围))
|
||||
assert 异常.value.错误码 == "CONTEXT_SCOPE_INVALID"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"改动",
|
||||
[
|
||||
{"run_purpose": None},
|
||||
{"content_purpose": "planning"},
|
||||
{"run_purpose": "production", "content_purpose": "extraction"},
|
||||
],
|
||||
ids=["missing-run-purpose", "content-mismatch", "content-unbound"],
|
||||
)
|
||||
def test_新节必须绑定与任务一致的用途__b6c7d8(改动: dict) -> None:
|
||||
范围 = {**任务范围(), **改动}
|
||||
if 改动.get("run_purpose") is None:
|
||||
del 范围["run_purpose"]
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务().恢复冻结范围(快照(范围))
|
||||
assert 异常.value.错误码 == "CONTEXT_SCOPE_INVALID"
|
||||
|
||||
|
||||
def test_仅历史文稿的范围不要求用途键__c9d0e1() -> None:
|
||||
范围 = 任务范围()
|
||||
for 键 in ("run_purpose", "content_purpose", "target_plan", "facts"):
|
||||
del 范围[键]
|
||||
范围["source_ids"] = ["doc-ch-1"]
|
||||
assert 服务().恢复冻结范围(快照(范围)) == 范围
|
||||
123
tests/单元/test_上下文回放清单形状.py
Normal file
123
tests/单元/test_上下文回放清单形状.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""探索清单形状校验纯函数:顺序、身份、类型与来源绑定的确定性拒绝;离线不连库。"""
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from muse.上下文.接口 import 上下文错误, 探索条目, 校验清单, 记录探索
|
||||
from muse.任务运行.接口 import 工具来源, 工具结果
|
||||
|
||||
范围 = {
|
||||
"work_id": "work-a",
|
||||
"as_of": 2,
|
||||
"source_ids": ["doc-1", "plan-1", "fact-lin"],
|
||||
"documents": [
|
||||
{
|
||||
"chapter_id": "ch-1",
|
||||
"branch_id": "main",
|
||||
"revision": 1,
|
||||
"document_id": "doc-1",
|
||||
"document_hash": "hash-doc-1",
|
||||
"position": 1,
|
||||
}
|
||||
],
|
||||
"directory_revision": 3,
|
||||
"authorized_by": "author-a",
|
||||
"run_purpose": "production",
|
||||
"content_purpose": "generation",
|
||||
"target_plan": {
|
||||
"chapter_id": "ch-3",
|
||||
"plan_id": "plan-1",
|
||||
"revision": 1,
|
||||
"content_hash": "hash-plan-1",
|
||||
},
|
||||
"facts": {"system_revision": 5},
|
||||
}
|
||||
|
||||
读取结果 = 工具结果(
|
||||
内容={"chapter_id": "ch-1", "text": "林深停在码头"},
|
||||
来源=(
|
||||
工具来源("doc-1", "1", "hash-doc-1", "visible-text-codepoints/v1:production:generation"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def 条目(调用ID: str = "call-1", 工具名: str = "read_history_document", 参数: dict | None = None):
|
||||
"""用真实读取结果形状构建顺序为 1 的清单条目;覆盖字段由用例自行 replace。"""
|
||||
return 记录探索(
|
||||
(),
|
||||
调用ID,
|
||||
工具名,
|
||||
参数 if 参数 is not None else {"chapter_id": "ch-1", "revision": 1},
|
||||
读取结果,
|
||||
)[0]
|
||||
|
||||
|
||||
def 顺序清单(*调用: tuple[str, str, dict]) -> tuple[探索条目, ...]:
|
||||
"""按调用次序经记录探索构建顺序连续的合法清单。"""
|
||||
清单: tuple[探索条目, ...] = ()
|
||||
for 调用ID, 工具名, 参数 in 调用:
|
||||
清单 = 记录探索(清单, 调用ID, 工具名, 参数, 读取结果)
|
||||
return 清单
|
||||
|
||||
|
||||
def test_顺序断裂拒绝__b7e0b1() -> None:
|
||||
跳号 = (条目("call-1"), replace(条目("call-2"), 顺序=3))
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单(跳号, 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
assert "顺序" in 异常.value.说明
|
||||
倒序 = (replace(条目("call-1"), 顺序=2), replace(条目("call-2"), 顺序=1))
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单(倒序, 范围)
|
||||
assert "顺序" in 异常.value.说明
|
||||
|
||||
|
||||
def test_重复调用ID拒绝__b7e0b2() -> None:
|
||||
重复 = (条目("call-1"), replace(条目("call-1"), 顺序=2))
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单(重复, 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
assert "call-1" in 异常.value.说明 and "重复" in 异常.value.说明
|
||||
|
||||
|
||||
def test_布尔与类型混淆拒绝__b7e0b3() -> None:
|
||||
# 顺序是 bool 而不是 int:True 不能冒充 1
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((replace(条目("call-1"), 顺序=True),), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
# 参数里的版本是 bool:JSON Schema 的 integer 不接受布尔
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((条目("call-1", 参数={"chapter_id": "ch-1", "revision": True}),), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
# 条目本身不是探索条目对象:字典形状不能冒充
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单(({"call_id": "call-1", "tool": "read_history_document"},), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
|
||||
|
||||
def test_缺来源绑定与非法消费哈希拒绝__b7e0b4() -> None:
|
||||
缺绑定 = (replace(条目("call-1"), 来源引用=()),)
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单(缺绑定, 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
assert "来源绑定" in 异常.value.说明
|
||||
非法哈希 = (replace(条目("call-1"), 消费哈希="不是十六进制"),)
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单(非法哈希, 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
assert "消费哈希" in 异常.value.说明
|
||||
|
||||
|
||||
def test_合法清单按序通过且空清单拒绝__b7e0b5() -> None:
|
||||
清单 = 顺序清单(
|
||||
("call-1", "query_work_plan", {}),
|
||||
("call-2", "query_facts_as_of", {}),
|
||||
("call-3", "read_history_document", {"chapter_id": "ch-1", "revision": 1}),
|
||||
("call-4", "read_history_document", {"chapter_id": "ch-1", "revision": 1, "end": 6}),
|
||||
)
|
||||
assert [项.顺序 for 项 in 清单] == [1, 2, 3, 4]
|
||||
校验清单(清单, 范围) # 不抛异常即通过
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
115
tests/单元/test_上下文快照哈希.py
Normal file
115
tests/单元/test_上下文快照哈希.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""上下文快照身份与输入哈希的确定性;离线纯函数,不连库。
|
||||
|
||||
哈希期望覆盖键序无关与逐类输入变化,身份期望按 uuid5 惯例稳定派生。
|
||||
"""
|
||||
|
||||
from muse.上下文.接口 import (
|
||||
快照身份,
|
||||
快照载荷,
|
||||
文本哈希,
|
||||
材料装配结果,
|
||||
来源绑定,
|
||||
输入哈希,
|
||||
)
|
||||
|
||||
范围 = {
|
||||
"work_id": "work-a",
|
||||
"as_of": 2,
|
||||
"source_ids": ["doc-1", "plan-1"],
|
||||
"documents": [
|
||||
{
|
||||
"chapter_id": "ch-1",
|
||||
"branch_id": "main",
|
||||
"revision": 1,
|
||||
"document_id": "doc-1",
|
||||
"document_hash": "hash-1",
|
||||
"position": 1,
|
||||
}
|
||||
],
|
||||
"directory_revision": 3,
|
||||
"authorized_by": "author-a",
|
||||
"run_purpose": "production",
|
||||
"content_purpose": "generation",
|
||||
"target_plan": {
|
||||
"chapter_id": "ch-3",
|
||||
"plan_id": "plan-1",
|
||||
"revision": 1,
|
||||
"content_hash": "plan-hash",
|
||||
},
|
||||
}
|
||||
|
||||
装配 = 材料装配结果(
|
||||
入选来源=("目标章细纲", "历史:ch-1"),
|
||||
文本="邮差在旧码头确认收信人。\n\n林深停在码头。",
|
||||
字节数=59, # 36 + 分隔符 2 + 21,均按 UTF-8 字节
|
||||
省略=(("事实:lin-shen", "BUDGET_EXCEEDED"),),
|
||||
)
|
||||
绑定 = (
|
||||
来源绑定("plan-1", "1", "plan-hash", "visible-text-codepoints/v1:production:generation"),
|
||||
来源绑定("doc-1", "1", "hash-1", "visible-text-codepoints/v1:production:generation"),
|
||||
)
|
||||
|
||||
|
||||
def test_同载荷同哈希且键序无关__b09301() -> None:
|
||||
载荷 = 快照载荷("task-1", 范围, 装配, 绑定)
|
||||
assert 输入哈希(载荷) == 输入哈希(快照载荷("task-1", 范围, 装配, 绑定))
|
||||
# 键序无关:把范围与载荷键按相反顺序重建,哈希不变
|
||||
逆序范围 = {键: 范围[键] for 键 in reversed(list(范围))}
|
||||
逆序载荷 = {键: 载荷[键] for 键 in reversed(list(载荷))}
|
||||
逆序装配节 = {键: 载荷["装配"][键] for 键 in reversed(list(载荷["装配"]))}
|
||||
逆序载荷["装配"] = 逆序装配节
|
||||
逆序范围["target_plan"] = {
|
||||
键: 范围["target_plan"][键] for 键 in reversed(list(范围["target_plan"]))
|
||||
}
|
||||
assert 输入哈希(逆序载荷) == 输入哈希(载荷)
|
||||
assert 输入哈希(快照载荷("task-1", 逆序范围, 装配, 绑定)) == 输入哈希(载荷)
|
||||
# 同一任务同一输入派生同一身份;文本哈希按 UTF-8 字节稳定
|
||||
哈希 = 输入哈希(载荷)
|
||||
assert 快照身份("task-1", 哈希) == 快照身份("task-1", 哈希)
|
||||
assert 快照身份("task-1", 哈希) != 快照身份("task-2", 哈希)
|
||||
assert 文本哈希(装配.文本) == 文本哈希("邮差在旧码头确认收信人。\n\n林深停在码头。")
|
||||
|
||||
|
||||
def test_任一输入变化产生新哈希与新身份__b09302() -> None:
|
||||
基准 = 输入哈希(快照载荷("task-1", 范围, 装配, 绑定))
|
||||
基准身份 = 快照身份("task-1", 基准)
|
||||
|
||||
变化装配省略 = 材料装配结果(
|
||||
入选来源=装配.入选来源,
|
||||
文本=装配.文本,
|
||||
字节数=装配.字节数,
|
||||
省略=(("事实:lin-shen", "BUDGET_EXCEEDED"), ("事实:dock-yard", "EMPTY")),
|
||||
)
|
||||
变化字节与文本 = 材料装配结果(
|
||||
入选来源=("目标章细纲",), 文本="邮差在旧码头确认收信人。", 字节数=36, 省略=装配.省略
|
||||
)
|
||||
变化文本同字节 = 材料装配结果(
|
||||
入选来源=装配.入选来源,
|
||||
文本="邮差在新码头确认收信人。\n\n林深停在码头。",
|
||||
字节数=装配.字节数,
|
||||
省略=装配.省略,
|
||||
)
|
||||
变化绑定 = (来源绑定("plan-1", "2", "plan-hash-2", 绑定[0].投影版本), 绑定[1])
|
||||
变化投影版本 = (
|
||||
来源绑定(
|
||||
绑定[0].来源ID,
|
||||
绑定[0].数据版本,
|
||||
绑定[0].结构哈希,
|
||||
"visible-text-codepoints/v1:evaluation:generation",
|
||||
),
|
||||
绑定[1],
|
||||
)
|
||||
变化范围 = dict(范围, directory_revision=4)
|
||||
|
||||
变化哈希集 = {
|
||||
"省略": 输入哈希(快照载荷("task-1", 范围, 变化装配省略, 绑定)),
|
||||
"字节数": 输入哈希(快照载荷("task-1", 范围, 变化字节与文本, 绑定)),
|
||||
"文本同字节": 输入哈希(快照载荷("task-1", 范围, 变化文本同字节, 绑定)),
|
||||
"来源版本": 输入哈希(快照载荷("task-1", 范围, 装配, 变化绑定)),
|
||||
"投影版本": 输入哈希(快照载荷("task-1", 范围, 装配, 变化投影版本)),
|
||||
"目录版本": 输入哈希(快照载荷("task-1", 变化范围, 装配, 绑定)),
|
||||
}
|
||||
assert 基准 not in 变化哈希集.values()
|
||||
assert len(set(变化哈希集.values())) == len(变化哈希集)
|
||||
for 哈希 in 变化哈希集.values():
|
||||
assert 快照身份("task-1", 哈希) != 基准身份
|
||||
205
tests/契约/test_探索回放合同.py
Normal file
205
tests/契约/test_探索回放合同.py
Normal file
@ -0,0 +1,205 @@
|
||||
"""探索清单与确定性回放的合同行为:未知工具、越界参数、缺来源拒绝;顺序、范围、理由与消费哈希可核对。
|
||||
|
||||
离线纯函数与合成读取器;不连库、不代表真实来源授权。旧 TC 分配不承身份,按功能价值新写。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
from muse.上下文.接口 import (
|
||||
上下文错误,
|
||||
探索条目,
|
||||
来源绑定,
|
||||
校验清单,
|
||||
记录探索,
|
||||
逐项比对,
|
||||
)
|
||||
from muse.任务运行.接口 import (
|
||||
内容哈希,
|
||||
只读工具集,
|
||||
工具定义,
|
||||
工具来源,
|
||||
工具结果,
|
||||
工具范围,
|
||||
工具请求,
|
||||
模型协议错误,
|
||||
)
|
||||
|
||||
范围 = {
|
||||
"work_id": "work-a",
|
||||
"as_of": 2,
|
||||
"source_ids": ["doc-1", "plan-1", "fact-lin"],
|
||||
"documents": [
|
||||
{
|
||||
"chapter_id": "ch-1",
|
||||
"branch_id": "main",
|
||||
"revision": 1,
|
||||
"document_id": "doc-1",
|
||||
"document_hash": "hash-doc-1",
|
||||
"position": 1,
|
||||
}
|
||||
],
|
||||
"directory_revision": 3,
|
||||
"authorized_by": "author-a",
|
||||
"run_purpose": "production",
|
||||
"content_purpose": "generation",
|
||||
"target_plan": {
|
||||
"chapter_id": "ch-3",
|
||||
"plan_id": "plan-1",
|
||||
"revision": 1,
|
||||
"content_hash": "hash-plan-1",
|
||||
},
|
||||
"facts": {"system_revision": 5},
|
||||
}
|
||||
|
||||
投影 = "visible-text-codepoints/v1:production:generation"
|
||||
文档来源 = 工具来源("doc-1", "1", "hash-doc-1", 投影)
|
||||
文档绑定 = (来源绑定("doc-1", "1", "hash-doc-1", 投影),)
|
||||
文档结果 = 工具结果({"chapter_id": "ch-1", "text": "林深停在码头,雨还没停。"}, (文档来源,))
|
||||
|
||||
|
||||
def 带参条目(工具名: str, 参数: dict, 顺序: int = 1, 调用ID: str = "call-x") -> 探索条目:
|
||||
"""以固定来源绑定与消费哈希构造任意参数条目;参数合法性正是被测对象。"""
|
||||
return 探索条目(
|
||||
调用ID=调用ID,
|
||||
工具名=工具名,
|
||||
顺序=顺序,
|
||||
参数=参数,
|
||||
来源引用=文档绑定,
|
||||
消费哈希=内容哈希(文档结果.内容),
|
||||
)
|
||||
|
||||
|
||||
def test_未知工具与未授权工具分别明确拒绝__b7e0a1() -> None:
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((带参条目("read_anything", {"chapter_id": "ch-1"}),), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
assert "未知工具" in 异常.value.说明 and "read_anything" in 异常.value.说明
|
||||
# 工具真实存在但任务范围未冻结对应来源:拒绝的是授权,不是字符串存在性
|
||||
无细纲 = {**范围, "target_plan": None}
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((带参条目("query_work_plan", {}),), 无细纲)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
assert "query_work_plan" in 异常.value.说明 and "冻结范围" in 异常.value.说明
|
||||
无事实 = {**范围, "facts": None}
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((带参条目("query_facts_as_of", {}),), 无事实)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
|
||||
|
||||
def test_参数越出冻结范围与扩权字段拒绝__b7e0a2() -> None:
|
||||
未冻结版本 = 带参条目("read_history_document", {"chapter_id": "ch-1", "revision": 2})
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((未冻结版本,), 范围)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
assert "不在冻结范围内" in 异常.value.说明 and "版本 2" in 异常.value.说明
|
||||
未冻结文稿 = 带参条目(
|
||||
"read_history_document", {"chapter_id": "ch-2", "revision": 1, "branch_id": "draft"}
|
||||
)
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((未冻结文稿,), 范围)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
扩权 = 带参条目("read_history_document", {"chapter_id": "ch-1", "revision": 1, "as_of": 9})
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((扩权,), 范围)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
assert "扩权" in 异常.value.说明 or "不得" in 异常.value.说明
|
||||
# 角色视角必须携带知情主体,其他视角不得携带:与工具读取同一规则
|
||||
无知情主体 = 带参条目("query_facts_as_of", {"view": "character"})
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((无知情主体,), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
assert "知情主体" in 异常.value.说明
|
||||
非法视角 = 带参条目("query_facts_as_of", {"view": "narrator"})
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
校验清单((非法视角,), 范围)
|
||||
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
|
||||
|
||||
|
||||
def test_回放比对指明版本哈希投影与内容漂移__b7e0a3() -> None:
|
||||
基准 = 带参条目(
|
||||
"read_history_document", {"chapter_id": "ch-1", "revision": 1}, 调用ID="call-hist"
|
||||
)
|
||||
assert 逐项比对(基准, 文档结果) == ()
|
||||
版本漂移 = 逐项比对(
|
||||
基准, 工具结果(文档结果.内容, (工具来源("doc-1", "2", "hash-doc-1", 投影),))
|
||||
)
|
||||
assert len(版本漂移) == 1 and "数据版本" in 版本漂移[0] and "call-hist" in 版本漂移[0]
|
||||
哈希漂移 = 逐项比对(基准, 工具结果(文档结果.内容, (工具来源("doc-1", "1", "hash-改", 投影),)))
|
||||
assert len(哈希漂移) == 1 and "结构哈希" in 哈希漂移[0]
|
||||
投影漂移 = 逐项比对(
|
||||
基准, 工具结果(文档结果.内容, (工具来源("doc-1", "1", "hash-doc-1", "proj/v2"),))
|
||||
)
|
||||
assert len(投影漂移) == 1 and "投影版本" in 投影漂移[0]
|
||||
内容漂移 = 逐项比对(基准, 工具结果({"chapter_id": "ch-1", "text": "改过的正文"}, (文档来源,)))
|
||||
assert any("消费哈希" in 项 for 项 in 内容漂移)
|
||||
来源消失 = 逐项比对(基准, 工具结果(文档结果.内容, ()))
|
||||
assert any("已不在" in 项 for 项 in 来源消失)
|
||||
清单外来源 = 逐项比对(
|
||||
基准, 工具结果(文档结果.内容, (文档来源, 工具来源("doc-9", "1", "hash-doc-1", 投影)))
|
||||
)
|
||||
assert any("清单外来源 doc-9" in 项 for 项 in 清单外来源)
|
||||
|
||||
|
||||
def test_记录探索消费哈希与来源绑定同键__b7e0a4() -> None:
|
||||
记录 = []
|
||||
|
||||
def 读取(_, 参数):
|
||||
assert 参数 == {"chapter_id": "ch-1", "revision": 1}
|
||||
return 文档结果
|
||||
|
||||
定义 = 工具定义(
|
||||
"read_history_document",
|
||||
"读取历史正文",
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": False,
|
||||
"required": ["chapter_id", "revision"],
|
||||
"properties": {
|
||||
"chapter_id": {"type": "string", "minLength": 1},
|
||||
"revision": {"type": "integer", "minimum": 1},
|
||||
},
|
||||
},
|
||||
读取,
|
||||
)
|
||||
Draft202012Validator.check_schema(定义.参数合同)
|
||||
集合 = 只读工具集(
|
||||
(定义,),
|
||||
("read_history_document",),
|
||||
工具范围("task-r", "work-a", ("doc-1",), 2, "production", "generation"),
|
||||
记录.append,
|
||||
)
|
||||
参数 = {"chapter_id": "ch-1", "revision": 1}
|
||||
结果 = 集合.调用(工具请求("call-hist", "read_history_document", 参数))
|
||||
条目 = 记录探索((), "call-hist", "read_history_document", 参数, 结果)[0]
|
||||
# 清单条目与 S02 工具读取事件留痕同键:来源四元组逐字段一致
|
||||
留痕 = 记录[0]["source_refs"][0]
|
||||
绑定 = 条目.来源引用[0]
|
||||
assert (绑定.来源ID, 绑定.数据版本, 绑定.结构哈希, 绑定.投影版本) == (
|
||||
留痕["source_id"],
|
||||
留痕["revision"],
|
||||
留痕["schema_hash"],
|
||||
留痕["projection_version"],
|
||||
)
|
||||
assert 条目.消费哈希 == 内容哈希(结果.内容)
|
||||
assert 条目.顺序 == 1
|
||||
校验清单((条目,), 范围)
|
||||
# 工具读取被 S02 拒绝时不产生清单可承接的结果
|
||||
with pytest.raises(模型协议错误):
|
||||
集合.调用(
|
||||
工具请求("call-bad", "read_history_document", {"chapter_id": "ch-1", "work_id": "x"})
|
||||
)
|
||||
assert len(记录) == 1
|
||||
|
||||
|
||||
def test_三类工具清单按序通过且顺序可核对__b7e0a5() -> None:
|
||||
清单: tuple[探索条目, ...] = ()
|
||||
for 调用ID, 工具名, 参数 in [
|
||||
("call-plan", "query_work_plan", {}),
|
||||
("call-facts", "query_facts_as_of", {}),
|
||||
("call-hist", "read_history_document", {"chapter_id": "ch-1", "revision": 1, "end": 6}),
|
||||
]:
|
||||
清单 = 记录探索(清单, 调用ID, 工具名, 参数, 文档结果)
|
||||
校验清单(清单, 范围) # 不抛异常即通过
|
||||
assert [项.调用ID for 项 in 清单] == ["call-plan", "call-facts", "call-hist"]
|
||||
assert [项.顺序 for 项 in 清单] == [1, 2, 3]
|
||||
496
tests/集成/test_上下文回放与复检.py
Normal file
496
tests/集成/test_上下文回放与复检.py
Normal file
@ -0,0 +1,496 @@
|
||||
"""确定性回放与依赖复检的真实链路:探索留痕、冻结、回放一致、漂移失效、恢复阻断。
|
||||
|
||||
真实 PG + S02 全链路,不调用模型;漂移用例各自指明失效项,不做字符串包含冒充。
|
||||
"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from muse.上下文.接口 import (
|
||||
上下文错误,
|
||||
依赖重验检查,
|
||||
回放,
|
||||
工具范围从冻结,
|
||||
探索条目,
|
||||
来源绑定,
|
||||
核对依赖,
|
||||
装配材料,
|
||||
记录探索,
|
||||
)
|
||||
from muse.任务运行.接口 import (
|
||||
事件类型,
|
||||
任务请求,
|
||||
工具请求,
|
||||
步骤处理器,
|
||||
步骤结果,
|
||||
步骤计划,
|
||||
)
|
||||
from muse.作品规划.接口 import 档案保存, 规划保存, 规划定位
|
||||
from muse.元数据.接口 import 元数据服务, 导入内置结构
|
||||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||||
from muse.启动 import 构建
|
||||
from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据
|
||||
from muse.正式变更.接口 import 作者动作
|
||||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||||
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
|
||||
from muse.配置 import 应用配置
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
工具集 = ("read_history_document", "query_work_plan", "query_facts_as_of")
|
||||
正文文本 = "林深停在码头,雨还没停。"
|
||||
|
||||
|
||||
def 细纲内容(parent_id: str) -> dict:
|
||||
return {
|
||||
"targetChapter": 3,
|
||||
"sourceRef": {"sourceId": parent_id, "sourceVersion": 1},
|
||||
"chapterGoal": "邮差在旧码头确认收信人。",
|
||||
"hardConstraints": ["上一章结尾雨还没停。"],
|
||||
"keyEvents": [
|
||||
{
|
||||
"id": "arrival",
|
||||
"order": 1,
|
||||
"event": "邮差到达旧码头",
|
||||
"participants": ["lin-shen"],
|
||||
"trigger": "收到退回的信",
|
||||
"resultDirection": "确认码头还在使用",
|
||||
}
|
||||
],
|
||||
"adjustableBeats": [],
|
||||
"declaredNewFacts": [],
|
||||
"mustAppearEntities": ["lin-shen"],
|
||||
"chapterEndHook": "仓库的门后有人应声。",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 回放环境(应用测试库):
|
||||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||||
导入内置结构(元数据服务(连))
|
||||
登记 = 流程登记()
|
||||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"), 流程=登记)
|
||||
服务 = 装配.要求上下文()
|
||||
作者 = 调用身份("replay-author", None, 用途.生产, 内容用途.生成)
|
||||
作品, 正文 = 装配.要求作品(), 装配.要求正文()
|
||||
结构选择 = 作品.新档案结构(作者, "replay-work", "work_core", 1)
|
||||
作品.保存档案(
|
||||
作者,
|
||||
"create-replay-work",
|
||||
档案保存(
|
||||
"replay-work", 0, {"名称": "旧码头", "题材定位": "现实", "主题立意": "还信"}, 结构选择
|
||||
),
|
||||
)
|
||||
for i in range(1, 4):
|
||||
作品.添加章节(作者, f"add-ch-{i}", "replay-work", f"ch-{i}", f"第{i}章", 预期目录版本=i - 1)
|
||||
正文.保存人工(
|
||||
作者,
|
||||
"body-ch-1",
|
||||
"ch-1",
|
||||
0,
|
||||
正文草稿((段落("p-ch-1", (文本节点(正文文本),)),)),
|
||||
)
|
||||
|
||||
规划 = 装配.要求规划()
|
||||
|
||||
def 保存并确认(请求, 命令: str) -> dict:
|
||||
回执 = 规划.保存人工候选(作者, 命令 + "-draft", 请求)
|
||||
候选 = 规划.读取候选(作者, 回执["results"][0]["candidate_id"])
|
||||
定位 = 规划定位(
|
||||
候选["candidate_id"],
|
||||
候选["revision"],
|
||||
候选["candidate_hash"],
|
||||
候选["plan_id"],
|
||||
请求.expected_revision,
|
||||
)
|
||||
审阅 = 规划.打开审阅(作者, 定位)
|
||||
规划.决定(
|
||||
作者,
|
||||
命令 + "-accept",
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("plan",),
|
||||
)
|
||||
return 规划.读取规划(作者, 候选["plan_id"])
|
||||
|
||||
世界 = 装配.要求故事世界()
|
||||
|
||||
def 正文源(章: str, 引文: str) -> 正文依据:
|
||||
记录 = 正文.读取正文(作者, 章)
|
||||
草稿 = TypeAdapter(正文草稿).validate_python(记录["document"])
|
||||
return 正文依据(
|
||||
章, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(草稿, 引文),)
|
||||
)
|
||||
|
||||
def 确认事实(对象, 类型, 内容, 来源=(), 生效="ch-1", 独立=False) -> None:
|
||||
选择 = 世界.新事实结构(作者, "replay-work", 对象, 类型, 1)
|
||||
回执 = 世界.提出事实(
|
||||
作者,
|
||||
"propose-" + 对象,
|
||||
事实提案请求("replay-work", 对象, 0, 选择, 内容, tuple(来源), 生效, independent=独立),
|
||||
)
|
||||
提案 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
|
||||
定位 = 事实定位(
|
||||
提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + 对象, 0
|
||||
)
|
||||
审阅 = 世界.打开事实审阅(作者, 定位)
|
||||
世界.决定事实(
|
||||
作者,
|
||||
"confirm-" + 对象,
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("object",),
|
||||
)
|
||||
|
||||
确认事实("lin-shen", "character", {"名称": "林深"}, (正文源("ch-1", "林深停在码头"),))
|
||||
父 = 保存并确认(
|
||||
规划保存(
|
||||
"replay-work", 0, 规划.新规划结构(作者, "replay-work", "outline", 1), {"名称": "旧码头"}
|
||||
),
|
||||
"outline",
|
||||
)
|
||||
保存并确认(
|
||||
规划保存(
|
||||
"replay-work",
|
||||
0,
|
||||
规划.新规划结构(作者, "replay-work", "fine_outline", 1),
|
||||
细纲内容(父["plan_id"]),
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"fine-outline",
|
||||
)
|
||||
|
||||
探索留痕: dict = {"清单": (), "首读": ()}
|
||||
|
||||
def 探索并冻结(上下文) -> 步骤结果:
|
||||
工具 = 装配.要求只读工具(上下文.领取)
|
||||
清单: tuple[探索条目, ...] = ()
|
||||
首读: list[tuple[str, dict]] = []
|
||||
绑定集: dict[str, 来源绑定] = {}
|
||||
材料: dict[str, str] = {}
|
||||
|
||||
def 读(调用ID: str, 名称: str, 参数: dict):
|
||||
nonlocal 清单
|
||||
结果 = 工具.调用(工具请求(调用ID, 名称, 参数))
|
||||
清单 = 记录探索(清单, 调用ID, 名称, 参数, 结果)
|
||||
首读.append((调用ID, 结果.内容))
|
||||
for 源 in 结果.来源:
|
||||
绑定集.setdefault(
|
||||
源.来源ID, 来源绑定(源.来源ID, 源.数据版本, 源.结构哈希, 源.投影版本)
|
||||
)
|
||||
return 结果
|
||||
|
||||
计划 = 读("plan-1", "query_work_plan", {})
|
||||
事实 = 读("facts-1", "query_facts_as_of", {})
|
||||
读("hist-1", "read_history_document", {"chapter_id": "ch-1", "revision": 1, "end": 6})
|
||||
读("hist-2", "read_history_document", {"chapter_id": "ch-1", "revision": 1, "start": 6})
|
||||
材料["目标章细纲"] = 计划.内容["content"]["chapterGoal"]
|
||||
for 项 in 事实.内容["facts"]:
|
||||
名称 = 项["content"].get("名称")
|
||||
if 名称:
|
||||
材料["事实:" + 项["object_id"]] = 名称
|
||||
材料["历史:ch-1"] = 首读[2][1]["text"] + 首读[3][1]["text"]
|
||||
上限 = 上下文.任务.冻结输入["冻结上下文"]["budget"]["context_bytes"]
|
||||
回执 = 服务.冻结上下文(
|
||||
上下文.任务.任务ID,
|
||||
装配材料(材料, tuple(材料), 字节上限=上限),
|
||||
tuple(绑定集.values()),
|
||||
)
|
||||
探索留痕["清单"] = 清单
|
||||
探索留痕["首读"] = tuple(首读)
|
||||
return 步骤结果(
|
||||
{
|
||||
"快照": {
|
||||
"snapshot_id": 回执.快照ID,
|
||||
"input_hash": 回执.输入哈希,
|
||||
"created": 回执.新建,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
登记.登记处理器(
|
||||
步骤处理器(
|
||||
"synthetic-replay",
|
||||
"1",
|
||||
探索并冻结,
|
||||
"replay/v1",
|
||||
"replay/v1",
|
||||
保护职责="范围核对",
|
||||
允许工具=工具集,
|
||||
)
|
||||
)
|
||||
登记.登记类型("synthetic-replay", 必需保护=("范围核对",))
|
||||
# 恢复检查注册真实依赖重验:授权、来源、结构与版本,不再只是只读范围的 lambda
|
||||
登记.登记恢复检查("synthetic-replay", "1", 依赖重验检查(服务))
|
||||
运行 = 装配.任务运行
|
||||
assert 运行 is not None
|
||||
流程服务(运行, 登记).发布(
|
||||
流程定义("synthetic-replay", "1", (步骤计划("read", "synthetic-replay", "1", 工具=工具集),))
|
||||
)
|
||||
|
||||
def 开始(范围=None):
|
||||
if 范围 is None:
|
||||
范围 = 服务.授权任务范围(
|
||||
作者,
|
||||
"replay-work",
|
||||
2,
|
||||
({"chapter_id": "ch-1", "revision": 1},),
|
||||
目标章ID="ch-3",
|
||||
含事实=True,
|
||||
)
|
||||
id_ = 运行.创建任务(
|
||||
任务请求(
|
||||
"synthetic-replay",
|
||||
str(uuid4()),
|
||||
作者.作者,
|
||||
用途.生产,
|
||||
内容用途.生成,
|
||||
{},
|
||||
"test",
|
||||
"test",
|
||||
{
|
||||
"source_scope": 范围,
|
||||
"schema_versions": {},
|
||||
"authorization": "synthetic-author-choice",
|
||||
"budget": {"context_bytes": 4096},
|
||||
"stop_conditions": {"max_reads": 6},
|
||||
},
|
||||
作品ID="replay-work",
|
||||
),
|
||||
"synthetic-replay",
|
||||
"1",
|
||||
)
|
||||
领取 = 运行.领取步骤("replay-test", ["synthetic-replay"], 租期秒=120)
|
||||
assert 领取 is not None and 领取.任务ID == id_
|
||||
return id_, 领取, 装配.要求只读工具(领取)
|
||||
|
||||
def 改细纲() -> None:
|
||||
内容 = 细纲内容(父["plan_id"])
|
||||
内容["chapterGoal"] = "作者改过的目标。"
|
||||
保存并确认(
|
||||
规划保存(
|
||||
"replay-work",
|
||||
1,
|
||||
规划.新规划结构(作者, "replay-work", "fine_outline", 1),
|
||||
内容,
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"revise-outline",
|
||||
)
|
||||
|
||||
def 改正文() -> None:
|
||||
正文.保存人工(
|
||||
作者, "freeze-rev-2", "ch-1", 1, 正文草稿((段落("p-2", (文本节点("改过的正文"),)),))
|
||||
)
|
||||
|
||||
def 前进事实() -> None:
|
||||
确认事实("late-comer", "character", {"名称": "后来的事实"}, 独立=True)
|
||||
|
||||
return {
|
||||
"装配": 装配,
|
||||
"服务": 服务,
|
||||
"作者": 作者,
|
||||
"规划": 规划,
|
||||
"正文": 正文,
|
||||
"开始": 开始,
|
||||
"改细纲": 改细纲,
|
||||
"改正文": 改正文,
|
||||
"前进事实": 前进事实,
|
||||
"探索留痕": 探索留痕,
|
||||
}
|
||||
|
||||
|
||||
def 工具读取事件数(装配, task_id: str) -> int:
|
||||
return sum(1 for e in 装配.任务运行.续接事件(task_id).事件 if e.类型 == 事件类型.工具读取)
|
||||
|
||||
|
||||
def test_回放与首读逐项一致且不再留痕__b7e0c1(回放环境):
|
||||
环境 = 回放环境
|
||||
装配, 服务 = 环境["装配"], 环境["服务"]
|
||||
task_id, 领取, _ = 环境["开始"]()
|
||||
结果 = 装配.任务运行.执行一步(领取)
|
||||
清单 = 环境["探索留痕"]["清单"]
|
||||
首读 = 环境["探索留痕"]["首读"]
|
||||
assert [项.调用ID for 项 in 清单] == ["plan-1", "facts-1", "hist-1", "hist-2"]
|
||||
留痕数 = 工具读取事件数(装配, task_id)
|
||||
assert 留痕数 == 4
|
||||
回放结果 = 服务.回放探索(task_id, 清单)
|
||||
# 回放确定性:逐项与首读完全相等,含中文码点片段
|
||||
assert [(条.调用ID, 条.内容) for 条 in 回放结果.逐项] == list(首读)
|
||||
assert 回放结果.逐项[2].内容["text"] == "林深停在码头"
|
||||
assert 回放结果.逐项[3].内容["text"] == ",雨还没停。"
|
||||
assert [条.消费哈希 for 条 in 回放结果.逐项] == [项.消费哈希 for 项 in 清单]
|
||||
assert [条.来源 for 条 in 回放结果.逐项] == [项.来源引用 for 项 in 清单]
|
||||
# 回放只用固定材料:不追加工具读取留痕
|
||||
assert 工具读取事件数(装配, task_id) == 留痕数
|
||||
# 冻结快照路径与任务内冻结范围路径同一结果
|
||||
快照 = 服务.读取任务快照(task_id)[0]
|
||||
assert 快照.快照ID == 结果.输出["快照"]["snapshot_id"]
|
||||
快照回放 = 回放(
|
||||
服务.数据库,
|
||||
快照.冻结范围,
|
||||
工具范围从冻结(task_id, 快照.冻结范围),
|
||||
清单,
|
||||
服务.规划引用解析,
|
||||
)
|
||||
assert 快照回放.逐项 == 回放结果.逐项
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"漂移源",
|
||||
["document", "plan", "facts"],
|
||||
ids=["document-revised", "plan-reconfirmed", "facts-advanced"],
|
||||
)
|
||||
def test_来源漂移后回放明确失效并指明项__b7e0c2(回放环境, 漂移源):
|
||||
环境 = 回放环境
|
||||
装配, 服务 = 环境["装配"], 环境["服务"]
|
||||
task_id, 领取, _ = 环境["开始"]()
|
||||
装配.任务运行.执行一步(领取)
|
||||
清单 = 环境["探索留痕"]["清单"]
|
||||
if 漂移源 == "document":
|
||||
环境["改正文"]()
|
||||
指明 = ("hist-1", "ch-1")
|
||||
elif 漂移源 == "plan":
|
||||
环境["改细纲"]()
|
||||
指明 = ("plan-1", "ch-3", "细纲")
|
||||
else:
|
||||
环境["前进事实"]()
|
||||
指明 = ("facts-1", "事实系统版本")
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务.回放探索(task_id, 清单)
|
||||
assert 异常.value.错误码 == "SOURCE_STALE"
|
||||
for 项 in 指明:
|
||||
assert 项 in 异常.value.说明
|
||||
# 失效不采纳内容:快照历史不改写,仍只有一条冻结记录
|
||||
assert len(服务.读取任务快照(task_id)) == 1
|
||||
|
||||
|
||||
def test_越界参数清单回放拒绝__b7e0c3(回放环境):
|
||||
环境 = 回放环境
|
||||
装配, 服务 = 环境["装配"], 环境["服务"]
|
||||
task_id, 领取, _ = 环境["开始"]()
|
||||
装配.任务运行.执行一步(领取)
|
||||
清单 = 环境["探索留痕"]["清单"]
|
||||
历史 = next(项 for 项 in 清单 if 项.工具名 == "read_history_document")
|
||||
|
||||
def 越界条目(调用ID: str, 参数: dict) -> 探索条目:
|
||||
return 探索条目(
|
||||
调用ID=调用ID,
|
||||
工具名="read_history_document",
|
||||
顺序=len(清单) + 1,
|
||||
参数=参数,
|
||||
来源引用=历史.来源引用,
|
||||
消费哈希=历史.消费哈希,
|
||||
)
|
||||
|
||||
for 参数 in (
|
||||
{"chapter_id": "ch-1", "revision": 2},
|
||||
{"chapter_id": "ch-2", "revision": 1},
|
||||
{"chapter_id": "ch-1", "revision": 1, "as_of": 3},
|
||||
):
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务.回放探索(task_id, 清单 + (越界条目("越界", 参数),))
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
assert "越界" in 异常.value.说明
|
||||
# 未追加越界条目的原清单仍可回放
|
||||
服务.回放探索(task_id, 清单)
|
||||
|
||||
|
||||
def test_依赖重验检查漂移后阻断任务重验__b7e0c4(回放环境):
|
||||
环境 = 回放环境
|
||||
装配 = 环境["装配"]
|
||||
运行 = 装配.任务运行
|
||||
task_id, 领取, _ = 环境["开始"]()
|
||||
运行.重验执行范围(领取) # 漂移前真实检查通过
|
||||
环境["前进事实"]()
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
运行.重验执行范围(领取)
|
||||
assert 异常.value.错误码 == "SOURCE_STALE"
|
||||
assert "事实系统版本" in 异常.value.说明 and "来源锁定失效" in 异常.value.说明
|
||||
|
||||
|
||||
def test_核对依赖输出锁定依据与变化项__b7e0c5(回放环境):
|
||||
环境 = 回放环境
|
||||
装配, 服务 = 环境["装配"], 环境["服务"]
|
||||
task_id, 领取, _ = 环境["开始"]()
|
||||
装配.任务运行.执行一步(领取)
|
||||
快照 = 服务.读取任务快照(task_id)[0]
|
||||
with 服务.数据库.连接(只读=True) as 连:
|
||||
状态 = 核对依赖(连, 快照.冻结范围, 服务.规划引用解析, 快照.来源绑定)
|
||||
assert 状态.有效 is True and 状态.变化项 == ()
|
||||
锁定 = {项.身份: 项 for 项 in 状态.锁定依据}
|
||||
目标 = 快照.冻结范围["target_plan"]
|
||||
assert 锁定[目标["plan_id"]].种类 == "plan"
|
||||
assert 锁定[目标["plan_id"]].版本 == str(目标["revision"])
|
||||
assert 锁定[目标["plan_id"]].哈希 == 目标["content_hash"]
|
||||
assert 锁定[目标["plan_id"]].投影版本
|
||||
文档ID = 快照.冻结范围["documents"][0]["document_id"]
|
||||
assert 锁定[文档ID].种类 == "document" and 锁定[文档ID].哈希
|
||||
assert any(项.种类 == "facts" for 项 in 状态.锁定依据)
|
||||
环境["前进事实"]()
|
||||
with 服务.数据库.连接(只读=True) as 连:
|
||||
漂移 = 核对依赖(连, 快照.冻结范围, 服务.规划引用解析, 快照.来源绑定)
|
||||
assert 漂移.有效 is False
|
||||
assert any("事实系统版本" in 项 for 项 in 漂移.变化项)
|
||||
assert not any(项.种类 == "facts" for 项 in 漂移.锁定依据)
|
||||
# 文稿漂移连带细纲引用失效时,仍输出结构化变化项而不是向外抛错
|
||||
环境["改正文"]()
|
||||
with 服务.数据库.连接(只读=True) as 连:
|
||||
文稿漂移 = 核对依赖(连, 快照.冻结范围, 服务.规划引用解析, 快照.来源绑定)
|
||||
assert 文稿漂移.有效 is False
|
||||
assert any("ch-1" in 项 and "前进到 2" in 项 for 项 in 文稿漂移.变化项)
|
||||
assert not any(项.种类 == "document" for 项 in 文稿漂移.锁定依据)
|
||||
|
||||
|
||||
def test_回放授权重验失败拒绝回放__b7e0c6(回放环境):
|
||||
"""恢复冻结范围失败(篡改冻结用途或他人作者)时回放整体 SCOPE_DENIED。
|
||||
|
||||
回放入口先按当前授权重验任务冻结范围;身份或用途不一致即拒绝,
|
||||
不沿用已记录清单的读取权限。同一清单在授权一致的任务上仍可回放。
|
||||
"""
|
||||
环境 = 回放环境
|
||||
装配, 服务, 开始, 作者 = 环境["装配"], 环境["服务"], 环境["开始"], 环境["作者"]
|
||||
正常ID, 领取, _ = 开始()
|
||||
装配.任务运行.执行一步(领取)
|
||||
清单 = 环境["探索留痕"]["清单"]
|
||||
范围 = 服务.读取范围(正常ID)
|
||||
篡改集 = (
|
||||
("run_purpose", "evaluation", "任务用途与冻结范围用途不一致"),
|
||||
("authorized_by", "someone-else", "冻结范围与任务作者不一致"),
|
||||
)
|
||||
for 键, 值, 指明 in 篡改集:
|
||||
# 篡改任务不经工具领取(领取路径会在供给工具时先行拒绝),直接入库后回放
|
||||
越权ID = 装配.任务运行.创建任务(
|
||||
任务请求(
|
||||
"synthetic-replay",
|
||||
str(uuid4()),
|
||||
作者.作者,
|
||||
用途.生产,
|
||||
内容用途.生成,
|
||||
{},
|
||||
"test",
|
||||
"test",
|
||||
{
|
||||
"source_scope": {**范围, 键: 值},
|
||||
"schema_versions": {},
|
||||
"authorization": "synthetic-author-choice",
|
||||
"budget": {"context_bytes": 4096},
|
||||
"stop_conditions": {"max_reads": 6},
|
||||
},
|
||||
作品ID="replay-work",
|
||||
),
|
||||
"synthetic-replay",
|
||||
"1",
|
||||
)
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务.回放探索(越权ID, 清单)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
assert "任务授权重验未通过" in 异常.value.说明 and 指明 in 异常.value.说明
|
||||
# 对照:同一真实清单在授权一致的任务上仍可回放,拒绝只来自授权重验
|
||||
服务.回放探索(正常ID, 清单)
|
||||
192
tests/集成/test_上下文工作台.py
Normal file
192
tests/集成/test_上下文工作台.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""作者工作台按任务读取上下文快照的 HTTP 合同:材料清单、来源证据与失效说明。
|
||||
|
||||
真实隔离 PG + 真实 ASGI;只读消费回合1–3 的上下文服务,不 mock 后端。
|
||||
清单只暴露取舍与绑定身份,不回传装配全文,也不泄漏连接串或内部异常。
|
||||
"""
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from muse.上下文.接口 import 投影版本, 来源绑定, 装配材料
|
||||
from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划
|
||||
from muse.作品规划.接口 import 档案保存
|
||||
from muse.元数据.接口 import 元数据服务, 导入内置结构
|
||||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||||
from muse.启动 import 构建
|
||||
from muse.接入.http.应用 import 创建应用
|
||||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||||
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
|
||||
from muse.配置 import 应用配置, 服务配置
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 工作台环境(应用测试库, tmp_path):
|
||||
"""真实作品、任务与冻结快照;HTTP 层只读消费,不另造第二套数据。"""
|
||||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||||
导入内置结构(元数据服务(连))
|
||||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"))
|
||||
作者 = 调用身份("author-local", None, 用途.生产, 内容用途.生成)
|
||||
作品 = 装配.要求作品()
|
||||
作品.保存档案(
|
||||
作者,
|
||||
"create-wb-work",
|
||||
档案保存(
|
||||
"wb-work",
|
||||
0,
|
||||
{"名称": "材料台账", "题材定位": "现实", "主题立意": "对账"},
|
||||
作品.新档案结构(作者, "wb-work", "work_core", 1),
|
||||
),
|
||||
)
|
||||
作品.添加章节(作者, "add-ch-1", "wb-work", "ch-1", "第一章", 预期目录版本=0)
|
||||
装配.要求正文().保存人工(
|
||||
作者, "body-ch-1", "ch-1", 0, 正文草稿((段落("p-1", (文本节点("林深把伞收了起来。"),)),))
|
||||
)
|
||||
范围 = 装配.要求上下文().授权任务范围(
|
||||
作者, "wb-work", 1, ({"chapter_id": "ch-1", "revision": 1},)
|
||||
)
|
||||
登记 = 流程登记()
|
||||
登记.登记处理器(
|
||||
步骤处理器("workbench-freeze", "1", lambda _: 步骤结果({"ok": True}), "v1", "v1")
|
||||
)
|
||||
登记.登记类型("workbench-freeze", 必需保护=())
|
||||
运行 = 任务服务(应用测试库[用途.生产], 登记)
|
||||
流程服务(运行, 登记).发布(
|
||||
流程定义("workbench-freeze", "1", (步骤计划("冻结", "workbench-freeze", "1"),))
|
||||
)
|
||||
|
||||
def 建任务(作者ID: str, 冻结范围=None) -> str:
|
||||
冻结 = {
|
||||
"source_scope": 冻结范围 if 冻结范围 is not None else {"work_id": "wb-work"},
|
||||
"schema_versions": {},
|
||||
"authorization": "workbench-synthetic",
|
||||
"budget": {"context_bytes": 4096},
|
||||
"stop_conditions": {},
|
||||
}
|
||||
return 运行.创建任务(
|
||||
任务请求(
|
||||
"workbench-freeze",
|
||||
f"create-{作者ID}-{uuid4().hex[:6]}",
|
||||
作者ID,
|
||||
用途.生产,
|
||||
内容用途.生成,
|
||||
{},
|
||||
"test",
|
||||
"test",
|
||||
冻结,
|
||||
作品ID="wb-work",
|
||||
),
|
||||
"workbench-freeze",
|
||||
"1",
|
||||
)
|
||||
|
||||
task_id = 建任务("author-local", 范围)
|
||||
他人任务 = 建任务("someone-else")
|
||||
材料 = {"历史:ch-1": "林深把伞收了起来。", "参考:超长素材": "很" * 4000}
|
||||
文稿 = 范围["documents"][0]
|
||||
绑定 = (
|
||||
来源绑定(
|
||||
文稿["document_id"],
|
||||
str(文稿["revision"]),
|
||||
文稿["document_hash"],
|
||||
投影版本("production", "generation"),
|
||||
),
|
||||
)
|
||||
装配.要求上下文().冻结上下文(task_id, 装配材料(材料, ("历史:ch-1",), 字节上限=4096), 绑定)
|
||||
口令文件 = tmp_path / "作者口令.txt"
|
||||
口令文件.write_text("workbench-password")
|
||||
配置 = 应用配置(
|
||||
应用测试库[用途.生产].引用,
|
||||
"test",
|
||||
HTTP=服务配置(str(口令文件), 公开地址="http://testserver", 允许来源=("http://testserver",)),
|
||||
)
|
||||
return {
|
||||
"装配": 装配,
|
||||
"task_id": task_id,
|
||||
"他人任务": 他人任务,
|
||||
"配置": 配置,
|
||||
"口令": 口令文件.read_text(),
|
||||
}
|
||||
|
||||
|
||||
def test_作者可见任务材料清单与来源证据__dd023f(工作台环境):
|
||||
环境 = 工作台环境
|
||||
task_id = 环境["task_id"]
|
||||
with TestClient(创建应用(环境["配置"])) as client:
|
||||
未登录 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots")
|
||||
assert 未登录.status_code == 401
|
||||
client.headers["Origin"] = "http://testserver"
|
||||
assert client.post("/api/v1/session", json={"password": 环境["口令"]}).status_code == 200
|
||||
快照集 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots")
|
||||
assert 快照集.status_code == 200
|
||||
assert len(快照集.json()) == 1
|
||||
快照 = 快照集.json()[0]
|
||||
assert 快照["task_id"] == task_id and 快照["work_id"] == "wb-work"
|
||||
assert 快照["snapshot_id"] and len(快照["input_hash"]) == 64
|
||||
assert 快照["state"] == "有效"
|
||||
assert 快照["invalidation"] is None and 快照["changes"] == []
|
||||
清单 = 快照["assembly"]
|
||||
assert 清单["included_sources"] == ["历史:ch-1"]
|
||||
assert 清单["omissions"] == [{"source": "参考:超长素材", "reason": "BUDGET_EXCEEDED"}]
|
||||
assert 清单["byte_size"] == len("林深把伞收了下来。".encode())
|
||||
assert len(清单["text_hash"]) == 64
|
||||
来源 = 快照["sources"]
|
||||
assert len(来源) == 1
|
||||
文稿 = 范围文稿(环境)
|
||||
assert 来源[0] == {
|
||||
"source_id": 文稿["document_id"],
|
||||
"data_version": "1",
|
||||
"schema_hash": 文稿["document_hash"],
|
||||
"projection_version": "visible-text-codepoints/v1:production:generation",
|
||||
"current_state": "有效",
|
||||
}
|
||||
# 清单只给取舍与身份,不回传装配全文
|
||||
assert "text" not in 清单
|
||||
assert "林深把伞收了下来" not in 快照集.text
|
||||
|
||||
|
||||
def test_他人任务与不存在任务被拒绝__5f6302(工作台环境):
|
||||
环境 = 工作台环境
|
||||
with TestClient(创建应用(环境["配置"])) as client:
|
||||
client.headers["Origin"] = "http://testserver"
|
||||
assert client.post("/api/v1/session", json={"password": 环境["口令"]}).status_code == 200
|
||||
他人 = client.get(f"/api/v1/tasks/{环境['他人任务']}/context-snapshots")
|
||||
assert 他人.status_code == 403
|
||||
assert 他人.json()["code"] == "SCOPE_DENIED"
|
||||
不存在 = client.get("/api/v1/tasks/00000000-0000-0000-0000-000000000000/context-snapshots")
|
||||
assert 不存在.status_code == 422
|
||||
assert 不存在.json()["code"] == "MUSE_TASK"
|
||||
|
||||
|
||||
def test_来源漂移后失效说明可见且历史装配原样__18a1af(工作台环境):
|
||||
环境 = 工作台环境
|
||||
task_id = 环境["task_id"]
|
||||
with TestClient(创建应用(环境["配置"])) as client:
|
||||
client.headers["Origin"] = "http://testserver"
|
||||
assert client.post("/api/v1/session", json={"password": 环境["口令"]}).status_code == 200
|
||||
冻结前 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots").json()[0]
|
||||
作者 = 调用身份("author-local", None, 用途.生产, 内容用途.生成)
|
||||
环境["装配"].要求正文().保存人工(
|
||||
作者, "wb-rev-2", "ch-1", 1, 正文草稿((段落("p-2", (文本节点("第二天清晨。"),)),))
|
||||
)
|
||||
漂移后集 = client.get(f"/api/v1/tasks/{task_id}/context-snapshots").json()
|
||||
assert len(漂移后集) == 1
|
||||
漂移后 = 漂移后集[0]
|
||||
assert 漂移后["snapshot_id"] == 冻结前["snapshot_id"]
|
||||
assert 漂移后["input_hash"] == 冻结前["input_hash"]
|
||||
assert 漂移后["state"] == "SOURCE_STALE"
|
||||
assert 漂移后["invalidation"] and "ch-1" in 漂移后["invalidation"]
|
||||
assert any("前进" in 变化 for 变化 in 漂移后["changes"])
|
||||
assert 漂移后["assembly"] == 冻结前["assembly"]
|
||||
assert 漂移后["sources"][0]["source_id"] == 冻结前["sources"][0]["source_id"]
|
||||
assert 漂移后["sources"][0]["data_version"] == "1"
|
||||
assert 漂移后["sources"][0]["current_state"] == "SOURCE_STALE"
|
||||
|
||||
|
||||
def 范围文稿(环境) -> dict:
|
||||
作者 = 调用身份("author-local", None, 用途.生产, 内容用途.生成)
|
||||
记录 = 环境["装配"].要求正文().读取正文(作者, "ch-1")
|
||||
return 记录
|
||||
511
tests/集成/test_上下文快照消费.py
Normal file
511
tests/集成/test_上下文快照消费.py
Normal file
@ -0,0 +1,511 @@
|
||||
"""任务上下文实际消费的冻结快照:幂等、输入变化新快照、漂移失效与任务追溯。
|
||||
|
||||
合成流程驱动真实 S02、B01/B02/B05 与隔离 PG;不调用模型、不证明完整创作链。
|
||||
"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from psycopg.types.json import Jsonb
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from muse.上下文.接口 import (
|
||||
上下文错误,
|
||||
快照身份,
|
||||
快照载荷,
|
||||
材料装配结果,
|
||||
来源绑定,
|
||||
装配材料,
|
||||
输入哈希,
|
||||
)
|
||||
from muse.任务运行.接口 import (
|
||||
事件类型,
|
||||
任务请求,
|
||||
工具请求,
|
||||
步骤处理器,
|
||||
步骤结果,
|
||||
步骤计划,
|
||||
)
|
||||
from muse.作品规划.接口 import 档案保存, 规划保存, 规划定位
|
||||
from muse.元数据.接口 import 元数据服务, 导入内置结构
|
||||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||||
from muse.启动 import 构建
|
||||
from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据
|
||||
from muse.正式变更.接口 import 作者动作
|
||||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||||
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
|
||||
from muse.配置 import 应用配置
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
工具集 = ("read_history_document", "query_work_plan", "query_facts_as_of")
|
||||
|
||||
|
||||
def 细纲内容(parent_id: str) -> dict:
|
||||
return {
|
||||
"targetChapter": 3,
|
||||
"sourceRef": {"sourceId": parent_id, "sourceVersion": 1},
|
||||
"chapterGoal": "邮差在旧码头确认收信人。",
|
||||
"hardConstraints": ["上一章结尾雨还没停。"],
|
||||
"keyEvents": [
|
||||
{
|
||||
"id": "arrival",
|
||||
"order": 1,
|
||||
"event": "邮差到达旧码头",
|
||||
"participants": ["lin-shen"],
|
||||
"trigger": "收到退回的信",
|
||||
"resultDirection": "确认码头还在使用",
|
||||
}
|
||||
],
|
||||
"adjustableBeats": [],
|
||||
"declaredNewFacts": [],
|
||||
"mustAppearEntities": ["lin-shen"],
|
||||
"chapterEndHook": "仓库的门后有人应声。",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 快照环境(应用测试库):
|
||||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||||
导入内置结构(元数据服务(连))
|
||||
登记 = 流程登记()
|
||||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"), 流程=登记)
|
||||
作者 = 调用身份("freeze-author", None, 用途.生产, 内容用途.生成)
|
||||
作品, 正文 = 装配.要求作品(), 装配.要求正文()
|
||||
结构选择 = 作品.新档案结构(作者, "freeze-work", "work_core", 1)
|
||||
作品.保存档案(
|
||||
作者,
|
||||
"create-freeze-work",
|
||||
档案保存(
|
||||
"freeze-work", 0, {"名称": "旧码头", "题材定位": "现实", "主题立意": "还信"}, 结构选择
|
||||
),
|
||||
)
|
||||
for i in range(1, 4):
|
||||
作品.添加章节(作者, f"add-ch-{i}", "freeze-work", f"ch-{i}", f"第{i}章", 预期目录版本=i - 1)
|
||||
正文.保存人工(
|
||||
作者,
|
||||
"body-ch-1",
|
||||
"ch-1",
|
||||
0,
|
||||
正文草稿((段落("p-ch-1", (文本节点("林深停在码头,雨还没停。"),)),)),
|
||||
)
|
||||
|
||||
规划 = 装配.要求规划()
|
||||
|
||||
def 保存并确认(请求, 命令: str) -> dict:
|
||||
回执 = 规划.保存人工候选(作者, 命令 + "-draft", 请求)
|
||||
候选 = 规划.读取候选(作者, 回执["results"][0]["candidate_id"])
|
||||
定位 = 规划定位(
|
||||
候选["candidate_id"],
|
||||
候选["revision"],
|
||||
候选["candidate_hash"],
|
||||
候选["plan_id"],
|
||||
请求.expected_revision,
|
||||
)
|
||||
审阅 = 规划.打开审阅(作者, 定位)
|
||||
规划.决定(
|
||||
作者,
|
||||
命令 + "-accept",
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("plan",),
|
||||
)
|
||||
return 规划.读取规划(作者, 候选["plan_id"])
|
||||
|
||||
世界 = 装配.要求故事世界()
|
||||
|
||||
def 正文源(章: str, 引文: str) -> 正文依据:
|
||||
记录 = 正文.读取正文(作者, 章)
|
||||
草稿 = TypeAdapter(正文草稿).validate_python(记录["document"])
|
||||
return 正文依据(
|
||||
章, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(草稿, 引文),)
|
||||
)
|
||||
|
||||
def 确认事实(对象, 类型, 内容, 来源=(), 生效="ch-1", 独立=False) -> None:
|
||||
选择 = 世界.新事实结构(作者, "freeze-work", 对象, 类型, 1)
|
||||
回执 = 世界.提出事实(
|
||||
作者,
|
||||
"propose-" + 对象,
|
||||
事实提案请求("freeze-work", 对象, 0, 选择, 内容, tuple(来源), 生效, independent=独立),
|
||||
)
|
||||
提案 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
|
||||
定位 = 事实定位(
|
||||
提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + 对象, 0
|
||||
)
|
||||
审阅 = 世界.打开事实审阅(作者, 定位)
|
||||
世界.决定事实(
|
||||
作者,
|
||||
"confirm-" + 对象,
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("object",),
|
||||
)
|
||||
|
||||
确认事实("lin-shen", "character", {"名称": "林深"}, (正文源("ch-1", "林深停在码头"),))
|
||||
父 = 保存并确认(
|
||||
规划保存(
|
||||
"freeze-work", 0, 规划.新规划结构(作者, "freeze-work", "outline", 1), {"名称": "旧码头"}
|
||||
),
|
||||
"outline",
|
||||
)
|
||||
细纲 = 保存并确认(
|
||||
规划保存(
|
||||
"freeze-work",
|
||||
0,
|
||||
规划.新规划结构(作者, "freeze-work", "fine_outline", 1),
|
||||
细纲内容(父["plan_id"]),
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"fine-outline",
|
||||
)
|
||||
|
||||
def 读取材料(工具) -> tuple[dict, tuple[来源绑定, ...]]:
|
||||
"""经任务只读工具取真实材料与来源绑定;顺序固定为细纲、事实、历史。"""
|
||||
材料: dict[str, str] = {}
|
||||
绑定: list[来源绑定] = []
|
||||
计划 = 工具.调用(工具请求("plan", "query_work_plan", {}))
|
||||
材料["目标章细纲"] = 计划.内容["content"]["chapterGoal"]
|
||||
事实 = 工具.调用(工具请求("facts", "query_facts_as_of", {}))
|
||||
for 项 in 事实.内容["facts"]:
|
||||
名称 = 项["content"].get("名称")
|
||||
if 名称:
|
||||
材料["事实:" + 项["object_id"]] = 名称
|
||||
历史 = 工具.调用(
|
||||
工具请求("history", "read_history_document", {"chapter_id": "ch-1", "revision": 1})
|
||||
)
|
||||
材料["历史:ch-1"] = 历史.内容["text"]
|
||||
for 结果 in (计划, 事实, 历史):
|
||||
绑定.extend(来源绑定(s.来源ID, s.数据版本, s.结构哈希, s.投影版本) for s in 结果.来源)
|
||||
return 材料, tuple(绑定)
|
||||
|
||||
def 冻结并输出(上下文) -> 步骤结果:
|
||||
工具 = 装配.要求只读工具(上下文.领取)
|
||||
材料, 绑定 = 读取材料(工具)
|
||||
上限 = 上下文.任务.冻结输入["冻结上下文"]["budget"]["context_bytes"]
|
||||
装配结果 = 装配材料(材料, tuple(材料), 字节上限=上限)
|
||||
回执 = 装配.要求上下文().冻结上下文(上下文.任务.任务ID, 装配结果, 绑定)
|
||||
return 步骤结果(
|
||||
{
|
||||
"快照": {
|
||||
"snapshot_id": 回执.快照ID,
|
||||
"input_hash": 回执.输入哈希,
|
||||
"created": 回执.新建,
|
||||
},
|
||||
"装配": asdict(装配结果),
|
||||
}
|
||||
)
|
||||
|
||||
登记.登记处理器(
|
||||
步骤处理器(
|
||||
"synthetic-freeze",
|
||||
"1",
|
||||
冻结并输出,
|
||||
"freeze/v1",
|
||||
"freeze/v1",
|
||||
保护职责="范围核对",
|
||||
允许工具=工具集,
|
||||
)
|
||||
)
|
||||
登记.登记类型("synthetic-freeze", 必需保护=("范围核对",))
|
||||
登记.登记恢复检查("synthetic-freeze", "1", lambda 任务: 装配.要求上下文().读取范围(任务.任务ID))
|
||||
运行 = 装配.任务运行
|
||||
assert 运行 is not None
|
||||
流程服务(运行, 登记).发布(
|
||||
流程定义("synthetic-freeze", "1", (步骤计划("read", "synthetic-freeze", "1", 工具=工具集),))
|
||||
)
|
||||
|
||||
def 开始(范围=None):
|
||||
if 范围 is None:
|
||||
范围 = 装配.要求上下文().授权任务范围(
|
||||
作者,
|
||||
"freeze-work",
|
||||
2,
|
||||
({"chapter_id": "ch-1", "revision": 1},),
|
||||
目标章ID="ch-3",
|
||||
含事实=True,
|
||||
)
|
||||
id_ = 运行.创建任务(
|
||||
任务请求(
|
||||
"synthetic-freeze",
|
||||
str(uuid4()),
|
||||
作者.作者,
|
||||
用途.生产,
|
||||
内容用途.生成,
|
||||
{},
|
||||
"test",
|
||||
"test",
|
||||
{
|
||||
"source_scope": 范围,
|
||||
"schema_versions": {},
|
||||
"authorization": "synthetic-author-choice",
|
||||
"budget": {"context_bytes": 4096},
|
||||
"stop_conditions": {"max_reads": 3},
|
||||
},
|
||||
作品ID="freeze-work",
|
||||
),
|
||||
"synthetic-freeze",
|
||||
"1",
|
||||
)
|
||||
领取 = 运行.领取步骤("freeze-test", ["synthetic-freeze"], 租期秒=120)
|
||||
assert 领取 is not None and 领取.任务ID == id_
|
||||
return id_, 领取, 装配.要求只读工具(领取)
|
||||
|
||||
return {
|
||||
"装配": 装配,
|
||||
"作者": 作者,
|
||||
"正文": 正文,
|
||||
"规划": 规划,
|
||||
"开始": 开始,
|
||||
"读取材料": 读取材料,
|
||||
"保存并确认": 保存并确认,
|
||||
"父": 父,
|
||||
"细纲": 细纲,
|
||||
"确认事实": 确认事实,
|
||||
}
|
||||
|
||||
|
||||
def 事件绑定(装配, task_id: str) -> tuple[来源绑定, ...]:
|
||||
"""从工具读取事件的真实留痕重建来源绑定;不经过处理器内存。"""
|
||||
事件 = 装配.任务运行.续接事件(task_id).事件
|
||||
return tuple(
|
||||
来源绑定(r["source_id"], r["revision"], r["schema_hash"], r["projection_version"])
|
||||
for e in 事件
|
||||
if e.类型 == 事件类型.工具读取
|
||||
for r in e.载荷["source_refs"]
|
||||
)
|
||||
|
||||
|
||||
def 重建装配(装配记录: dict) -> 材料装配结果:
|
||||
return 材料装配结果(
|
||||
入选来源=tuple(装配记录["入选来源"]),
|
||||
文本=装配记录["文本"],
|
||||
字节数=装配记录["字节数"],
|
||||
省略=tuple(tuple(项) for 项 in 装配记录["省略"]),
|
||||
)
|
||||
|
||||
|
||||
def test_完整链路冻结回读一致且任务可追溯__b09311(快照环境):
|
||||
环境 = 快照环境
|
||||
装配, 开始 = 环境["装配"], 环境["开始"]
|
||||
task_id, 领取, _ = 开始()
|
||||
结果 = 装配.任务运行.执行一步(领取)
|
||||
回执 = 结果.输出["快照"]
|
||||
assert 回执["created"] is True
|
||||
记录集 = 装配.要求上下文().读取任务快照(task_id)
|
||||
assert len(记录集) == 1
|
||||
快照 = 记录集[0]
|
||||
assert 快照.快照ID == 回执["snapshot_id"]
|
||||
assert 快照.任务ID == task_id and 快照.作品ID == "freeze-work"
|
||||
assert 快照.输入哈希 == 回执["input_hash"]
|
||||
assert 快照.当前状态 == "有效" and 快照.失效说明 is None
|
||||
记录 = 结果.输出["装配"]
|
||||
assert 快照.装配["入选"] == list(记录["入选来源"])
|
||||
assert 快照.装配["省略"] == []
|
||||
assert 快照.装配["文本"] == 记录["文本"]
|
||||
assert 快照.装配["字节数"] == 记录["字节数"] == len(记录["文本"].encode("utf-8"))
|
||||
assert 快照.装配["文本哈希"]
|
||||
绑定 = {b.来源ID: b for b in 快照.来源绑定}
|
||||
assert {环境["细纲"]["plan_id"], "lin-shen"} <= 绑定.keys()
|
||||
assert all((b.数据版本, b.结构哈希, b.投影版本) for b in 快照.来源绑定)
|
||||
按身份 = 装配.要求上下文().读取快照(回执["snapshot_id"])
|
||||
assert (按身份.输入哈希, 按身份.任务ID) == (快照.输入哈希, task_id)
|
||||
# 消费绑定与 S02 工具读取留痕一致,不另造第二套事件账本
|
||||
assert set(事件绑定(装配, task_id)) == set(快照.来源绑定)
|
||||
|
||||
|
||||
def test_重复冻结同一输入幂等为一行__b09312(快照环境):
|
||||
环境 = 快照环境
|
||||
装配, 开始 = 环境["装配"], 环境["开始"]
|
||||
task_id, 领取, _ = 开始()
|
||||
结果 = 装配.任务运行.执行一步(领取)
|
||||
首次 = 结果.输出["快照"]
|
||||
再次 = 装配.要求上下文().冻结上下文(
|
||||
task_id, 重建装配(结果.输出["装配"]), 事件绑定(装配, task_id)
|
||||
)
|
||||
assert 再次.快照ID == 首次["snapshot_id"]
|
||||
assert 再次.输入哈希 == 首次["input_hash"]
|
||||
assert 再次.新建 is False
|
||||
assert len(装配.要求上下文().读取任务快照(task_id)) == 1
|
||||
|
||||
|
||||
def test_装配输入变化产生新快照且不改写旧快照__b09313(快照环境):
|
||||
环境 = 快照环境
|
||||
装配, 开始, 读取材料 = 环境["装配"], 环境["开始"], 环境["读取材料"]
|
||||
task_id, _, 工具 = 开始()
|
||||
服务 = 装配.要求上下文()
|
||||
材料, 绑定 = 读取材料(工具)
|
||||
全套 = 装配材料(材料, tuple(材料), 字节上限=4096)
|
||||
# 同一批真实材料、压减一字节预算:末位历史正文被整项省略
|
||||
压减 = 装配材料(材料, (), 字节上限=全套.字节数 - 1)
|
||||
assert 压减.省略 == (("历史:ch-1", "BUDGET_EXCEEDED"),)
|
||||
首次 = 服务.冻结上下文(task_id, 全套, 绑定)
|
||||
再次 = 服务.冻结上下文(task_id, 压减, 绑定)
|
||||
assert 再次.新建 is True
|
||||
assert 再次.快照ID != 首次.快照ID and 再次.输入哈希 != 首次.输入哈希
|
||||
快照集 = 服务.读取任务快照(task_id)
|
||||
assert len(快照集) == 2
|
||||
旧 = 服务.读取快照(首次.快照ID)
|
||||
assert 旧.装配["字节数"] == 全套.字节数 and 旧.装配["省略"] == []
|
||||
新 = 服务.读取快照(再次.快照ID)
|
||||
assert 新.装配["省略"] == [["历史:ch-1", "BUDGET_EXCEEDED"]]
|
||||
assert 新.装配["字节数"] == 压减.字节数 < 全套.字节数
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"漂移源",
|
||||
["plan", "document", "facts"],
|
||||
ids=["plan-revised", "document-revised", "facts-advanced"],
|
||||
)
|
||||
def test_来源漂移后回读明确失效且历史不改写__b09314(快照环境, 漂移源):
|
||||
环境 = 快照环境
|
||||
装配, 作者, 开始 = 环境["装配"], 环境["作者"], 环境["开始"]
|
||||
task_id, 领取, _ = 开始()
|
||||
结果 = 装配.任务运行.执行一步(领取)
|
||||
身份 = 结果.输出["快照"]["snapshot_id"]
|
||||
服务 = 装配.要求上下文()
|
||||
冻结前 = 服务.读取快照(身份)
|
||||
assert 冻结前.当前状态 == "有效"
|
||||
if 漂移源 == "plan":
|
||||
内容 = 细纲内容(环境["父"]["plan_id"])
|
||||
内容["chapterGoal"] = "作者改过的目标。"
|
||||
环境["保存并确认"](
|
||||
规划保存(
|
||||
"freeze-work",
|
||||
1,
|
||||
环境["规划"].新规划结构(作者, "freeze-work", "fine_outline", 1),
|
||||
内容,
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"revise-outline",
|
||||
)
|
||||
elif 漂移源 == "document":
|
||||
环境["正文"].保存人工(
|
||||
作者, "freeze-rev-2", "ch-1", 1, 正文草稿((段落("p-2", (文本节点("改过的正文"),)),))
|
||||
)
|
||||
else:
|
||||
环境["确认事实"]("late-comer", "character", {"名称": "后来的事实"}, 独立=True)
|
||||
漂移后 = 服务.读取快照(身份)
|
||||
assert 漂移后.当前状态 == "SOURCE_STALE"
|
||||
assert 漂移后.失效说明 and "需重新授权" in 漂移后.失效说明
|
||||
assert 漂移后.快照ID == 身份
|
||||
assert 漂移后.输入哈希 == 冻结前.输入哈希
|
||||
assert 漂移后.装配 == 冻结前.装配
|
||||
assert 漂移后.来源绑定 == 冻结前.来源绑定
|
||||
assert len(服务.读取任务快照(task_id)) == 1
|
||||
|
||||
|
||||
def test_来源版本变化经重新授权产生新快照__b09315(快照环境):
|
||||
环境 = 快照环境
|
||||
装配, 作者, 开始 = 环境["装配"], 环境["作者"], 环境["开始"]
|
||||
task_1, 领取_1, _ = 开始()
|
||||
旧 = 装配.任务运行.执行一步(领取_1).输出["快照"]
|
||||
内容 = 细纲内容(环境["父"]["plan_id"])
|
||||
内容["chapterGoal"] = "作者改过的目标。"
|
||||
环境["保存并确认"](
|
||||
规划保存(
|
||||
"freeze-work",
|
||||
1,
|
||||
环境["规划"].新规划结构(作者, "freeze-work", "fine_outline", 1),
|
||||
内容,
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"revise-outline",
|
||||
)
|
||||
范围 = 装配.要求上下文().授权任务范围(
|
||||
作者,
|
||||
"freeze-work",
|
||||
2,
|
||||
({"chapter_id": "ch-1", "revision": 1},),
|
||||
目标章ID="ch-3",
|
||||
含事实=True,
|
||||
)
|
||||
task_2, 领取_2, _ = 开始(范围)
|
||||
结果 = 装配.任务运行.执行一步(领取_2)
|
||||
新 = 结果.输出["快照"]
|
||||
assert 新["snapshot_id"] != 旧["snapshot_id"] and 新["input_hash"] != 旧["input_hash"]
|
||||
服务 = 装配.要求上下文()
|
||||
旧快照 = 服务.读取快照(旧["snapshot_id"])
|
||||
新快照 = 服务.读取快照(新["snapshot_id"])
|
||||
assert 旧快照.当前状态 == "SOURCE_STALE"
|
||||
assert 新快照.当前状态 == "有效"
|
||||
assert 新快照.任务ID == task_2 and 旧快照.任务ID == task_1
|
||||
旧绑定 = {b.来源ID: b.数据版本 for b in 旧快照.来源绑定}
|
||||
新绑定 = {b.来源ID: b.数据版本 for b in 新快照.来源绑定}
|
||||
旧计划ID = 环境["细纲"]["plan_id"]
|
||||
新计划ID = 新快照.冻结范围["target_plan"]["plan_id"]
|
||||
if 新计划ID == 旧计划ID:
|
||||
assert 新绑定[新计划ID] != 旧绑定[旧计划ID]
|
||||
else:
|
||||
assert 新计划ID not in 旧绑定
|
||||
|
||||
|
||||
def test_绑定越出冻结范围拒绝冻结__b09316(快照环境):
|
||||
环境 = 快照环境
|
||||
装配, 开始, 读取材料 = 环境["装配"], 环境["开始"], 环境["读取材料"]
|
||||
task_id, _, 工具 = 开始()
|
||||
材料, 绑定 = 读取材料(工具)
|
||||
计划绑定 = next(b for b in 绑定 if b.来源ID == 环境["细纲"]["plan_id"])
|
||||
文本 = 材料["目标章细纲"]
|
||||
越界装配 = 材料装配结果(
|
||||
入选来源=("越界材料",), 文本=文本, 字节数=len(文本.encode("utf-8")), 省略=()
|
||||
)
|
||||
越界绑定 = (计划绑定, 来源绑定("not-in-scope", "1", "hash", "proj"))
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
装配.要求上下文().冻结上下文(task_id, 越界装配, 越界绑定)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
assert 装配.要求上下文().读取任务快照(task_id) == ()
|
||||
|
||||
|
||||
def test_存储级同身份不同输入哈希冲突拒绝且不改写原行__b09317(快照环境, 应用测试库):
|
||||
"""同一快照身份对应不同输入哈希时存储层拒绝写入且不改写原行。
|
||||
|
||||
快照身份由任务与输入哈希派生,公开路径不可达本分支;经维护连接
|
||||
直插同 ID 但不同 input_hash 与载荷的行后重放真实冻结,触发防御校验。
|
||||
"""
|
||||
环境 = 快照环境
|
||||
装配, 开始, 读取材料 = 环境["装配"], 环境["开始"], 环境["读取材料"]
|
||||
task_id, _, 工具 = 开始()
|
||||
服务 = 装配.要求上下文()
|
||||
材料, 绑定 = 读取材料(工具)
|
||||
装配结果 = 装配材料(材料, tuple(材料), 字节上限=4096)
|
||||
范围 = 服务.读取范围(task_id)
|
||||
哈希 = 输入哈希(快照载荷(task_id, 范围, 装配结果, 绑定))
|
||||
身份 = 快照身份(task_id, 哈希)
|
||||
占位哈希 = "deadbeef" * 8
|
||||
assert 占位哈希 != 哈希
|
||||
占位装配 = {"入选": [], "省略": [], "字节数": 1, "文本哈希": 占位哈希, "文本": "占位"}
|
||||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||||
连.execute(
|
||||
"INSERT INTO muse_context_snapshot (snapshot_id,run_purpose,task_id,work_id,"
|
||||
"input_hash,frozen_scope,assembly,source_bindings,created_by) "
|
||||
"VALUES (%s,'production',%s,%s,%s,%s,%s,%s,%s)",
|
||||
(
|
||||
身份,
|
||||
task_id,
|
||||
"freeze-work",
|
||||
占位哈希,
|
||||
Jsonb(范围),
|
||||
Jsonb(占位装配),
|
||||
Jsonb([]),
|
||||
"conflict-probe",
|
||||
),
|
||||
)
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
服务.冻结上下文(task_id, 装配结果, 绑定)
|
||||
assert 异常.value.错误码 == "CONTEXT_SNAPSHOT_CONFLICT"
|
||||
assert 身份 in 异常.value.说明
|
||||
# 原行不被改写:仍是直插的占位哈希与占位装配,任务名下仅此一行
|
||||
快照集 = 服务.读取任务快照(task_id)
|
||||
assert len(快照集) == 1
|
||||
assert 快照集[0].快照ID == 身份 and 快照集[0].输入哈希 == 占位哈希
|
||||
assert 快照集[0].装配["文本"] == "占位" and 快照集[0].来源绑定 == ()
|
||||
# 冲突只拒绝同身份不同哈希:不同输入的新冻结仍正常落库
|
||||
压减 = 装配材料(材料, (), 字节上限=装配结果.字节数 - 1)
|
||||
新回执 = 服务.冻结上下文(task_id, 压减, 绑定)
|
||||
assert 新回执.新建 is True and 新回执.快照ID != 身份
|
||||
assert len(服务.读取任务快照(task_id)) == 2
|
||||
423
tests/集成/test_上下文投影读取.py
Normal file
423
tests/集成/test_上下文投影读取.py
Normal file
@ -0,0 +1,423 @@
|
||||
"""目标章细纲与时点事实经 S02 任务以 aiContext 真投影读取;不调用模型。"""
|
||||
|
||||
from dataclasses import asdict
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from muse.上下文.接口 import 上下文错误, 装配材料
|
||||
from muse.任务运行.接口 import (
|
||||
事件类型,
|
||||
任务请求,
|
||||
工具请求,
|
||||
步骤处理器,
|
||||
步骤结果,
|
||||
步骤计划,
|
||||
)
|
||||
from muse.作品规划.接口 import 档案保存, 规划保存, 规划定位
|
||||
from muse.元数据.接口 import (
|
||||
值规则,
|
||||
元数据服务,
|
||||
字段定义,
|
||||
字段用途,
|
||||
导入内置结构,
|
||||
类型定义,
|
||||
结构定义,
|
||||
)
|
||||
from muse.共享.调用身份 import 内容用途, 用途, 调用身份
|
||||
from muse.启动 import 构建
|
||||
from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据
|
||||
from muse.正式变更.接口 import 作者动作
|
||||
from muse.正文写作.接口 import 文本节点, 正文草稿, 段落
|
||||
from muse.编排.接口 import 流程定义, 流程服务, 流程登记
|
||||
from muse.配置 import 应用配置
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
|
||||
def 细纲内容(parent_id: str) -> dict:
|
||||
return {
|
||||
"targetChapter": 3,
|
||||
"sourceRef": {"sourceId": parent_id, "sourceVersion": 1},
|
||||
"chapterGoal": "邮差在旧码头确认收信人。",
|
||||
"hardConstraints": ["上一章结尾雨还没停。"],
|
||||
"keyEvents": [
|
||||
{
|
||||
"id": "arrival",
|
||||
"order": 1,
|
||||
"event": "邮差到达旧码头",
|
||||
"participants": ["lin-shen"],
|
||||
"trigger": "收到退回的信",
|
||||
"resultDirection": "确认码头还在使用",
|
||||
}
|
||||
],
|
||||
"adjustableBeats": [],
|
||||
"declaredNewFacts": [],
|
||||
"mustAppearEntities": ["lin-shen"],
|
||||
"chapterEndHook": "仓库的门后有人应声。",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 投影环境(应用测试库):
|
||||
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
|
||||
元数据 = 元数据服务(连)
|
||||
导入内置结构(元数据)
|
||||
# 动态类型:名称对两种投影可见,秘密备注只对 uiVisible 可见。
|
||||
元数据.登记类型(类型定义("landing", "泊位", "world", "entity", ("entity",)))
|
||||
结构 = 结构定义(
|
||||
"landing",
|
||||
"landing",
|
||||
1,
|
||||
(
|
||||
字段定义(
|
||||
"landing.name",
|
||||
"名称",
|
||||
"名称",
|
||||
值规则("text"),
|
||||
必填=True,
|
||||
用途=字段用途(aiContext=True),
|
||||
),
|
||||
字段定义(
|
||||
"landing.note",
|
||||
"秘密备注",
|
||||
"秘密备注",
|
||||
值规则("text"),
|
||||
用途=字段用途(uiVisible=True, userEditable=True, aiContext=False),
|
||||
),
|
||||
),
|
||||
)
|
||||
元数据.登记结构候选(结构)
|
||||
元数据.发布结构("landing", 1, 结构.内容哈希)
|
||||
登记 = 流程登记()
|
||||
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"), 流程=登记)
|
||||
作者 = 调用身份("ctx-author", None, 用途.生产, 内容用途.生成)
|
||||
作品, 正文 = 装配.要求作品(), 装配.要求正文()
|
||||
结构选择 = 作品.新档案结构(作者, "ctx-work", "work_core", 1)
|
||||
作品.保存档案(
|
||||
作者,
|
||||
"create-ctx-work",
|
||||
档案保存(
|
||||
"ctx-work", 0, {"名称": "旧码头", "题材定位": "现实", "主题立意": "还信"}, 结构选择
|
||||
),
|
||||
)
|
||||
for i in range(1, 5):
|
||||
作品.添加章节(作者, f"add-ch-{i}", "ctx-work", f"ch-{i}", f"第{i}章", 预期目录版本=i - 1)
|
||||
for 章, 文本 in [
|
||||
("ch-1", "林深停在码头,雨还没停。"),
|
||||
("ch-3", "林深来到北岸。"),
|
||||
]:
|
||||
正文.保存人工(作者, f"body-{章}", 章, 0, 正文草稿((段落(f"p-{章}", (文本节点(文本),)),)))
|
||||
外结构 = 作品.新档案结构(作者, "other-work", "work_core", 1)
|
||||
作品.保存档案(
|
||||
作者,
|
||||
"create-other-work",
|
||||
档案保存("other-work", 0, {"名称": "别的书", "题材定位": "现实", "主题立意": "无"}, 外结构),
|
||||
)
|
||||
作品.添加章节(作者, "add-foreign", "other-work", "foreign", "外章", 预期目录版本=0)
|
||||
|
||||
规划 = 装配.要求规划()
|
||||
|
||||
def 保存并确认(请求, 命令: str) -> dict:
|
||||
回执 = 规划.保存人工候选(作者, 命令 + "-draft", 请求)
|
||||
候选 = 规划.读取候选(作者, 回执["results"][0]["candidate_id"])
|
||||
定位 = 规划定位(
|
||||
候选["candidate_id"],
|
||||
候选["revision"],
|
||||
候选["candidate_hash"],
|
||||
候选["plan_id"],
|
||||
请求.expected_revision,
|
||||
)
|
||||
审阅 = 规划.打开审阅(作者, 定位)
|
||||
规划.决定(
|
||||
作者,
|
||||
命令 + "-accept",
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("plan",),
|
||||
)
|
||||
return 规划.读取规划(作者, 候选["plan_id"])
|
||||
|
||||
世界 = 装配.要求故事世界()
|
||||
|
||||
def 正文源(章: str, 引文: str) -> 正文依据:
|
||||
记录 = 正文.读取正文(作者, 章)
|
||||
草稿 = TypeAdapter(正文草稿).validate_python(记录["document"])
|
||||
return 正文依据(
|
||||
章, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(草稿, 引文),)
|
||||
)
|
||||
|
||||
def 确认事实(对象, 类型, 内容, 来源=(), 生效="ch-1", 独立=False) -> None:
|
||||
选择 = 世界.新事实结构(作者, "ctx-work", 对象, 类型, 1)
|
||||
回执 = 世界.提出事实(
|
||||
作者,
|
||||
"propose-" + 对象,
|
||||
事实提案请求("ctx-work", 对象, 0, 选择, 内容, tuple(来源), 生效, independent=独立),
|
||||
)
|
||||
提案 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"])
|
||||
定位 = 事实定位(
|
||||
提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + 对象, 0
|
||||
)
|
||||
审阅 = 世界.打开事实审阅(作者, 定位)
|
||||
世界.决定事实(
|
||||
作者,
|
||||
"confirm-" + 对象,
|
||||
定位,
|
||||
作者动作.采纳,
|
||||
审阅["review_id"],
|
||||
审阅["review_hash"],
|
||||
("object",),
|
||||
)
|
||||
|
||||
确认事实("lin-shen", "character", {"名称": "林深"}, (正文源("ch-1", "林深停在码头"),))
|
||||
确认事实("dock-yard", "landing", {"名称": "旧码头", "秘密备注": "夜里卸货"}, 独立=True)
|
||||
确认事实("secret-cargo", "character", {"名称": "夜间货物"}, (正文源("ch-3", "林深来到北岸"),))
|
||||
|
||||
父 = 保存并确认(
|
||||
规划保存(
|
||||
"ctx-work", 0, 规划.新规划结构(作者, "ctx-work", "outline", 1), {"名称": "旧码头"}
|
||||
),
|
||||
"outline",
|
||||
)
|
||||
细纲 = 保存并确认(
|
||||
规划保存(
|
||||
"ctx-work",
|
||||
0,
|
||||
规划.新规划结构(作者, "ctx-work", "fine_outline", 1),
|
||||
细纲内容(父["plan_id"]),
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"fine-outline",
|
||||
)
|
||||
|
||||
def 执行(上下文):
|
||||
工具 = 装配.要求只读工具(上下文.领取)
|
||||
材料 = {}
|
||||
计划 = 工具.调用(工具请求("plan", "query_work_plan", {}))
|
||||
材料["目标章细纲"] = 计划.内容["content"]["chapterGoal"]
|
||||
事实 = 工具.调用(工具请求("facts", "query_facts_as_of", {}))
|
||||
for 项 in 事实.内容["facts"]:
|
||||
名称 = 项["content"].get("名称")
|
||||
if 名称:
|
||||
材料["事实:" + 项["object_id"]] = 名称
|
||||
历史 = 工具.调用(
|
||||
工具请求("history", "read_history_document", {"chapter_id": "ch-1", "revision": 1})
|
||||
)
|
||||
材料["历史:ch-1"] = 历史.内容["text"]
|
||||
上限 = 上下文.任务.冻结输入["冻结上下文"]["budget"]["context_bytes"]
|
||||
return 步骤结果(asdict(装配材料(材料, tuple(材料), 字节上限=上限)))
|
||||
|
||||
登记.登记处理器(
|
||||
步骤处理器(
|
||||
"synthetic-projection",
|
||||
"1",
|
||||
执行,
|
||||
"projection/v1",
|
||||
"projection/v1",
|
||||
保护职责="范围核对",
|
||||
允许工具=("read_history_document", "query_work_plan", "query_facts_as_of"),
|
||||
)
|
||||
)
|
||||
登记.登记类型("synthetic-projection", 必需保护=("范围核对",))
|
||||
登记.登记恢复检查(
|
||||
"synthetic-projection", "1", lambda 任务: 装配.要求上下文().读取范围(任务.任务ID)
|
||||
)
|
||||
运行 = 装配.任务运行
|
||||
assert 运行 is not None
|
||||
流程服务(运行, 登记).发布(
|
||||
流程定义(
|
||||
"synthetic-projection",
|
||||
"1",
|
||||
(
|
||||
步骤计划(
|
||||
"read",
|
||||
"synthetic-projection",
|
||||
"1",
|
||||
工具=("read_history_document", "query_work_plan", "query_facts_as_of"),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def 开始(范围=None, 任务内容用途=内容用途.生成):
|
||||
if 范围 is None:
|
||||
范围 = 装配.要求上下文().授权任务范围(
|
||||
作者,
|
||||
"ctx-work",
|
||||
2,
|
||||
({"chapter_id": "ch-1", "revision": 1},),
|
||||
目标章ID="ch-3",
|
||||
含事实=True,
|
||||
)
|
||||
id_ = 运行.创建任务(
|
||||
任务请求(
|
||||
"synthetic-projection",
|
||||
str(uuid4()),
|
||||
作者.作者,
|
||||
用途.生产,
|
||||
任务内容用途,
|
||||
{},
|
||||
"test",
|
||||
"test",
|
||||
{
|
||||
"source_scope": 范围,
|
||||
"schema_versions": {},
|
||||
"authorization": "synthetic-author-choice",
|
||||
"budget": {"context_bytes": 2048},
|
||||
"stop_conditions": {"max_reads": 3},
|
||||
},
|
||||
作品ID="ctx-work",
|
||||
),
|
||||
"synthetic-projection",
|
||||
"1",
|
||||
)
|
||||
领取 = 运行.领取步骤("projection-test", ["synthetic-projection"], 租期秒=120)
|
||||
assert 领取 is not None and 领取.任务ID == id_
|
||||
return id_, 领取, 装配.要求只读工具(领取)
|
||||
|
||||
return {
|
||||
"装配": 装配,
|
||||
"作者": 作者,
|
||||
"开始": 开始,
|
||||
"保存并确认": 保存并确认,
|
||||
"父": 父,
|
||||
"细纲": 细纲,
|
||||
"确认事实": 确认事实,
|
||||
"正文源": 正文源,
|
||||
"规划": 规划,
|
||||
}
|
||||
|
||||
|
||||
def test_细纲与事实经真实任务读取留痕并进入预算__b09201(投影环境):
|
||||
环境 = 投影环境
|
||||
装配, 开始, 细纲 = 环境["装配"], 环境["开始"], 环境["细纲"]
|
||||
task_id, 领取, _ = 开始()
|
||||
结果 = 装配.任务运行.执行一步(领取)
|
||||
assert set(结果.输出["入选来源"]) == {
|
||||
"目标章细纲",
|
||||
"事实:dock-yard",
|
||||
"事实:lin-shen",
|
||||
"事实:secret-cargo",
|
||||
"历史:ch-1",
|
||||
}
|
||||
assert len(结果.输出["入选来源"]) == 5
|
||||
读取 = [e for e in 装配.任务运行.续接事件(task_id).事件 if e.类型 == 事件类型.工具读取]
|
||||
assert [e.载荷["tool"] for e in 读取] == [
|
||||
"query_work_plan",
|
||||
"query_facts_as_of",
|
||||
"read_history_document",
|
||||
]
|
||||
计划引用 = 读取[0].载荷["source_refs"][0]
|
||||
assert 计划引用["source_id"] == 细纲["plan_id"]
|
||||
assert 计划引用["revision"] == "1" and 计划引用["schema_hash"]
|
||||
事实身份 = {r["source_id"] for e in 读取 for r in e.载荷["source_refs"]}
|
||||
assert {"lin-shen", "dock-yard", "secret-cargo"} <= 事实身份
|
||||
|
||||
|
||||
def test_目标细纲按任务用途返回真投影__b09202(投影环境):
|
||||
_, _, 工具 = 投影环境["开始"]()
|
||||
结果 = 工具.调用(工具请求("plan", "query_work_plan", {}))
|
||||
内容 = 结果.内容
|
||||
assert 内容["chapter_id"] == "ch-3"
|
||||
assert 内容["content"]["chapterGoal"] == "邮差在旧码头确认收信人。"
|
||||
assert 内容["content"]["targetChapter"] == 3
|
||||
assert 内容["content"]["sourceRef"]["sourceVersion"] == 1
|
||||
assert 内容["run_purpose"] == "production"
|
||||
assert 内容["content_purpose"] == "generation"
|
||||
assert 结果.来源[0].数据版本 == "1"
|
||||
assert 结果.来源[0].投影版本
|
||||
|
||||
|
||||
def test_时点事实按冻结系统版本与视角收窄返回__b09203(投影环境):
|
||||
_, _, 工具 = 投影环境["开始"]()
|
||||
作者视角 = 工具.调用(工具请求("facts", "query_facts_as_of", {}))
|
||||
对象 = {项["object_id"] for 项 in 作者视角.内容["facts"]}
|
||||
assert {"lin-shen", "dock-yard", "secret-cargo"} <= 对象
|
||||
assert 作者视角.内容["run_purpose"] == "production"
|
||||
assert 作者视角.内容["content_purpose"] == "generation"
|
||||
读者视角 = 工具.调用(工具请求("facts", "query_facts_as_of", {"view": "reader"}))
|
||||
读者对象 = {项["object_id"] for 项 in 读者视角.内容["facts"]}
|
||||
assert "secret-cargo" not in 读者对象 and "lin-shen" in 读者对象
|
||||
省略 = {项["object_id"]: 项["reason"] for 项 in 读者视角.内容["omitted"]}
|
||||
assert 省略["secret-cargo"] == "FUTURE_EVIDENCE"
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
工具.调用(
|
||||
工具请求(
|
||||
"facts",
|
||||
"query_facts_as_of",
|
||||
{"view": "character", "knower_id": "missing-actor"},
|
||||
)
|
||||
)
|
||||
assert 异常.value.错误码 == "SCOPE_DENIED"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("漂移源", ["plan", "facts"], ids=["plan-revised", "facts-advanced"])
|
||||
def test_细纲或事实漂移后拒绝沿用原任务__b09204(投影环境, 漂移源):
|
||||
环境 = 投影环境
|
||||
_, _, 工具 = 环境["开始"]()
|
||||
if 漂移源 == "plan":
|
||||
内容 = 细纲内容(环境["父"]["plan_id"])
|
||||
内容["chapterGoal"] = "作者改过的目标。"
|
||||
环境["保存并确认"](
|
||||
规划保存(
|
||||
"ctx-work",
|
||||
1,
|
||||
环境["规划"].新规划结构(环境["作者"], "ctx-work", "fine_outline", 1),
|
||||
内容,
|
||||
chapter_id="ch-3",
|
||||
),
|
||||
"revise-outline",
|
||||
)
|
||||
else:
|
||||
环境["确认事实"]("late-comer", "character", {"名称": "后来的事实"}, 独立=True)
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
工具.调用(
|
||||
工具请求(
|
||||
"stale",
|
||||
"query_work_plan" if 漂移源 == "plan" else "query_facts_as_of",
|
||||
{},
|
||||
)
|
||||
)
|
||||
assert 异常.value.错误码 == "SOURCE_STALE"
|
||||
|
||||
|
||||
def test_aiContext投影与uiVisible差异真实生效__b09205(投影环境):
|
||||
环境 = 投影环境
|
||||
装配, 作者, 开始 = 环境["装配"], 环境["作者"], 环境["开始"]
|
||||
_, _, 工具 = 开始()
|
||||
界面 = 装配.要求故事世界().查询世界(作者, "ctx-work", "ch-2")
|
||||
泊位 = next(项 for 项 in 界面["facts"] if 项["object_id"] == "dock-yard")
|
||||
assert 泊位["content"]["秘密备注"] == "夜里卸货"
|
||||
投影 = 工具.调用(工具请求("facts", "query_facts_as_of", {}))
|
||||
工具泊位 = next(项 for 项 in 投影.内容["facts"] if 项["object_id"] == "dock-yard")
|
||||
assert "秘密备注" not in 工具泊位["content"]
|
||||
assert 工具泊位["omitted_fields"]["秘密备注"] == "FIELD_NOT_ALLOWED"
|
||||
assert 工具泊位["content"]["名称"] == "旧码头"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("目标章", "截止", "错误码"),
|
||||
[
|
||||
("ch-1", 2, "SCOPE_DENIED"),
|
||||
("ch-2", 2, "SCOPE_DENIED"),
|
||||
("foreign", 2, "SCOPE_DENIED"),
|
||||
("ch-4", 3, "PRECONDITION_PENDING"),
|
||||
],
|
||||
ids=["past-chapter", "cutoff-chapter", "cross-work", "unconfirmed-outline"],
|
||||
)
|
||||
def test_目标章不在截止之后紧邻或无细纲拒绝授权__b09206(投影环境, 目标章, 截止, 错误码):
|
||||
环境 = 投影环境
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
环境["装配"].要求上下文().授权任务范围(
|
||||
环境["作者"], "ctx-work", 截止, (), 目标章ID=目标章, 含事实=True
|
||||
)
|
||||
assert 异常.value.错误码 == 错误码
|
||||
|
||||
|
||||
def test_任务用途与冻结范围不一致拒绝__b09207(投影环境):
|
||||
with pytest.raises(上下文错误) as 异常:
|
||||
投影环境["开始"](任务内容用途=内容用途.规划)
|
||||
assert 异常.value.错误码 == "CONTEXT_SCOPE_INVALID"
|
||||
2
web/src/功能/上下文/index.ts
Normal file
2
web/src/功能/上下文/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { Ui任务上下文, Ui上下文清单 } from "./上下文清单";
|
||||
export { Ui来源证据面板 } from "./来源证据面板";
|
||||
83
web/src/功能/上下文/上下文清单.tsx
Normal file
83
web/src/功能/上下文/上下文清单.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
/** 任务材料清单:入选来源、省略原因与装配规模;只读展示冻结取舍。 */
|
||||
import { 快照状态, 省略原因, useMuse任务上下文 } from "./数据";
|
||||
import type { 任务上下文快照 } from "./数据";
|
||||
import { Ui来源证据面板 } from "./来源证据面板";
|
||||
import { Ui反馈状态 } from "../../界面/反馈状态";
|
||||
|
||||
export function Ui任务上下文({
|
||||
taskId,
|
||||
scope,
|
||||
}: {
|
||||
taskId: string;
|
||||
scope: string | null;
|
||||
}) {
|
||||
const query = useMuse任务上下文(scope, taskId);
|
||||
return (
|
||||
<section
|
||||
className="paper-panel"
|
||||
style={{ gridColumn: "1 / -1" }}
|
||||
aria-label="任务材料"
|
||||
>
|
||||
<div className="panel-heading">
|
||||
<h2>任务材料</h2>
|
||||
<span className="muted">{query.data?.length ?? 0} 份冻结快照</span>
|
||||
</div>
|
||||
{query.isPending ? (
|
||||
<Ui反馈状态 message="正在读取任务材料…" />
|
||||
) : query.error ? (
|
||||
<Ui反馈状态 message={query.error.message} error />
|
||||
) : query.data?.length ? (
|
||||
query.data.map((快照, 序) => (
|
||||
<article key={快照.snapshot_id}>
|
||||
<div className="panel-heading">
|
||||
<h2>材料快照 {String(序 + 1).padStart(2, "0")}</h2>
|
||||
<span className="state-pill">
|
||||
{快照状态[快照.state] ?? 快照.state}
|
||||
</span>
|
||||
</div>
|
||||
<Ui上下文清单 snapshot={快照} />
|
||||
<Ui来源证据面板 snapshot={快照} />
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<h2>这个任务还没有冻结材料快照</h2>
|
||||
<p>任务实际消费的材料会在冻结后出现在这里,并标注每处来源的版本。</p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function Ui上下文清单({ snapshot }: { snapshot: 任务上下文快照 }) {
|
||||
const 装配 = snapshot.assembly;
|
||||
return (
|
||||
<div>
|
||||
<ol className="step-list">
|
||||
{装配.included_sources.map((来源, 序) => (
|
||||
<li key={来源}>
|
||||
<span className="step-number">
|
||||
{String(序 + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<div>
|
||||
<strong>{来源}</strong>
|
||||
<span>已按冻结版本装配</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{装配.omissions.map((条目) => (
|
||||
<li key={条目.source}>
|
||||
<span className="step-number">省</span>
|
||||
<div>
|
||||
<strong>{条目.source}</strong>
|
||||
<span>省略原因:{省略原因[条目.reason] ?? 条目.reason}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="mono muted">
|
||||
材料 {装配.byte_size} 字节 · 文本哈希 {装配.text_hash}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
web/src/功能/上下文/数据.ts
Normal file
32
web/src/功能/上下文/数据.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/** 上下文功能的数据接入:任务材料快照的查询与稳定标签。 */
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { 客户端 } from "../../接口/生成/客户端";
|
||||
import { 查询键 } from "../../接口/查询键";
|
||||
import type { operations } from "../../接口/生成/类型";
|
||||
|
||||
export type 任务上下文快照 =
|
||||
operations["list_context_snapshots"]["responses"][200]["content"]["application/json"][number];
|
||||
|
||||
/** 快照键挂在任务键之下:任务事件失效任务查询时一并刷新材料。 */
|
||||
export function useMuse任务上下文(scope: string | null, taskId: string) {
|
||||
return useQuery({
|
||||
queryKey: 查询键(scope, taskId, "context-snapshots"),
|
||||
enabled: !!taskId,
|
||||
queryFn: async ({ signal }) =>
|
||||
(
|
||||
await 客户端.GET("/api/v1/tasks/{task_id}/context-snapshots", {
|
||||
params: { path: { task_id: taskId }, signal },
|
||||
})
|
||||
).data!,
|
||||
});
|
||||
}
|
||||
|
||||
export const 快照状态: Record<string, string> = {
|
||||
有效: "仍可用于回读",
|
||||
SOURCE_STALE: "来源已漂移,需重新授权",
|
||||
};
|
||||
|
||||
export const 省略原因: Record<string, string> = {
|
||||
BUDGET_EXCEEDED: "超出材料预算,整项省略",
|
||||
EMPTY: "内容为空,没有可装配材料",
|
||||
};
|
||||
43
web/src/功能/上下文/来源证据面板.tsx
Normal file
43
web/src/功能/上下文/来源证据面板.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
/** 来源证据面板:逐来源版本、结构哈希、投影版本与当前状态和失效说明。 */
|
||||
import { 快照状态 } from "./数据";
|
||||
import type { 任务上下文快照 } from "./数据";
|
||||
|
||||
export function Ui来源证据面板({ snapshot }: { snapshot: 任务上下文快照 }) {
|
||||
const 已失效 = snapshot.state !== "有效";
|
||||
return (
|
||||
<div>
|
||||
<div className="panel-heading">
|
||||
<h2>来源证据</h2>
|
||||
<span className="muted">{snapshot.sources.length} 处来源</span>
|
||||
</div>
|
||||
{已失效 && (
|
||||
<p role="status">{snapshot.invalidation ?? 快照状态[snapshot.state]}</p>
|
||||
)}
|
||||
{已失效 && snapshot.changes.length > 0 && (
|
||||
<ul className="event-list" aria-label="失效说明">
|
||||
{snapshot.changes.map((变化) => (
|
||||
<li key={变化}>
|
||||
<span className="mono">变化</span>
|
||||
<div>{变化}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<ul className="event-list">
|
||||
{snapshot.sources.map((来源) => (
|
||||
<li key={来源.source_id}>
|
||||
<span className="mono">版本 {来源.data_version}</span>
|
||||
<div>
|
||||
<strong className="mono">{来源.source_id}</strong>
|
||||
<span className="mono">结构哈希 {来源.schema_hash}</span>
|
||||
<span className="mono">投影版本 {来源.projection_version}</span>
|
||||
</div>
|
||||
<span className="state-pill">
|
||||
{来源.current_state === "有效" ? "仍有效" : "已漂移"}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Ui任务上下文 } from "../上下文";
|
||||
import { Ui恢复控制 } from "./恢复控制";
|
||||
import { 订阅任务事件 } from "../../接口/事件连接";
|
||||
import { 查询键 } from "../../接口/查询键";
|
||||
@ -74,6 +75,7 @@ export function Ui任务详情({
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
<Ui任务上下文 taskId={task.task_id} scope={scope} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -225,6 +225,23 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/tasks/{task_id}/context-snapshots": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** 列出任务快照 */
|
||||
get: operations["list_context_snapshots"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/schemas/commands": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -2293,6 +2310,26 @@ export interface components {
|
||||
/** Author Id */
|
||||
author_id: string;
|
||||
};
|
||||
/** 快照响应 */
|
||||
muse______http_______________: {
|
||||
/** Snapshot Id */
|
||||
snapshot_id: string;
|
||||
/** Task Id */
|
||||
task_id: string;
|
||||
/** Work Id */
|
||||
work_id: string;
|
||||
/** Input Hash */
|
||||
input_hash: string;
|
||||
/** State */
|
||||
state: string;
|
||||
/** Invalidation */
|
||||
invalidation: string | null;
|
||||
/** Changes */
|
||||
changes: string[];
|
||||
assembly: components["schemas"]["muse______http___________________2"];
|
||||
/** Sources */
|
||||
sources: components["schemas"]["muse______http___________________3"][];
|
||||
};
|
||||
/** 步骤响应 */
|
||||
muse______http__________________1: {
|
||||
/** Step Id */
|
||||
@ -2314,6 +2351,37 @@ export interface components {
|
||||
/** Steps */
|
||||
steps: components["schemas"]["muse______http__________________1"][];
|
||||
};
|
||||
/** 省略条目响应 */
|
||||
muse______http___________________1: {
|
||||
/** Source */
|
||||
source: string;
|
||||
/** Reason */
|
||||
reason: string;
|
||||
};
|
||||
/** 装配清单响应 */
|
||||
muse______http___________________2: {
|
||||
/** Included Sources */
|
||||
included_sources: string[];
|
||||
/** Text Hash */
|
||||
text_hash: string;
|
||||
/** Byte Size */
|
||||
byte_size: number;
|
||||
/** Omissions */
|
||||
omissions: components["schemas"]["muse______http___________________1"][];
|
||||
};
|
||||
/** 来源证据响应 */
|
||||
muse______http___________________3: {
|
||||
/** Source Id */
|
||||
source_id: string;
|
||||
/** Data Version */
|
||||
data_version: string;
|
||||
/** Schema Hash */
|
||||
schema_hash: string;
|
||||
/** Projection Version */
|
||||
projection_version: string;
|
||||
/** Current State */
|
||||
current_state: string;
|
||||
};
|
||||
/** 会话授权请求 */
|
||||
muse______http____________________1: {
|
||||
/** Command Id */
|
||||
@ -2848,6 +2916,39 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
list_context_snapshots: {
|
||||
parameters: {
|
||||
query?: {
|
||||
work_id?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
task_id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["muse______http_______________"][];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
schema_command: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
111
web/tests/交互/上下文材料.test.tsx
Normal file
111
web/tests/交互/上下文材料.test.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
/** 组件传输替身只验证渲染与取数路径;真实后端旅程由 Playwright 承担。 */
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { Ui依赖装配 } from "../../src/应用/依赖装配";
|
||||
import { Ui任务上下文 } from "../../src/功能/上下文";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const 有效快照 = {
|
||||
snapshot_id: "snap-1",
|
||||
task_id: "task-1",
|
||||
work_id: "work-1",
|
||||
input_hash: "a".repeat(64),
|
||||
state: "有效",
|
||||
invalidation: null,
|
||||
changes: [] as string[],
|
||||
assembly: {
|
||||
included_sources: ["目标章细纲", "历史:ch-1"],
|
||||
text_hash: "b".repeat(64),
|
||||
byte_size: 328,
|
||||
omissions: [{ source: "参考:超长素材", reason: "BUDGET_EXCEEDED" }],
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
source_id: "doc-1",
|
||||
data_version: "1",
|
||||
schema_hash: "c".repeat(64),
|
||||
projection_version: "visible-text-codepoints/v1:production:generation",
|
||||
current_state: "有效",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function 桩快照(快照: unknown) {
|
||||
return vi.fn(async (request: Request) => {
|
||||
expect(new URL(request.url).pathname).toBe(
|
||||
"/api/v1/tasks/task-1/context-snapshots",
|
||||
);
|
||||
return new Response(JSON.stringify(快照), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("NC-ui-context-snapshot-list:材料清单与省略原因渲染", async () => {
|
||||
vi.stubGlobal("fetch", 桩快照([有效快照]));
|
||||
render(
|
||||
<Ui依赖装配>
|
||||
<Ui任务上下文 taskId="task-1" scope="work-1" />
|
||||
</Ui依赖装配>,
|
||||
);
|
||||
expect(
|
||||
await screen.findByText("目标章细纲", { selector: ".step-list strong" }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("历史:ch-1")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText("省略原因:超出材料预算,整项省略"),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText(/材料 328 字节/)).toBeTruthy();
|
||||
expect(screen.getByText(new RegExp("b".repeat(64)))).toBeTruthy();
|
||||
expect(screen.getByText("doc-1")).toBeTruthy();
|
||||
expect(screen.getByText("版本 1")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/visible-text-codepoints\/v1:production:generation/),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("仍有效")).toBeTruthy();
|
||||
expect(screen.queryByRole("status")).toBeNull();
|
||||
});
|
||||
|
||||
it("NC-ui-context-stale-evidence:来源漂移后失效标注与具体变化可见", async () => {
|
||||
const 漂移快照 = {
|
||||
...有效快照,
|
||||
state: "SOURCE_STALE",
|
||||
invalidation: "文稿 ch-1 已漂移(当前版本 2,冻结版本 1);冻结范围当前失效,需重新授权",
|
||||
changes: ["文稿 ch-1 已从版本 1 前进到 2;来源锁定失效"],
|
||||
sources: [
|
||||
{ ...有效快照.sources[0], data_version: "1", current_state: "SOURCE_STALE" },
|
||||
],
|
||||
};
|
||||
vi.stubGlobal("fetch", 桩快照([漂移快照]));
|
||||
render(
|
||||
<Ui依赖装配>
|
||||
<Ui任务上下文 taskId="task-1" scope="work-1" />
|
||||
</Ui依赖装配>,
|
||||
);
|
||||
expect(await screen.findByText("来源已漂移,需重新授权")).toBeTruthy();
|
||||
expect(screen.getByText(/文稿 ch-1 已漂移/)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText("文稿 ch-1 已从版本 1 前进到 2;来源锁定失效"),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("已漂移")).toBeTruthy();
|
||||
// 失效不改写历史装配:清单与省略原因仍按冻结时原样显示。
|
||||
expect(screen.getByText("历史:ch-1")).toBeTruthy();
|
||||
expect(screen.getByText("省略原因:超出材料预算,整项省略")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("NC-ui-context-empty-state:无快照任务如实显示空态", async () => {
|
||||
vi.stubGlobal("fetch", 桩快照([]));
|
||||
render(
|
||||
<Ui依赖装配>
|
||||
<Ui任务上下文 taskId="task-1" scope={null} />
|
||||
</Ui依赖装配>,
|
||||
);
|
||||
expect(
|
||||
await screen.findByText("这个任务还没有冻结材料快照"),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("0 份冻结快照")).toBeTruthy();
|
||||
});
|
||||
103
web/tests/旅程/任务材料.spec.ts
Normal file
103
web/tests/旅程/任务材料.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
/** 真实浏览器和隔离库:任务材料清单、来源证据与漂移后的失效说明可见。 */
|
||||
import { readFileSync } from "node:fs";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.use({ trace: "off" });
|
||||
|
||||
test("NC-browser-context-workbench:任务材料清单、来源证据与失效说明", async ({
|
||||
page,
|
||||
}, info) => {
|
||||
if (!process.env.MUSE_WORKBENCH_URL || !process.env.MUSE_AUTHOR_PASSWORD_FILE)
|
||||
throw new Error("需要明确的隔离后端和作者口令文件。");
|
||||
const taskId = process.env.MUSE_CONTEXT_TASK_ID!;
|
||||
const chapterId = process.env.MUSE_CONTEXT_CHAPTER_ID ?? "ch-1";
|
||||
if (!process.env.MUSE_CONTEXT_TASK_ID)
|
||||
throw new Error("需要带冻结快照的任务编号(MUSE_CONTEXT_TASK_ID)。");
|
||||
const consoleErrors: string[] = [];
|
||||
page.on("pageerror", (error) => consoleErrors.push(String(error)));
|
||||
|
||||
await page.goto("/");
|
||||
await page
|
||||
.getByLabel("访问口令")
|
||||
.fill(readFileSync(process.env.MUSE_AUTHOR_PASSWORD_FILE, "utf8").trim());
|
||||
await page.getByRole("button", { name: "进入工作台" }).click();
|
||||
await page.getByRole("link", { name: /任务记录/ }).waitFor();
|
||||
// 连接前探测会话的 401 与 favicon 404 属正常噪声;只记录进入工作台后的真实错误。
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error" && !/favicon/.test(message.text()))
|
||||
consoleErrors.push(message.text());
|
||||
});
|
||||
|
||||
async function 打开任务() {
|
||||
await page.goto("/tasks");
|
||||
await page.getByLabel("任务编号").fill(taskId);
|
||||
await page.getByRole("button", { name: "查看任务" }).click();
|
||||
await expect(
|
||||
page.locator('section[aria-label="任务材料"]'),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
await 打开任务();
|
||||
const 材料 = page.locator('section[aria-label="任务材料"]');
|
||||
await expect(材料.getByText("材料快照 01")).toBeVisible();
|
||||
await expect(材料.getByText("仍可用于回读")).toBeVisible();
|
||||
await expect(材料.getByText("历史:ch-1")).toBeVisible();
|
||||
await expect(材料.getByText("已按冻结版本装配")).toBeVisible();
|
||||
await expect(材料.getByText("参考:超长素材")).toBeVisible();
|
||||
await expect(
|
||||
材料.getByText("省略原因:超出材料预算,整项省略"),
|
||||
).toBeVisible();
|
||||
await expect(材料.getByText(/材料 \d+ 字节/)).toBeVisible();
|
||||
await expect(材料.getByText(/文本哈希 [0-9a-f]{64}/)).toBeVisible();
|
||||
await expect(材料.getByText("来源证据")).toBeVisible();
|
||||
await expect(材料.getByText("1 处来源")).toBeVisible();
|
||||
await expect(材料.getByText("版本 1", { exact: true })).toBeVisible();
|
||||
await expect(
|
||||
材料.getByText(/visible-text-codepoints\/v1:production:generation/),
|
||||
).toBeVisible();
|
||||
await expect(材料.getByText("仍有效")).toBeVisible();
|
||||
|
||||
// 造一处真实漂移:以当前正文版本为基线保存新版本,冻结快照的来源随即失效。
|
||||
const 当前 = await (
|
||||
await page.request.get(`/api/v1/chapters/${chapterId}/documents`)
|
||||
).json();
|
||||
const save = await page.request.post(`/api/v1/chapters/${chapterId}/documents`, {
|
||||
headers: { Origin: process.env.MUSE_WORKBENCH_URL },
|
||||
data: {
|
||||
command_id: `browser-drift-revision-${当前.revision}`,
|
||||
chapter_id: chapterId,
|
||||
expected_revision: 当前.revision,
|
||||
document: {
|
||||
format_version: 1,
|
||||
paragraphs: [
|
||||
{
|
||||
paragraph_id: "p-2",
|
||||
content: [{ type: "text", text: "第二天清晨,雨停了。", marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(save.ok()).toBeTruthy();
|
||||
await 打开任务();
|
||||
await expect(材料.getByText("来源已漂移,需重新授权")).toBeVisible();
|
||||
await expect(材料.getByText(/文稿 ch-1 已漂移/)).toBeVisible();
|
||||
await expect(
|
||||
材料.getByText(/文稿 ch-1 已从版本 \d+ 前进到 \d+/),
|
||||
).toBeVisible();
|
||||
await expect(材料.getByText("已漂移", { exact: true })).toBeVisible();
|
||||
// 失效不改写历史装配:清单与省略原因仍按冻结时原样显示。
|
||||
await expect(材料.getByText("历史:ch-1")).toBeVisible();
|
||||
await expect(
|
||||
材料.getByText("省略原因:超出材料预算,整项省略"),
|
||||
).toBeVisible();
|
||||
await expect(材料.getByText("版本 1", { exact: true })).toBeVisible();
|
||||
|
||||
await page.screenshot({
|
||||
path: info.outputPath("任务材料与失效说明.png"),
|
||||
fullPage: true,
|
||||
});
|
||||
expect(consoleErrors, `浏览器控制台错误:${consoleErrors.join(" | ")}`).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
28
数据库/迁移/V0020__上下文快照与消费.sql
Normal file
28
数据库/迁移/V0020__上下文快照与消费.sql
Normal file
@ -0,0 +1,28 @@
|
||||
-- W14 回合2(B09):任务范围与实际消费的不可变上下文快照。
|
||||
-- 快照身份由载荷确定性派生,同一输入重复冻结幂等为一行;历史快照只读不改写。
|
||||
DO $migration$
|
||||
DECLARE
|
||||
namespace text;
|
||||
BEGIN
|
||||
FOREACH namespace IN ARRAY ARRAY['public', 'evaluation'] LOOP
|
||||
EXECUTE format($ddl$
|
||||
CREATE TABLE %1$I.muse_context_snapshot (
|
||||
snapshot_id uuid PRIMARY KEY,
|
||||
run_purpose public.muse_purpose NOT NULL,
|
||||
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
|
||||
work_id text NOT NULL,
|
||||
input_hash text NOT NULL,
|
||||
frozen_scope jsonb NOT NULL,
|
||||
assembly jsonb NOT NULL,
|
||||
source_bindings jsonb NOT NULL,
|
||||
created_by text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
||||
UNIQUE (run_purpose, task_id, input_hash),
|
||||
CHECK (assembly ? ALL (ARRAY['入选','省略','字节数','文本哈希','文本'])),
|
||||
CHECK (jsonb_typeof(source_bindings) = 'array')
|
||||
);
|
||||
CREATE INDEX ON %1$I.muse_context_snapshot (task_id, created_at);
|
||||
$ddl$, namespace);
|
||||
END LOOP;
|
||||
END
|
||||
$migration$;
|
||||
Loading…
x
Reference in New Issue
Block a user