Skip to content

fix: eject <하위경로>/<파일> 이 다른 디렉터리 동명 원본의 변환본까지 삭제한다 #324

Description

@SeoyunL

요약

factlog eject <하위경로>/<파일>다른 디렉터리에 있는 동명 원본의 변환본까지 삭제한다.
경로로 지정했는데도 매칭이 basename 으로 퇴화한다. 원본은 남고 변환본만 사라지므로, 손대지
않은 소스가 조용히 추출 입력을 잃는다. 종료 코드는 0 이고 경고도 없다.

재현 (upstream/main c6d359d, macOS, pandoc 설치 환경에서 실측)

factlog init --target /tmp/kb && export FACTLOG_ROOT=/tmp/kb
mkdir -p /tmp/kb/sources/sub
printf '<html><body><p>top level</p></body></html>\n' > /tmp/kb/sources/report.html
printf '<html><body><p>nested</p></body></html>\n'    > /tmp/kb/sources/sub/report.html
factlog ingest /tmp/kb/sources/report.html /tmp/kb/sources/sub/report.html --target /tmp/kb
# report.html     -> runs/sources/report.html.md
# sub/report.html -> runs/sources/sub/report.html.md

factlog eject sub/report.html --target /tmp/kb     # 중첩본만 지우려는 의도
factlog eject (KB: /tmp/kb): 2 matched source ref(s):
  - runs/sources/report.html.md        [on disk]      <- 요청하지 않은 최상위본의 변환본
  - runs/sources/sub/report.html.md    [on disk]
  runs/sources conversion(s) to delete: 2
factlog eject: 2 conversion(s) deleted, ... ; accepted.dl recompiled

결과: 변환본 두 개 모두 삭제. sources/report.html 원본은 그대로 남아 있으나 변환본이
없어져 추출 대상에서 빠지고, 그 소스가 뒷받침하던 사실은 다음 sync 에서 근거를 잃는다.

근거

provenance 헤더에는 디렉터리 정보가 이미 들어 있다(업스트림 #214 가 넣었다).

runs/sources/report.html.md      <!-- ... | source: report.html     | converter: pandoc | ... -->
runs/sources/sub/report.html.md  <!-- ... | source: sub/report.html | converter: pandoc | ... -->

factlog/cli.py:2344 가 그 정보를 버린다.

conv_origin[ref] = PurePosixPath(origin).name      # sub/report.html -> report.html

그리고 factlog/cli.py:2352-2356 의 경로 분기가 basename 끼리 비교한다.

if "/" in name:
    # A path was given: the exact original is handled above; for a
    # binary original also match the conversion it produced (by
    # recorded origin). Same-basename files elsewhere are NOT matched.
    return is_conv and conv_origin.get(ref) == np_.name

주석은 "Same-basename files elsewhere are NOT matched" 라고 계약을 명시하지만, 좌변은
:2344 에서 basename 으로 축소된 값이고 우변 np_.name 도 basename 이다. 양쪽에서
디렉터리가 지워지므로 경로 분기가 사실상 basename 매칭이고, 변환본이 미러링된
서브디렉터리(runs/sources/sub/)를 전혀 구분하지 않는다.

docs/reference/ignore-eject.md:41-43 도 "어간만 주면 같은 어간을 가진 모든 소스가
매칭된다" 고 적어, 경로 지정은 그보다 좁다는 것을 전제로 설명한다.

영향

  • 심각도 높음. 사용자가 명시적으로 좁혀 지정했는데 요청하지 않은 파일이 삭제된다.
  • 조용하다. 삭제 목록이 출력되긴 하지만 종료 코드 0 이고 "요청 범위를 넘었다" 는 신호가 없다.
  • 복구가 자동이 아니다. 원본은 남아 있으므로 재-ingest 로 복구할 수 있지만, 사용자가
    사고를 알아채야 가능하다.
  • 서브디렉터리로 소스를 정리하는 KB(문서가 권장하는 구성)에서 동명 파일은 흔하다
    (report.html, README.md, notes.md 등).

제안

conv_origin원본 경로를 그대로 보관하고(또는 basename 과 함께), 경로 분기에서
디렉터리까지 비교한다. 변환본은 runs/sources/ 아래 원본의 서브디렉터리를 미러링하므로,
ref 의 미러 경로와 주어진 경로의 디렉터리가 일치하는지 확인하면 된다.
:2344 의 basename 축소가 필요한 곳은 orphan 재구성 경로이므로, 그쪽에서 필요할 때만 줄이고
저장은 원본 형태를 유지하는 편이 안전하다.

수용 기준

  • eject sub/report.htmlruns/sources/sub/report.html.md 만 지운다.
  • eject report.html(최상위 경로 지정)이 sub/ 쪽을 건드리지 않는다.
  • 어간·bare filename 지정의 기존 넓은 매칭 동작은 불변(회귀 없음).
  • legacy basename 헤더(경로 없는 source:)를 가진 변환본도 종전대로 동작.
  • 동명 원본 2개 + 서브디렉터리 구성을 고정하는 하니스 테스트.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions