修复资源依赖截断深度信任边界
保留 Rust manifest 与引用 read model 的合法依赖深度,仅关闭不完整 producer 审计数据。 补充模型、布局 Hook 与工作台截断回归,阻止自动布局扁平写回。 同步 PRD、技术方案和项目记忆中的跨层合同。
This commit is contained in:
+1
-3
@@ -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) &&
|
||||
|
||||
@@ -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<string, unknown>) => {
|
||||
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';
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<string, unknown>) => {
|
||||
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');
|
||||
|
||||
@@ -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 正式项目版本阶段六验收
|
||||
|
||||
@@ -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 图片画布素材类型采用资源默认值与布局覆盖双层模型
|
||||
|
||||
|
||||
@@ -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`。
|
||||
|
||||
## 依赖图未就绪时不能先初始化资源布局
|
||||
|
||||
@@ -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 前要求磁盘版本数组是新数组的逐项相等前缀,从存储边界保证历史记录不可修改、删除或重排。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user