diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts index b3e54ea54..074a40121 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts @@ -190,9 +190,7 @@ export function normalizeProjectResourceGraph( } producerTaskIdByResourceId.set(assignment.resourceId, assignment.taskId); } - for (const depth of producerMappingTruncated - ? [] - : readModel.dependencyDepths) { + for (const depth of readModel.dependencyDepths) { if ( resourceIds.has(depth.resourceId) && Number.isSafeInteger(depth.dependencyDepth) && diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 9d7fd07d2..56a2b1c50 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -1543,6 +1543,228 @@ export function registerProjectWorkbenchFoundationTests() { expect(layoutReads).toBe(1); }); + it('keeps trusted truncated-graph depths through the workbench without persisting a flat automatic layout', async () => { + const projectId = 'workbench-truncated-resource-graph'; + const projectPath = '/tmp/workbench-truncated-resource-graph'; + const manifest = createGameCreationAppManifest( + projectId, + '截断依赖图布局保护测试', + ); + manifest.assets.push( + { + id: 'truncated-depth-0', + kind: 'design-spec', + mediaType: 'application/json', + localPath: 'assets/truncated-depth-0.json', + source: { + kind: 'canvas', + resourceId: 'external-truncated-depth-0', + }, + }, + { + id: 'truncated-depth-1', + kind: 'metadata', + mediaType: 'application/json', + localPath: 'assets/truncated-depth-1.json', + source: { + kind: 'canvas', + resourceId: 'external-truncated-depth-1', + referenceResourceIds: ['external-truncated-depth-0'], + }, + }, + { + id: 'truncated-depth-2', + kind: 'metadata', + mediaType: 'application/json', + localPath: 'assets/truncated-depth-2.json', + source: { + kind: 'canvas', + resourceId: 'external-truncated-depth-2', + referenceResourceIds: ['external-truncated-depth-1'], + }, + }, + ); + const slotWidth = + RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP; + const existingPositions: ProjectResourceCanvasPosition[] = [ + { + resourceId: 'asset:truncated-depth-0', + section: 'document', + x: 0, + y: 0, + manuallyPlaced: false, + }, + { + resourceId: 'asset:truncated-depth-1', + section: 'document', + x: slotWidth, + y: 0, + manuallyPlaced: false, + }, + { + resourceId: 'asset:truncated-depth-2', + section: 'document', + x: slotWidth * 2, + y: 0, + manuallyPlaced: false, + }, + ]; + const layoutUpdates: ProjectResourceCanvasPosition[][] = []; + let layoutReads = 0; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_graph') { + return { + resourceIds: existingPositions.map( + ({ resourceId }) => resourceId, + ), + referenceEdges: [ + { + id: 'reference:truncated-0-1', + kind: 'asset-reference', + sourceResourceId: 'asset:truncated-depth-0', + targetResourceId: 'asset:truncated-depth-1', + cyclic: false, + }, + { + id: 'reference:truncated-1-2', + kind: 'asset-reference', + sourceResourceId: 'asset:truncated-depth-1', + targetResourceId: 'asset:truncated-depth-2', + cyclic: false, + }, + ], + taskFlows: [ + { + id: 'flow:untrusted-producer', + kind: 'task-flow', + sourceTaskId: 'art-director', + targetTaskId: 'design-foundation', + sourceResourceIds: ['asset:truncated-depth-0'], + targetResourceIds: ['asset:truncated-depth-1'], + cyclic: true, + }, + ], + connectionIndex: [ + { + resourceId: 'asset:truncated-depth-0', + upstreamReferenceResourceIds: [], + downstreamReferenceResourceIds: ['asset:truncated-depth-1'], + referenceEdgeIds: ['reference:truncated-0-1'], + taskFlowIds: ['flow:untrusted-producer'], + }, + { + resourceId: 'asset:truncated-depth-1', + upstreamReferenceResourceIds: ['asset:truncated-depth-0'], + downstreamReferenceResourceIds: ['asset:truncated-depth-2'], + referenceEdgeIds: [ + 'reference:truncated-0-1', + 'reference:truncated-1-2', + ], + taskFlowIds: ['flow:untrusted-producer'], + }, + { + resourceId: 'asset:truncated-depth-2', + upstreamReferenceResourceIds: ['asset:truncated-depth-1'], + downstreamReferenceResourceIds: [], + referenceEdgeIds: ['reference:truncated-1-2'], + taskFlowIds: [], + }, + ], + producerAssignments: [ + { + resourceId: 'asset:truncated-depth-0', + taskId: 'art-director', + }, + ], + dependencyDepths: [ + { + resourceId: 'asset:truncated-depth-0', + dependencyDepth: 0, + }, + { + resourceId: 'asset:truncated-depth-1', + dependencyDepth: 1, + }, + { + resourceId: 'asset:truncated-depth-2', + dependencyDepth: 2, + }, + ], + unresolvedReferenceResourceIds: [], + cyclicResourceIds: [], + cyclicTaskIds: ['art-director'], + producerMappingTruncated: true, + }; + } + if (command === 'read_local_project_resource_canvas_layout') { + layoutReads += 1; + return { + schemaVersion: 'game-creator-resource-layout.v1', + projectId, + mode: args?.mode, + revision: 7, + positions: structuredClone(existingPositions), + updatedAt: 7, + }; + } + if (command === 'update_local_project_resource_canvas_layout') { + layoutUpdates.push( + structuredClone( + args?.positions as ProjectResourceCanvasPosition[], + ), + ); + return { + status: 'updated', + layout: { + schemaVersion: 'game-creator-resource-layout.v1', + projectId, + mode: args?.mode, + revision: 8, + positions: structuredClone( + args?.positions as ProjectResourceCanvasPosition[], + ), + updatedAt: 8, + }, + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + + render( + React.createElement(ProjectDevelopmentView, { + projectName: '截断依赖图布局保护测试', + projectPath, + manifest, + attachments: [], + agentResults: [], + supervisor: React.createElement('div', null, '项目总控'), + }), + ); + + await waitFor(() => expect(layoutReads).toBe(1)); + const cards = [0, 1, 2].map((depth) => + screen.getByRole('button', { + name: new RegExp(`truncated-depth-${depth}\\.json`, 'u'), + }), + ); + await waitFor(() => { + expect(cards[0]?.getAttribute('style')).toContain('--resource-x: 0px'); + expect(cards[1]?.getAttribute('style')).toContain( + `--resource-x: ${slotWidth}px`, + ); + expect(cards[2]?.getAttribute('style')).toContain( + `--resource-x: ${slotWidth * 2}px`, + ); + }); + await act(async () => { + await Promise.resolve(); + }); + expect(layoutUpdates).toEqual([]); + }); + it('restores historical resource positions but never moves or persists them from pointer input', async () => { const projectId = 'workbench-layout-persistence'; const projectPath = '/tmp/workbench-layout-persistence'; diff --git a/apps/ai-game-creator-shell/tests/resourceDependencyGraphModel.test.ts b/apps/ai-game-creator-shell/tests/resourceDependencyGraphModel.test.ts index 62a91b90f..9fdc7a1f7 100644 --- a/apps/ai-game-creator-shell/tests/resourceDependencyGraphModel.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceDependencyGraphModel.test.ts @@ -128,10 +128,17 @@ describe('resource dependency graph model', () => { ); }); - it('fails closed for producer-derived data when the audit tail is truncated', () => { + it('fails closed only for producer-derived data when the audit tail is truncated', () => { const graph = normalizeProjectResourceGraph( readModel({ - resourceIds: ['asset:spec', 'asset:ui', 'asset:derived'], + resourceIds: [ + 'asset:spec', + 'asset:ui', + 'asset:derived', + 'asset:negative', + 'asset:fractional', + 'asset:unsafe', + ], producerAssignments: [ { resourceId: 'asset:spec', @@ -167,6 +174,18 @@ describe('resource dependency graph model', () => { resourceId: 'asset:derived', dependencyDepth: 2, }, + { + resourceId: 'asset:negative', + dependencyDepth: -1, + }, + { + resourceId: 'asset:fractional', + dependencyDepth: 1.5, + }, + { + resourceId: 'asset:unsafe', + dependencyDepth: Number.MAX_SAFE_INTEGER + 1, + }, ], taskFlows: [ { @@ -188,19 +207,52 @@ describe('resource dependency graph model', () => { cyclic: false, }, ], + connectionIndex: [ + { + resourceId: 'asset:spec', + upstreamReferenceResourceIds: [], + downstreamReferenceResourceIds: ['asset:ui'], + referenceEdgeIds: ['reference:spec-ui'], + taskFlowIds: ['flow:spec-ui'], + }, + { + resourceId: 'asset:ui', + upstreamReferenceResourceIds: ['asset:spec'], + downstreamReferenceResourceIds: [], + referenceEdgeIds: ['reference:spec-ui'], + taskFlowIds: ['flow:spec-ui'], + }, + ], + unresolvedReferenceResourceIds: ['external:missing'], + cyclicResourceIds: ['asset:ui'], cyclicTaskIds: ['art-director'], producerMappingTruncated: true, }), ); expect(graph.producerTaskIdByResourceId).toEqual(new Map()); - expect(graph.dependencyDepthByResourceId).toEqual(new Map()); + expect(graph.dependencyDepthByResourceId).toEqual( + new Map([ + ['asset:spec', 0], + ['asset:ui', 1], + ['asset:derived', 2], + ]), + ); expect(graph.taskFlows).toEqual([]); expect(graph.cyclicTaskIds).toEqual(new Set()); expect(graph.referenceEdges.map((edge) => edge.id)).toEqual([ 'reference:spec-ui', ]); + expect(graph.unresolvedReferenceResourceIds).toEqual([ + 'external:missing', + ]); + expect(graph.cyclicResourceIds).toEqual(new Set(['asset:ui'])); expect(graph.producerMappingTruncated).toBe(true); + expect(projectResourceGraphNeighbors(graph, 'asset:ui')).toEqual({ + upstreamResourceIds: new Set(['asset:spec']), + downstreamResourceIds: new Set(), + connectedEdgeIds: new Set(['reference:spec-ui']), + }); expect(projectResourceGraphNeighbors(graph, 'asset:deleted')).toEqual({ upstreamResourceIds: new Set(), downstreamResourceIds: new Set(), diff --git a/apps/ai-game-creator-shell/tests/useProjectResourceCanvasLayout.test.ts b/apps/ai-game-creator-shell/tests/useProjectResourceCanvasLayout.test.ts index 2a975210d..1b4cf3e2c 100644 --- a/apps/ai-game-creator-shell/tests/useProjectResourceCanvasLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/useProjectResourceCanvasLayout.test.ts @@ -139,6 +139,67 @@ describe('useProjectResourceCanvasLayout', () => { ); }); + it('persists distinct automatic columns for dependency depths 0, 1, and 2', async () => { + const updates: ProjectResourceCanvasPosition[][] = []; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_canvas_layout') { + return persistedLayout('dependency', 0, []); + } + if (command === 'update_local_project_resource_canvas_layout') { + const positions = structuredClone( + args?.positions as ProjectResourceCanvasPosition[], + ); + updates.push(positions); + return { + status: 'updated', + layout: persistedLayout('dependency', 1, positions), + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + + renderHook(() => + useProjectResourceCanvasLayout({ + projectPath, + projectId, + mode: 'dependency', + resources: [ + resource('resource-depth-0'), + { ...resource('resource-depth-1'), dependencyDepth: 1 }, + { ...resource('resource-depth-2'), dependencyDepth: 2 }, + ], + rederiveAutomaticPositions: true, + }), + ); + + await waitFor(() => expect(updates).toHaveLength(1)); + expect(updates[0]).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + resourceId: 'resource-depth-0', + x: 0, + y: 0, + manuallyPlaced: false, + }), + expect.objectContaining({ + resourceId: 'resource-depth-1', + x: 196, + y: 0, + manuallyPlaced: false, + }), + expect.objectContaining({ + resourceId: 'resource-depth-2', + x: 392, + y: 0, + manuallyPlaced: false, + }), + ]), + ); + }); + it('rederives automatic dependency positions while preserving manual positions', async () => { const resourceA = { ...resource('resource-a'), dependencyDepth: 2 }; const resourceB = resource('resource-b'); diff --git a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md index e536d0a38..2983be366 100644 --- a/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md +++ b/docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md @@ -261,6 +261,7 @@ type UpdateProjectResourceCanvasLayoutResult = #### 5.2.6 资源依赖关系图层 - 阶段五实现状态(2026-08-03):dependency 自动排列同时消费任务 DAG 与精确资源引用。Rust 把可信 producer 的任务深度作为资源深度下限,再对 `asset-reference` 图做迭代式 SCC 压缩与确定性层级传播;被引用资源位于引用资源之前,同一引用环共享稳定深度,环后资源继续递增,没有引用关系的资源保持默认不重叠位置。布局深度通过独立 `dependencyDepths` 返回,不能把 producer assignment 冒充全部资源的布局结果。 +- `producerMappingTruncated=true` 只关闭依赖有界 Agent DB 审计的 `producerAssignments`、`taskFlows` 和 `cyclicTaskIds`。Rust 返回的 `dependencyDepths` 仍是 manifest / 精确引用 read model 的权威结果,前端必须过滤未知资源、负数、非整数和非安全整数后继续消费;不得因 producer 截断清空全部深度,也不得在前端重算替代深度。`referenceEdges`、connection index 中的 reference 关系、`cyclicResourceIds` 和 unresolved reference 继续保持可信。 - 图层只在 dependency 模式挂载;type 模式不得渲染 SVG、连线或 marker。切换 mode、切换项目或卸载工作台时必须销毁旧图层,并清理尺寸观察和窗口事件监听。 - 输入固定为当前资源投影的全部卡片身份 / 坐标与 Tauri Rust 返回的 `ProjectResourceGraph` 只读 DTO;Rust 负责资源过滤、去重、迭代式环检测、SCC 压缩后的确定性依赖深度、任务流聚合和一跳连接索引,前端只负责 DTO 防御归一化、浏览器几何与原生 SVG path / marker。SVG 叠加在资源卡底层并设置 `pointer-events: none`,不得引入 D3、React Flow 等图表库,也不得阻断卡片点击。 - `asset-reference` 表示精确资源引用,使用明亮橙色实线与连续贝塞尔曲线。`GameCreationAppAssetManifestEntry.source.referenceResourceIds` 中的外部资源 ID 必须先唯一匹配另一项资产的 `source.resourceId`,再映射为当前资源卡 ID;缺失、重复或已删除的目标均不得渲染幽灵连线。 @@ -435,7 +436,7 @@ type ProjectAgentMudPointAttribution = { 5. 搜索触发端点过滤;资源点击不改变上下游卡片或任何连线的视觉状态,资源卡指针移动不更新线段,点击与中央聚焦行为不回归。 6. 切换布局模式或项目后旧 SVG、ResizeObserver 与窗口监听全部清理;图层从不写入 layout sidecar、manifest 或其它持久化。 7. 4096 资源链式 fixture 继续验证拓扑、聚合复杂度和自动布局性能;拖动局部更新与真实 Chromium 拖动帧预算暂缓,不作为当前验收条件。最右侧自环与箭头仍需完整显示。 -8. Rust 图读取延迟时,dependency sidecar 在图进入 `ready / failed` 前没有读取或写入;首次布局直接使用 Rust 返回的最终 producer 与 dependency depth。Agent DB 有界读取截断时 producer、task flow 与其派生深度失败关闭,精确 manifest 引用仍可显示。重新打开旧布局时手动位置逐项不变,自动位置按最终拓扑协调且相同结果不增加 revision。 +8. Rust 图读取延迟时,dependency sidecar 在图进入 `ready / failed` 前没有读取或写入;首次布局直接使用 Rust 返回的最终 producer 与 dependency depth。Agent DB 有界读取截断时 producer、task flow 与 `cyclicTaskIds` 失败关闭,精确 manifest 引用及 Rust 返回的合法 `dependencyDepths` 继续到达布局层。重新打开包含深度 `0 / 1 / 2` 自动坐标的旧布局时不得降成全 `0` 或持久化扁平布局;手动位置逐项不变,自动位置按最终拓扑协调且相同结果不增加 revision。 9. 依赖 SVG 作为装饰层不可聚焦并对辅助技术隐藏;画布通过关联的视觉隐藏文本逐条说明当前可见资源引用和任务流,搜索过滤或模式切换后文本与可见关系同步变化。 ### 7.4 P1 正式项目版本阶段六验收 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index dc353e354..d01b3b7d8 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6029,7 +6029,7 @@ - 存储决策:`.agent/manifest.json` 的版本追加不可变约束由同目录持久专用锁保护,读取旧状态、校验版本前缀、安装临时文件和安装后回读必须处于同一临界区;进程内 Mutex 不能替代跨进程文件锁。 - UI 决策:Project Supervisor 持有运行中 manifest 状态并向外层启动器同步完整快照;外层项目上下文继续是工作台投影的唯一输入,只接受当前项目路径的更新,不另建资产、任务或版本平行状态。 -- 依赖图决策:Agent DB 尾部读取一旦截断,审计 producer 及其 task flow / depth 派生失败关闭;manifest 精确资源引用与审计生产者证据分离。SVG 保持装饰性,辅助技术消费画布关联的文本关系列表。 +- 依赖图决策:Agent DB 尾部读取一旦截断,审计 producer、task flow 与对应 `cyclicTaskIds` 失败关闭;独立 `dependencyDepths` 仍由 Rust 从当前 manifest、精确资源引用和仍可信的任务深度下限构建,前端只做资源存在性与非负安全整数校验后继续消费。manifest 精确资源引用、reference connection index、资源环和 unresolved reference 与审计生产者证据分离。SVG 保持装饰性,辅助技术消费画布关联的文本关系列表。 ## 2026-08-04 图片画布素材类型采用资源默认值与布局覆盖双层模型 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 79c616aec..701031b5c 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -26,8 +26,8 @@ - 现象:画布资产之间已有橙色精确引用线,但依赖任务之间没有灰色 task flow;测试用 `design-foundation` 之类字符串时正常,真实生成返回 `task-1` 后失败。 - 原因:`GameCreationAppAssetSource.taskId` 保存的是 External Editor 生成任务身份,命名空间与本地 `.agent/manifest.json` 的 Agent/task 身份不同;前端用 `taskById.get(source.taskId)` 会让真实画布资产全部失去 producer。 -- 处理:资源依赖图的 Tauri Rust read model 从有界 `.agent/agent.db` 读取 `agent.runtime.canvas.asset_generate`,以 `assetId -> agentId` 映射 producer,并要求 `agentId` 存在于当前 manifest。记录缺失、多个不同有效 Agent 冲突或读取已截断时失败关闭该资产的 task flow,不回退 `source.taskId`。精确 `asset-reference` 仍只依赖 manifest 中外部 resourceId 的唯一匹配。 -- 验证:Rust fixture 把 `source.taskId` 固定为 `task-1 / task-2`,只有审计提供 `art-director / design-foundation` 后才生成 task flow;移除审计后橙色引用保留、灰色任务流消失。AppSurface 使用相同生产数据形状回归。 +- 处理:资源依赖图的 Tauri Rust read model 从有界 `.agent/agent.db` 读取 `agent.runtime.canvas.asset_generate`,以 `assetId -> agentId` 映射 producer,并要求 `agentId` 存在于当前 manifest。记录缺失、多个不同有效 Agent 冲突或读取已截断时失败关闭 producer assignment、task flow 与对应 `cyclicTaskIds`,不回退 `source.taskId`。精确 `asset-reference` 仍只依赖 manifest 中外部 resourceId 的唯一匹配;Rust 独立返回的 `dependencyDepths` 继续作为 manifest / reference read model 权威结果,前端只过滤未知资源、负数、非整数和非安全整数,不得因 producer 截断把它整体清空。 +- 验证:Rust fixture 把 `source.taskId` 固定为 `task-1 / task-2`,只有审计提供 `art-director / design-foundation` 后才生成 task flow;移除或截断审计后橙色引用保留、灰色任务流消失,合法深度仍为 `asset:spec=0 / asset:ui=1`。AppSurface 使用截断生产数据形状证明深度 `0 / 1 / 2` 真实到达卡片布局,并且不会把已有自动坐标持久化成扁平布局。 - 关联:`apps/ai-game-creator-shell/src-tauri/src/project/resource_dependency_graph.rs`、`apps/ai-game-creator-shell/src/view/project-development/resourceDependencyGraphModel.ts`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 ## 依赖图未就绪时不能先初始化资源布局 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 9c350ad0b..28023064d 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -414,7 +414,7 @@ game-project/ - 基础 positions 保持稳定;卡片 Pointer Move 不进入 SVG preview,只有搜索、选择、项目切换、真实 positions 或 section origin 变化才重新协调图层。SVG 几何从不持久化。 - Rust、前端 DTO/SVG 和工作台 AppSurface 回归覆盖生产 `task-1` 数据形状、真实 producer、证据缺失、精确引用、去重、无效 ID、完整任务环、4096 链式拓扑、聚合复杂度、搜索过滤、选择高亮、稳定 Observer、type 模式卸载与项目切换销毁。局部拖动更新与真实 Chromium 拖动性能目标暂缓。 -2026-08-03 阶段五加固:Rust read model 把 producer assignment 与布局深度分离。完整任务图先经 SCC 压缩形成可信 producer 的任务深度下限,精确资源引用图再经迭代式 SCC 压缩和确定性最长层级传播形成所有可见资源的 `dependencyDepths`;因此同一任务的派生资源、缺少 producer 审计的 manifest 资源和引用环都能稳定满足“被引用资源在前、引用资源在后”,没有引用关系的资源保持深度 `0`。前端不递归推导正式依赖层级。dependency 模式以 scope 化 `idle / loading / ready / failed` 状态阻断布局 Hook;图终态前不创建 fallback、不读取或写入 sidecar,图失败只以空图初始化一次。读取已有 dependency 布局时保留全部 `manuallyPlaced=true` 坐标,把 `manuallyPlaced=false` 作为可派生自动位置按最终深度重新协调;结果未变化时不写入。任务流仍按任务对聚合,不为布局计算或 SVG 绘制生成资源笛卡尔积。 +2026-08-03 阶段五加固,2026-08-05 明确截断信任边界:Rust read model 把 producer assignment 与布局深度分离。完整任务图先经 SCC 压缩形成可信 producer 的任务深度下限,精确资源引用图再经迭代式 SCC 压缩和确定性最长层级传播形成所有可见资源的 `dependencyDepths`;因此同一任务的派生资源、缺少 producer 审计的 manifest 资源和引用环都能稳定满足“被引用资源在前、引用资源在后”,没有引用关系的资源保持深度 `0`。`producerMappingTruncated=true` 只表示有界 Agent DB 尾部不足以证明 producer:前端必须失败关闭 `producerAssignments`、`taskFlows` 及其 `cyclicTaskIds`,但继续严格校验并消费 Rust 从当前 manifest、精确引用和仍可信下限构建的 `dependencyDepths`;`referenceEdges`、connection index 中的 reference 关系、`cyclicResourceIds` 和 unresolved reference 同样继续有效。前端不递归推导正式依赖层级。dependency 模式以 scope 化 `idle / loading / ready / failed` 状态阻断布局 Hook;图终态前不创建 fallback、不读取或写入 sidecar,图失败只以空图初始化一次。读取已有 dependency 布局时保留全部 `manuallyPlaced=true` 坐标,把 `manuallyPlaced=false` 作为可派生自动位置按最终深度重新协调;结果未变化时不写入。任务流仍按任务对聚合,不为布局计算或 SVG 绘制生成资源笛卡尔积。 2026-08-03 阶段六:正式迭代版本直接扩展本地 `.agent/manifest.json`,不新增 checkpoint / layout sidecar / SpacetimeDB 平行业务真相。共享 Rust / TypeScript 合同新增可选 `versions: GameIterationVersion[]`;旧项目缺失字段时只读为空,不回填。Rust 在 manifest 读写边界校验版本唯一性、父先于子、根/原因一致、父子修订与时间单调、slot 唯一和 JavaScript 安全整数,并在覆盖已有 manifest 前要求磁盘版本数组是新数组的逐项相等前缀,从存储边界保证历史记录不可修改、删除或重排。