Skip to content

feat(dag): 6-node flow diagram + relations cycle detection fix#23

Open
villainscode wants to merge 1 commit intodevelopfrom
feature/dag-6-node-flow-redesign
Open

feat(dag): 6-node flow diagram + relations cycle detection fix#23
villainscode wants to merge 1 commit intodevelopfrom
feature/dag-6-node-flow-redesign

Conversation

@villainscode
Copy link
Copy Markdown
Contributor

Summary

  • 문서 맵을 6-node flow diagram으로 리디자인 (f4bccb4)
  • 이미지 업로드 버그 수정 (bbd81df)
  • 연관 링크 1차 수정 (be92840) + 사이클 감지 로직 재작성 (a9f4e5c)
  • 이메일 인증 구현 가이드 문서 추가 (eab2906)
  • assistant가 git commit/push를 실행할 수 있도록 settings 권한 추가 (175ccb5)

사이클 감지 수정 (a9f4e5c)

문제

  • PUT /api/v1/workspaces/:wsId/documents/:docId/relations 저장 시 400 (CIRCULAR_REFERENCE)
  • 기존 detectCycle이 (1) 현재 그래프만 보고 새 엣지를 시뮬레이션하지 않아 prev+next 동시 설정 시 진짜 미래 사이클을 놓침, (2) 양방향 짝 엣지 때문에 동일 prev/next 재저장 시 false positive 발생

해결

  • detectCycle을 미래 그래프 시뮬레이션 방식으로 재작성: setRelations가 삭제할 엣지(source=docId / target=docId 의 next)를 SQL 단계에서 제외하고, 새 prev/next 엣지를 in-memory에 주입한 뒤 docId에서 'next' 체인을 따라 docId로 돌아오는지 단일 traversal로 검사
  • 호출부 두 번 → 한 번으로 단순화 (apps/web/lib/server/services/relation-service.ts)

Test plan

  • pnpm --filter @markflow/web test (96/96 통과)
  • pnpm --filter @markflow/web exec tsc --noEmit 통과
  • 수동: 문서 상세 → 연관문서 추가/삭제 후 저장 → 400 미발생 확인
  • 수동: 진짜 사이클(예: A.next=B & A.prev=C with C→…→B) 차단 확인
  • 수동: DAG 6-node flow diagram 시각 검증

🤖 Generated with Claude Code

기존 detectCycle은 현재 그래프만 보고 새 엣지를 반영하지 않아 두 가지
결함이 있었다.

1. False positive: 같은 prev/next 값을 재저장할 때 기존 양방향 짝
   (B→A type='next') 때문에 CIRCULAR_REFERENCE(400)가 발생했다.
   증상: 문서 상세에서 연관문서만 추가/삭제 후 저장 시 400 에러.

2. False negative: prev와 next를 동시에 설정해 미래 그래프에 진짜
   사이클이 만들어지는 케이스(예: A.next=C, A.prev=B + 기존 C→…→B)
   를 감지하지 못했다.

setRelations가 적용되기 직전 상태(현재 엣지 - docId 인접 엣지 + 새
prev/next 엣지) 그래프를 메모리에 구성한 뒤, docId에서 'next' 체인을
따라가 다시 docId로 돌아오는지 확인하는 단일 traversal로 재작성했다.
호출부도 두 번에서 한 번으로 단순화했다.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
markflow-web Ready Ready Preview, Comment May 1, 2026 2:49am

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the cycle detection logic in the relation service to simulate the future state of the graph before applying updates. The implementation simplifies the process by combining forward and reverse checks into a single traversal starting from the modified document. However, a high-severity issue was identified: the current logic uses a Map<number, number>, which assumes a linear chain and cannot handle branching. If multiple documents reference the same document as their 'prev', the cycle detection will fail to explore all paths. The reviewer recommends using a Map<number, number[]> and a depth-first search (DFS) to ensure all possible paths are checked for circular references.

Comment on lines 95 to 113
const nextMap = new Map<number, number>();
for (const rel of nextRelations) {
for (const rel of surviving) {
nextMap.set(rel.sourceId, rel.targetId);
}
if (data.next) {
nextMap.set(numDocId, Number(data.next));
}
if (data.prev) {
nextMap.set(Number(data.prev), numDocId);
}

// Forward traversal: startNode -> ... -> searchFor?
let current: number | null = nextMap.get(numDocId) ?? null;
const visited = new Set<number>();
let current: number | null = startNode;
while (current !== null) {
if (visited.has(current)) break;
if (current === numDocId) return true;
if (visited.has(current)) return false;
visited.add(current);
const next: number | null = nextMap.get(current) ?? null;
if (next === searchFor) return true;
current = next;
}

// Reverse traversal: searchFor -> ... -> startNode?
const visited2 = new Set<number>();
let current2: number | null = searchFor;
while (current2 !== null) {
if (visited2.has(current2)) break;
visited2.add(current2);
const next: number | null = nextMap.get(current2) ?? null;
if (next === startNode) return true;
current2 = next;
current = nextMap.get(current) ?? null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 사이클 감지 로직은 Map<number, number>를 사용하여 각 문서가 최대 하나의 'next' 관계만 가진다고 가정하고 있습니다. 하지만 데이터베이스 스키마와 setRelations 로직상 여러 문서가 동일한 문서를 'prev'로 지정할 수 있으며, 이 경우 해당 문서는 여러 개의 'next' 관계를 가지게 됩니다(분기 발생). 이러한 경우 현재 로직은 일부 경로를 누락하여 사이클을 감지하지 못할 수 있습니다. Map<number, number[]>와 DFS(깊이 우선 탐색)를 사용하여 모든 가능한 경로를 탐색하도록 수정해야 합니다.

    const nextMap = new Map<number, number[]>();
    const addEdge = (s: number, t: number) => {
      const edges = nextMap.get(s) ?? [];
      edges.push(t);
      nextMap.set(s, edges);
    };
    for (const rel of surviving) addEdge(rel.sourceId, rel.targetId);
    if (data.next) addEdge(numDocId, Number(data.next));
    if (data.prev) addEdge(Number(data.prev), numDocId);

    const stack = [...(nextMap.get(numDocId) ?? [])];
    const visited = new Set<number>();
    while (stack.length > 0) {
      const curr = stack.pop()!;
      if (curr === numDocId) return true;
      if (!visited.has(curr)) {
        visited.add(curr);
        const targets = nextMap.get(curr);
        if (targets) stack.push(...targets);
      }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant