W14 受限上下文、工具与探索回放(收口):B01/B02 aiContext 真投影与用途绑定、V0020 冻结快照与实际消费、确定性回放与依赖复检、工作台材料清单与来源证据、收尾补测;整包兜底验证通过后置 verified(离线263/PG204/前端24;NC-ctx 54+前端4;W15 依赖解除)

This commit is contained in:
zizi 2026-09-10 23:54:12 +08:00
parent de4820b595
commit baf7d431a4
44 changed files with 50761 additions and 44338 deletions

View File

@ -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

View File

@ -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)。

View File

@ -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`
- 归属与种类:数据演进;迁移。
- 意图:为 上下文快照与消费记录 提供版本化数据库演进。
- 职责:上下文快照与消费结构迁移。
- 边界:只执行本版本声明的结构变化;不占用其他工作包预留序号,不以启动自动修表或复用旧序号覆盖历史。
- 目标:迁移顺序与校验和可复验,失败不留下假完成版本。
- 合同依据:结构迁移。

View File

@ -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)。

View File

@ -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": {

View File

@ -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 和测试环境

View File

@ -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)。

View File

@ -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)。

View File

@ -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 记录的覆盖缺口按功能价值补齐,不承接已退役旧用例身份。"
}
]
}

View File

@ -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:

View 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__ = ["依赖重验检查", "核对依赖"]

View 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__ = [
"快照身份",
"快照从行",
"快照载荷",
"快照身份前缀",
"文本哈希",
"绑定载荷",
"装配记录",
"输入哈希",
]

View 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__ = ["上下文存储"]

View 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__ = [
"已知工具",
"回放",
"工具范围从冻结",
"扩权字段",
"校验清单",
"记录探索",
"逐项比对",
"装配读取器",
]

View File

@ -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__ = [
"上下文错误",
"上下文服务",
"上下文快照",
"上下文错误",
"依赖重验检查",
"依赖锁定",
"依赖状态",
"事实参数合同",
"事实工具",
"冻结回执",
"已知工具",
"回放",
"回放条目",
"回放结果",
"探索条目",
"来源绑定",
"材料装配结果",
"装配材料",
"工具范围从冻结",
"核对依赖",
"历史正文参数合同",
"历史正文工具",
"范围漂移说明",
"投影合同",
"投影版本",
"快照身份",
"快照载荷",
"文本哈希",
"输入哈希",
"读取历史正文",
"读取时点事实",
"读取目标细纲",
"校验截止",
"校验清单",
"细纲参数合同",
"细纲工具",
"记录探索",
"逐项比对",
]

View File

@ -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__ = [
"历史正文参数合同",
"历史正文工具",
"范围漂移说明",
"细纲参数合同",
"细纲工具",
"事实参数合同",
"事实工具",
"投影合同",
"投影版本",
"读取历史正文",
"读取目标细纲",
"读取时点事实",
]

View File

@ -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__ = [
"上下文错误",
"上下文快照",
"依赖锁定",
"依赖状态",
"允许执行用途",
"允许内容用途",
"回放条目",
"回放结果",
"探索条目",
"材料装配结果",
"冻结回执",
"来源绑定",
]

View File

@ -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"]))

View File

@ -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__ = [
"规划保存",
"规划定位",
"规划服务",
"读取细纲投影",
"目录节点",
"节点目录保存",
]

View File

@ -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(投影.剔除原因),
}

View File

@ -196,7 +196,7 @@ def 构建(配置: 应用配置, *, 时钟: 时钟 | None = None, 流程: 流程
作品进度=创作进度服务(作品, 正文, 任务),
故事世界=故事服务(数据库, 正式),
规划=规划服务(数据库, 正式, 解析本书规划引用),
上下文=上下文服务(数据库, 任务),
上下文=上下文服务(数据库, 任务, 解析本书规划引用),
)

View File

@ -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(正文路由)

View 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 快照集
]

View File

@ -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,

View File

@ -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)

View File

@ -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")}

View 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 服务().恢复冻结范围(快照(范围)) == 范围

View 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"

View 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", 哈希) != 基准身份

View 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]

View 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, 清单)

View 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 记录

View 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

View 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"

View File

@ -0,0 +1,2 @@
export { Ui任务上下文, Ui上下文清单 } from "./上下文清单";
export { Ui来源证据面板 } from "./来源证据面板";

View 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>
);
}

View 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: "内容为空,没有可装配材料",
};

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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;

View 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();
});

View 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(
[],
);
});

View 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$;