fix(graph): add_edge 가 없는 endpoint 에 타입을 지어내 dangling 엣지를 만든다 - #11
Open
epruseal wants to merge 1 commit into
Open
fix(graph): add_edge 가 없는 endpoint 에 타입을 지어내 dangling 엣지를 만든다#11epruseal wants to merge 1 commit into
epruseal wants to merge 1 commit into
Conversation
존재하지 않는 endpoint 로 add_edge 를 호출하면 성공을 보고하고 잘못된 타입의 행을 쓴다. 실제 노드의 타입이 space 기본값과 다르면 뒤늦게 진짜 노드를 만들고 재실행해도 교정되지 않고 중복으로 남는다. builder.py 의 타입 해석부(#4 도입)에서 lookup 이 None 을 돌려주는 것은 "그 노드가 없다"는 뜻인데, 폴백이 이를 "타입을 모른다"로 취급해 _space_to_default_type() 으로 space 의 첫 선언 타입을 지어낸다 (subject -> User, resource -> Project, evidence -> TextUnit ...). 여기에 백엔드 비대칭이 겹친다. Neo4jStore.upsert_edge 는 MATCH 기반이라 endpoint 가 없으면 아무것도 쓰지 않고 False 를 돌려주지만, LocalGraphStore 는 endpoint 검사가 없는 INSERT ... ON CONFLICT DO UPDATE 라 그대로 쓴다. graph_edges 의 PK 가 (from_type, from_id, relation, to_type, to_id) 로 타입을 포함하므로, 지어낸 타입이 실제와 다르면 재적재 시 정상 행이 따로 삽입되고 날조된 행은 dangling 으로 남는다. 실제 타입이 우연히 space 기본값과 같으면 재적재로 수렴한다. 수정: 양쪽 endpoint 를 확인하고 하나라도 없으면 Neo4j 가 이미 쓰던 "no match" 계약으로 거부한다. 예외를 던지지 않으므로 기존 호출자 계약이 유지된다. 거부 사유는 missing_nodes 로 반환하고 PostgreSQL 레지스트리 기록도 함께 건너뛴다(그래프에 없는 엣지를 레지스트리가 갖지 않도록). 스토어가 available=False 면 가드는 작동하지 않는다 - "노드 없음"과 "스토어 다운"을 구분할 수 없고, 어차피 그래프에 쓰지 않아 오염이 불가능하기 때문이다. 이 경우 기존 space 기본값 동작을 그대로 둔다. 실측 (d34352c 대비). resource 의 기본값은 Project 이고 실제 노드는 Document 인 경우: [수정 전] stores.neo4j='ok' 저장: [('User','u1','owns','Project','doc-late')] 노드 생성 후 재실행: 2행(정상 + 날조), dangling 1 [수정 후] stores.neo4j='no match (missing node: resource/doc-late)' stores.postgres='skipped (missing node)' missing_nodes=['resource/doc-late'] 저장: [] 노드 생성 후 재실행: 1행, dangling 0 대조군: 실제 타입이 기본값과 같은 경우(evidence 의 TextUnit)는 수정 전에도 재실행으로 1행 dangling 0 으로 수렴한다. 테스트 5종 추가(양쪽 endpoint 각각 결손, 양쪽 동시 결손, 정상 타입 보존, unavailable 시 가드 비작동). d34352c 트리에 이 테스트를 얹으면 3건 실패해 결함을 실제로 잡는 것을 확인했다. 패치 후 5 passed / 패치 전 3 failed 2 passed 전체 스위트 136 passed 3 skipped (base 131 + 신규 5)
epruseal
force-pushed
the
fix/add-edge-endpoint-guard
branch
from
July 30, 2026 01:54
b668ad8 to
792cf21
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
현상
존재하지 않는 endpoint 로
add_edge를 호출하면 성공을 보고하고, 잘못된 타입의 행을 씁니다.뒤늦게 진짜 노드를 만들고 재실행해도 교정되지 않고 중복 행이 남습니다.
원인
opencrab/ontology/builder.py:206-208(#4 에서 도입된 타입 해석부):lookup이None을 돌려주는 것은 "그 노드가 없다" 는 뜻인데, 폴백이 그걸 "타입을 모른다"로 취급해_space_to_default_type()으로 space 의 첫 선언 타입을 지어냅니다.여기서 백엔드 비대칭이 겹칩니다.
upsert_edge동작Neo4jStoreMATCH기반 - endpoint 가 없으면 아무것도 쓰지 않고False반환LocalGraphStoreINSERT ... ON CONFLICT DO UPDATE- 그대로 씀그리고
graph_edges의 PK 가(from_type, from_id, relation, to_type, to_id)라 타입이 키에 포함됩니다. 지어낸 타입이 실제 타입과 다르면 재적재로도 그 행을 만날 수 없어 영구 잔존합니다.수정
add_edge가 양쪽 endpoint 를 확인하고, 하나라도 없으면 Neo4j 가 이미 쓰던no match계약으로 거부합니다. 예외를 던지지 않으므로 기존 호출자 계약이 유지됩니다.missing_nodes로 반환하고logger.warning으로 남깁니다available=False면 가드는 작동하지 않습니다 - "노드 없음"과 "스토어 다운"을 구분할 수 없고, 어차피 그래프에 쓰지 않아 오염이 불가능하기 때문입니다. 이 경우 기존 space 기본값 동작을 그대로 둡니다결과 (실측)
같은 시나리오,
d34352c대비:테스트 5종 추가(양쪽 endpoint 각각 결손, 양쪽 동시 결손, 정상 타입 보존, unavailable 시 가드 비작동).
d34352c트리에 이 테스트를 얹으면 3건 실패해 결함을 실제로 잡는 것을 확인했습니다.배경
로컬 모드 배포에서 실제로 이 결함을 만났습니다. 한 배포의 그래프에 11,833건의
dangling 엣지가 이 경로로 생성돼 있었고, 전수 조사 결과 전부
from쪽 결손이며from_type이 예외 없이User(=subjectspace 의 첫 선언 타입)였습니다. 원인노드 4개가 다른 이유로 적재 중 누락돼 있었고, 그 노드를 복구하니 해소됐습니다
(실제 타입이
User였으므로 위 "수렴하는 경우"에 해당).타입이 실제와 다르게 박히는(= 위 "수렴하지 않는") 사례도 관측했습니다. 다만 이
PR 초안에 적었던 건수는 재확인 과정에서 맞지 않아 철회했습니다. 결함 서명 자체는
남아 있습니다 - 예를 들어 어떤 스냅샷에는
Evidence -[supports]-> Claim엣지가83건 있는데 해당 노드의 실제 타입은
CollectionCompleteness입니다(claimspace 의첫 선언 타입이
Claim).참고: 같은 폴백을 쓰는 형제 경로
이 PR 범위는
OntologyBuilder.add_edge입니다.apps/api/main.py의_resolve_node_types(:414)도 존재 검사 없이_space_to_default_type폴백만하고, 그 타입이
:677의ctx.graph.upsert_edge로 넘어갑니다. 리뷰어가 함께 보실 만한 지점이라적어 둡니다(범위를 넓히길 원하시면 이 PR 에 포함하겠습니다).