完成资源依赖排列阶段五
计算资源引用 SCC 与任务深度下限 拆分 producer assignment 和布局深度合同 补齐依赖布局回归测试 同步工作台 PRD、技术方案与项目记忆
This commit is contained in:
@@ -50,6 +50,12 @@ pub(crate) struct ProjectResourceConnectionIndex {
|
||||
pub(crate) struct ProjectResourceProducerAssignment {
|
||||
pub resource_id: String,
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ProjectResourceDependencyDepth {
|
||||
pub resource_id: String,
|
||||
pub dependency_depth: u32,
|
||||
}
|
||||
|
||||
@@ -61,6 +67,7 @@ pub(crate) struct ProjectResourceGraphReadModel {
|
||||
pub task_flows: Vec<ProjectResourceTaskFlow>,
|
||||
pub connection_index: Vec<ProjectResourceConnectionIndex>,
|
||||
pub producer_assignments: Vec<ProjectResourceProducerAssignment>,
|
||||
pub dependency_depths: Vec<ProjectResourceDependencyDepth>,
|
||||
pub unresolved_reference_resource_ids: Vec<String>,
|
||||
pub cyclic_resource_ids: Vec<String>,
|
||||
pub cyclic_task_ids: Vec<String>,
|
||||
@@ -194,6 +201,7 @@ fn analyze_directed_cycles<'a>(
|
||||
fn dependency_depth_by_node(
|
||||
analysis: &CycleAnalysis,
|
||||
edges: &[DirectedEdge],
|
||||
minimum_depth_by_node: &BTreeMap<String, u32>,
|
||||
) -> BTreeMap<String, u32> {
|
||||
let component_count = analysis
|
||||
.component_by_node
|
||||
@@ -223,6 +231,12 @@ fn dependency_depth_by_node(
|
||||
.filter_map(|(component, degree)| (*degree == 0).then_some(component))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut depth_by_component = vec![0u32; component_count];
|
||||
for (node_id, minimum_depth) in minimum_depth_by_node {
|
||||
let Some(component) = analysis.component_by_node.get(node_id) else {
|
||||
continue;
|
||||
};
|
||||
depth_by_component[*component] = depth_by_component[*component].max(*minimum_depth);
|
||||
}
|
||||
while let Some(component) = ready.pop_first() {
|
||||
for &target in &outgoing[component] {
|
||||
depth_by_component[target] =
|
||||
@@ -464,7 +478,22 @@ pub(crate) fn build_project_resource_graph(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let task_cycles = analyze_directed_cycles(task_by_id.keys(), &task_dependency_edges);
|
||||
let task_dependency_depths = dependency_depth_by_node(&task_cycles, &task_dependency_edges);
|
||||
let task_dependency_depths =
|
||||
dependency_depth_by_node(&task_cycles, &task_dependency_edges, &BTreeMap::new());
|
||||
let minimum_resource_dependency_depths = producer_by_resource_id
|
||||
.iter()
|
||||
.filter_map(|(resource_id, task_id)| {
|
||||
task_dependency_depths
|
||||
.get(task_id)
|
||||
.copied()
|
||||
.map(|depth| (resource_id.clone(), depth))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let resource_dependency_depths = dependency_depth_by_node(
|
||||
&reference_cycles,
|
||||
&reference_directed_edges,
|
||||
&minimum_resource_dependency_depths,
|
||||
);
|
||||
let task_flows = task_dependency_edges
|
||||
.iter()
|
||||
.filter_map(|edge| {
|
||||
@@ -538,13 +567,18 @@ pub(crate) fn build_project_resource_graph(
|
||||
.into_iter()
|
||||
.map(|(resource_id, task_id)| ProjectResourceProducerAssignment {
|
||||
resource_id,
|
||||
dependency_depth: task_dependency_depths
|
||||
.get(&task_id)
|
||||
.copied()
|
||||
.unwrap_or_default(),
|
||||
task_id,
|
||||
})
|
||||
.collect(),
|
||||
dependency_depths: resource_dependency_depths
|
||||
.into_iter()
|
||||
.map(
|
||||
|(resource_id, dependency_depth)| ProjectResourceDependencyDepth {
|
||||
resource_id,
|
||||
dependency_depth,
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
unresolved_reference_resource_ids: unresolved_reference_resource_ids.into_iter().collect(),
|
||||
cyclic_resource_ids: reference_cycles.cyclic_node_ids.into_iter().collect(),
|
||||
cyclic_task_ids: task_cycles.cyclic_node_ids.into_iter().collect(),
|
||||
@@ -680,7 +714,18 @@ mod tests {
|
||||
graph
|
||||
.producer_assignments
|
||||
.iter()
|
||||
.map(|assignment| (assignment.resource_id.as_str(), assignment.dependency_depth,))
|
||||
.map(|assignment| (assignment.resource_id.as_str(), assignment.task_id.as_str()))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
BTreeMap::from([
|
||||
("asset:spec", "art-director"),
|
||||
("asset:ui", "design-foundation"),
|
||||
]),
|
||||
);
|
||||
assert_eq!(
|
||||
graph
|
||||
.dependency_depths
|
||||
.iter()
|
||||
.map(|depth| (depth.resource_id.as_str(), depth.dependency_depth))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
BTreeMap::from([("asset:spec", 0), ("asset:ui", 1)]),
|
||||
);
|
||||
@@ -720,6 +765,14 @@ mod tests {
|
||||
assert_eq!(graph.reference_edges.len(), 1);
|
||||
assert!(graph.task_flows.is_empty());
|
||||
assert!(graph.producer_assignments.is_empty());
|
||||
assert_eq!(
|
||||
graph
|
||||
.dependency_depths
|
||||
.iter()
|
||||
.map(|depth| (depth.resource_id.as_str(), depth.dependency_depth))
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
BTreeMap::from([("asset:spec", 0), ("asset:ui", 1)]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -810,10 +863,10 @@ mod tests {
|
||||
.all(|index| index.task_flow_ids.len() <= 2));
|
||||
assert_eq!(
|
||||
graph
|
||||
.producer_assignments
|
||||
.dependency_depths
|
||||
.iter()
|
||||
.find(|assignment| assignment.resource_id == "resource:4095")
|
||||
.map(|assignment| assignment.dependency_depth),
|
||||
.find(|depth| depth.resource_id == "resource:4095")
|
||||
.map(|depth| depth.dependency_depth),
|
||||
Some(4095),
|
||||
);
|
||||
}
|
||||
@@ -841,13 +894,72 @@ mod tests {
|
||||
);
|
||||
|
||||
let depths = graph
|
||||
.producer_assignments
|
||||
.dependency_depths
|
||||
.iter()
|
||||
.map(|assignment| (assignment.resource_id.as_str(), assignment.dependency_depth))
|
||||
.map(|depth| (depth.resource_id.as_str(), depth.dependency_depth))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(depths["source-resource"], 0);
|
||||
assert_eq!(depths["cycle-a-resource"], 1);
|
||||
assert_eq!(depths["cycle-b-resource"], 1);
|
||||
assert_eq!(depths["target-resource"], 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependency_depth_uses_reference_sccs_after_task_depth_floors() {
|
||||
let graph = build_project_resource_graph(
|
||||
&manifest(
|
||||
vec![
|
||||
task("source-task", &[]),
|
||||
task("late-task", &["source-task"]),
|
||||
],
|
||||
vec![
|
||||
asset("base", Some("external-base"), &[], None),
|
||||
asset(
|
||||
"cycle-a",
|
||||
Some("external-cycle-a"),
|
||||
&["external-base", "external-cycle-b"],
|
||||
None,
|
||||
),
|
||||
asset(
|
||||
"cycle-b",
|
||||
Some("external-cycle-b"),
|
||||
&["external-cycle-a"],
|
||||
None,
|
||||
),
|
||||
asset(
|
||||
"target",
|
||||
Some("external-target"),
|
||||
&["external-cycle-b"],
|
||||
None,
|
||||
),
|
||||
],
|
||||
),
|
||||
vec![
|
||||
resource("asset:base", Some("base"), None),
|
||||
resource("asset:cycle-a", Some("cycle-a"), None),
|
||||
resource("asset:cycle-b", Some("cycle-b"), None),
|
||||
resource("asset:target", Some("target"), None),
|
||||
],
|
||||
&[
|
||||
serde_json::json!({"recordType": "agent.runtime.canvas.asset_generate", "assetId": "base", "agentId": "source-task"}),
|
||||
serde_json::json!({"recordType": "agent.runtime.canvas.asset_generate", "assetId": "cycle-a", "agentId": "late-task"}),
|
||||
],
|
||||
false,
|
||||
);
|
||||
|
||||
let depths = graph
|
||||
.dependency_depths
|
||||
.iter()
|
||||
.map(|depth| (depth.resource_id.as_str(), depth.dependency_depth))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(depths["asset:base"], 0);
|
||||
assert_eq!(depths["asset:cycle-a"], 1);
|
||||
assert_eq!(depths["asset:cycle-b"], 1);
|
||||
assert_eq!(depths["asset:target"], 2);
|
||||
assert_eq!(
|
||||
graph.cyclic_resource_ids,
|
||||
vec!["asset:cycle-a", "asset:cycle-b"]
|
||||
);
|
||||
assert_eq!(graph.task_flows.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-4
@@ -33,6 +33,10 @@ export type ProjectResourceConnectionIndexDto = {
|
||||
export type ProjectResourceProducerAssignment = {
|
||||
resourceId: string;
|
||||
taskId: string;
|
||||
};
|
||||
|
||||
export type ProjectResourceDependencyDepth = {
|
||||
resourceId: string;
|
||||
dependencyDepth: number;
|
||||
};
|
||||
|
||||
@@ -42,6 +46,7 @@ export type ProjectResourceGraphReadModel = {
|
||||
taskFlows: ProjectResourceTaskFlow[];
|
||||
connectionIndex: ProjectResourceConnectionIndexDto[];
|
||||
producerAssignments: ProjectResourceProducerAssignment[];
|
||||
dependencyDepths: ProjectResourceDependencyDepth[];
|
||||
unresolvedReferenceResourceIds: string[];
|
||||
cyclicResourceIds: string[];
|
||||
cyclicTaskIds: string[];
|
||||
@@ -185,13 +190,19 @@ export function normalizeProjectResourceGraph(
|
||||
continue;
|
||||
}
|
||||
producerTaskIdByResourceId.set(assignment.resourceId, assignment.taskId);
|
||||
}
|
||||
for (const depth of readModel.dependencyDepths) {
|
||||
if (
|
||||
Number.isSafeInteger(assignment.dependencyDepth) &&
|
||||
assignment.dependencyDepth >= 0
|
||||
resourceIds.has(depth.resourceId) &&
|
||||
Number.isSafeInteger(depth.dependencyDepth) &&
|
||||
depth.dependencyDepth >= 0
|
||||
) {
|
||||
dependencyDepthByResourceId.set(
|
||||
assignment.resourceId,
|
||||
assignment.dependencyDepth,
|
||||
depth.resourceId,
|
||||
Math.max(
|
||||
dependencyDepthByResourceId.get(depth.resourceId) ?? 0,
|
||||
depth.dependencyDepth,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ function graphFixture() {
|
||||
taskFlowIds: resourceId === 'unrelated' ? [] : [flowId],
|
||||
})),
|
||||
producerAssignments: [],
|
||||
dependencyDepths: [],
|
||||
unresolvedReferenceResourceIds: [],
|
||||
cyclicResourceIds: ['unrelated'],
|
||||
cyclicTaskIds: [],
|
||||
@@ -373,6 +374,7 @@ describe('ResourceDependencyOverlay', () => {
|
||||
],
|
||||
})),
|
||||
producerAssignments: [],
|
||||
dependencyDepths: [],
|
||||
unresolvedReferenceResourceIds: [],
|
||||
cyclicResourceIds: [],
|
||||
cyclicTaskIds: [],
|
||||
|
||||
@@ -78,11 +78,14 @@ function resourceGraphForInputs(args?: Record<string, unknown>) {
|
||||
{
|
||||
resourceId: resource.resourceId,
|
||||
taskId: resource.producerTaskId,
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
),
|
||||
dependencyDepths: resources.map((resource) => ({
|
||||
resourceId: resource.resourceId,
|
||||
dependencyDepth: 0,
|
||||
})),
|
||||
unresolvedReferenceResourceIds: [],
|
||||
cyclicResourceIds: [],
|
||||
cyclicTaskIds: [],
|
||||
@@ -1103,13 +1106,25 @@ export function registerProjectWorkbenchFoundationTests() {
|
||||
{
|
||||
resourceId: 'asset:dependency-spec',
|
||||
taskId: 'art-director',
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:dependency-ui',
|
||||
taskId: 'design-foundation',
|
||||
},
|
||||
],
|
||||
dependencyDepths: [
|
||||
{
|
||||
resourceId: 'asset:dependency-spec',
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:dependency-ui',
|
||||
dependencyDepth: 1,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:unrelated-cycle',
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
],
|
||||
unresolvedReferenceResourceIds: [],
|
||||
cyclicResourceIds: ['asset:unrelated-cycle'],
|
||||
|
||||
@@ -15,6 +15,7 @@ function readModel(
|
||||
taskFlows: [],
|
||||
connectionIndex: [],
|
||||
producerAssignments: [],
|
||||
dependencyDepths: [],
|
||||
unresolvedReferenceResourceIds: [],
|
||||
cyclicResourceIds: [],
|
||||
cyclicTaskIds: [],
|
||||
@@ -130,23 +131,42 @@ describe('resource dependency graph model', () => {
|
||||
it('keeps real producer assignments and audit truncation metadata', () => {
|
||||
const graph = normalizeProjectResourceGraph(
|
||||
readModel({
|
||||
resourceIds: ['asset:spec', 'asset:ui'],
|
||||
resourceIds: ['asset:spec', 'asset:ui', 'asset:derived'],
|
||||
producerAssignments: [
|
||||
{
|
||||
resourceId: 'asset:spec',
|
||||
taskId: 'art-director',
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:ui',
|
||||
taskId: 'design-foundation',
|
||||
dependencyDepth: 1,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:deleted',
|
||||
taskId: 'task-1',
|
||||
},
|
||||
],
|
||||
dependencyDepths: [
|
||||
{
|
||||
resourceId: 'asset:spec',
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:ui',
|
||||
dependencyDepth: 1,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:deleted',
|
||||
dependencyDepth: 99,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:ui',
|
||||
dependencyDepth: 0,
|
||||
},
|
||||
{
|
||||
resourceId: 'asset:derived',
|
||||
dependencyDepth: 2,
|
||||
},
|
||||
],
|
||||
producerMappingTruncated: true,
|
||||
}),
|
||||
@@ -162,6 +182,7 @@ describe('resource dependency graph model', () => {
|
||||
new Map([
|
||||
['asset:spec', 0],
|
||||
['asset:ui', 1],
|
||||
['asset:derived', 2],
|
||||
]),
|
||||
);
|
||||
expect(graph.producerMappingTruncated).toBe(true);
|
||||
|
||||
@@ -260,6 +260,7 @@ type UpdateProjectResourceCanvasLayoutResult =
|
||||
|
||||
#### 5.2.6 资源依赖关系图层
|
||||
|
||||
- 阶段五实现状态(2026-08-03):dependency 自动排列同时消费任务 DAG 与精确资源引用。Rust 把可信 producer 的任务深度作为资源深度下限,再对 `asset-reference` 图做迭代式 SCC 压缩与确定性层级传播;被引用资源位于引用资源之前,同一引用环共享稳定深度,环后资源继续递增,没有引用关系的资源保持默认不重叠位置。布局深度通过独立 `dependencyDepths` 返回,不能把 producer assignment 冒充全部资源的布局结果。
|
||||
- 图层只在 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;缺失、重复或已删除的目标均不得渲染幽灵连线。
|
||||
@@ -271,6 +272,7 @@ type UpdateProjectResourceCanvasLayoutResult =
|
||||
- 选中资源后,高亮其直接上游、直接下游卡片和关联边,弱化其余边;不做跨多层递归高亮。选中 ID 已失效时按未选中处理。
|
||||
- 资源卡 Pointer Move 不改变基础 positions 或 SVG 几何。连线只随布局读取、资源自动协调、搜索、选择、项目切换或 section origin 变化而更新。
|
||||
- `ResizeObserver` 在单个图层生命周期只允许构造一次。dependency section 额外提供至少 `64px` 右侧视觉 gutter,确保最右侧自环和箭头可完整滚动显示,但不得修改卡片坐标或布局 sidecar。
|
||||
- 阶段五不改变手动位置边界:已有 `manuallyPlaced=true` 坐标原样保留,资源引用新增或变化只允许重新派生 `manuallyPlaced=false` 的自动坐标;任务流继续按任务对聚合,禁止为了计算深度或绘线生成资源笛卡尔积。
|
||||
|
||||
### 5.3 资源类型与替换兼容性(P1)
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# 决策记录
|
||||
|
||||
## 2026-08-03 资源依赖阶段五以引用 SCC 深度驱动自动排列
|
||||
|
||||
- 背景:资源关系图已经能展示精确引用与聚合任务流,但 dependency 自动布局只消费可信 producer 对应的任务 DAG 深度;同一任务生成的派生资源、没有 producer 审计的 manifest 资源和资源引用环均无法稳定体现“被引用资源在前、引用资源在后”的顺序。
|
||||
- 决策:`read_local_project_resource_graph` 把 producer assignment 与布局深度拆成两个只读字段。Rust 先压缩完整任务图并把可信 producer 的任务深度作为资源下限,再对精确资源引用图做迭代式 SCC 压缩和确定性最长层级传播;同一引用环共享深度,环后资源递增一层,没有引用关系的资源保持默认深度 `0`。前端只校验并消费 `dependencyDepths`,沿用现有自动位置协调和 SVG 几何,不自行推导关系。
|
||||
- 边界:不修改 manifest、layout sidecar、External Editor API、api-server 或 SpacetimeDB;不恢复资源卡拖动。已有 `manuallyPlaced=true` 坐标继续保留,只有可派生自动坐标会按新深度重算;任务流继续按任务对聚合,不展开资源笛卡尔积。
|
||||
- 验证方式:Rust 定向测试覆盖无 producer 的精确引用、任务深度下限、资源引用环 SCC、环后资源与 4096 任务链;前端 DTO、布局 Hook、纯布局和 SVG 测试覆盖独立深度字段、自动重排与手动坐标保留,并运行 shell typecheck、编码检查和 `git diff --check`。
|
||||
- 关联文档:`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-08-03 资源聚焦阶段四采用只读受控文档与媒体链路
|
||||
|
||||
- 背景:阶段一至三已经完成资源卡禁拖、固定四类资源投影与中央聚焦容器,但只有 PNG / JPEG / WEBP 和合法 Agent 文本回执具备真实主体预览;本地文档、SVG / 视频与音频仍只有路径和元数据。飞书需求同时把“编辑并生成新资源”写为条件项,而当前仓库尚未具备从画板返回后的血缘登记与自动选中闭环。
|
||||
|
||||
@@ -397,7 +397,7 @@ game-project/
|
||||
|
||||
2026-07-31 起,项目工作台使用“Tauri Rust 只读拓扑 + 前端原生 SVG 几何”的资源依赖图层;不修改 layout sidecar、既有布局模型、api-server 或 SpacetimeDB:
|
||||
|
||||
- `read_local_project_resource_graph` 读取当前 manifest、前端资源卡身份列表和最多 `32 MiB` 的安全 Agent DB 尾部,通过 Rust 构建稳定 read model;读取使用既有 Agent DB 普通文件 / 链接 / 追加锁边界,不新增数据库或 sidecar。返回资源 ID、引用边、聚合任务流、producer assignment、循环集合、unresolved 外部 ID、局部连接索引和 `producerMappingTruncated`。
|
||||
- `read_local_project_resource_graph` 读取当前 manifest、前端资源卡身份列表和最多 `32 MiB` 的安全 Agent DB 尾部,通过 Rust 构建稳定 read model;读取使用既有 Agent DB 普通文件 / 链接 / 追加锁边界,不新增数据库或 sidecar。返回资源 ID、引用边、聚合任务流、producer assignment、独立 `dependencyDepths`、循环集合、unresolved 外部 ID、局部连接索引和 `producerMappingTruncated`。
|
||||
- 精确引用把 manifest 资产 `source.referenceResourceIds` 唯一匹配到另一资产的 `source.resourceId`,再转换为本次资源卡 ID;无匹配、多匹配、重复卡片或已删除资源只记录为 unresolved / 忽略,不生成边。引用边按 `sourceResourceId + targetResourceId` 稳定去重;前端 `resourceDependencyGraphModel.ts` 再做一次 DTO 端点防御过滤,避免异步切项目时出现幽灵线。
|
||||
- task flow 只读取存在于当前 manifest 的任务依赖。画布资产 producer 仅接受 `agent.runtime.canvas.asset_generate` 中经 manifest 校验的 `assetId -> agentId`;External Editor 返回并保存在 `source.taskId` 的 `task-1` 等身份属于平台生成任务,禁止复用为 manifest task。多个有效 Agent 对同一资产形成冲突或证据缺失时,不生成该资产对应 task flow。任务产物 / Agent 回执继续使用资源投影中已有的 manifest task 身份。
|
||||
- 资源按可信 producer 分组,每个 `sourceTaskId -> targetTaskId` 只生成一个聚合 flow;SVG 侧绘制 source 分支、唯一主线和 target 分支,路径数量为 `O(S+T)`,禁止资源笛卡尔积。局部连接索引保存 resource 关联的 reference edge ID / task flow ID,不预先展开 `S×T` 邻接矩阵。
|
||||
@@ -408,7 +408,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 压缩计算的确定性 `dependencyDepth`,前端不再递归推导正式依赖层级。dependency 模式以 scope 化 `idle / loading / ready / failed` 状态阻断布局 Hook;图终态前不创建 fallback、不读取或写入 sidecar,图失败只以空图初始化一次。读取已有 dependency 布局时保留全部 `manuallyPlaced=true` 坐标,把 `manuallyPlaced=false` 作为可派生自动位置按最终深度重新协调;结果未变化时不写入。
|
||||
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 绘制生成资源笛卡尔积。
|
||||
|
||||
历史命令式 drag preview 句柄与局部连接索引可以保留,但项目工作台不再向资源卡传入该入口。拖动热路径、4096 张真实卡片拖动重渲染和 Chromium p95 门槛统一暂缓;当前回归只要求 Pointer Move 不改变卡片坐标、SVG path 或布局 revision。`ResizeObserver` 仍保持单图层单实例,任何实时 DOM 几何都不得通过 Tauri IPC 往返 Rust。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user