feat(skim): 멈춰 있던 소스 2종 복구 + 안정화 + 소스/소비 경로 확장 - #14
Merged
Conversation
added 7 commits
August 10, 2026 10:36
arxiv와 huggingface가 2026-08-08 이후 데일리 크롤에서 매번 0건이었다. 원인이 서로 달랐다. arxiv: 주말 announce 공백을 메우는 요일 규칙이 `days is None`일 때만 걸려 있었다. 일일 배치는 `crawl all --days 1`로 돌아 그 규칙을 매번 덮어썼다. 규칙을 바닥값으로 옮겨 `min_lookback_days()`로 통일했다. huggingface: Daily Papers는 큐레이션 목록인데 크롤러가 `publishedAt`(= arXiv 발행일)로 창을 잘랐다. 실측으로 목록의 publishedAt 최댓값이 5일 전이라 3일 창에서 전량 걸러졌다. HF가 목록에 올린 날짜인 `paper.submittedOnDailyAt`으로 거른다. 같이 고친 것: - 본문 결손 경고가 API형 4종에서 100% 오탐이었다. 집계는 승격 전 Post를 보고 승격은 save_posts 안에서 일어났다. `canonical_body()`를 양쪽이 함께 쓴다. 이 오탐이 매일 4줄씩 찍혀서 바로 위의 진짜 0건 경고를 덮고 있었다. - save_posts의 commit/close가 try 밖이라 sqlite3.Error가 아닌 예외에서 RESERVED 락이 남았다. try/finally로 감싸고, 같은 결함이 있던 6개 함수도 함께 고쳤다. extra의 직렬화 불가 값은 `default=str`로 받아 배치를 날리지 않는다. - 백업이 0벌이었다. stdlib 온라인 백업 기반 `skim backup`(회전 보관)과 `PRAGMA quick_check`를 넣고 doctor에 integrity/외부 CLI 확인과 `--strict`를 붙였다. - 데일리 스크립트에 중복 실행 락(macOS에 flock이 없어 mkdir 원자성), 로그 로테이션, 크롤 전 백업, doctor 리포트 분리를 넣었다. - 크로스소스 중복을 묶을 `posts.canonical_url`을 추가했다. 행 병합은 하지 않는다.
AGENTS.md는 "댓글 수집 실패는 게시글 저장을 막지 않는다"고 못박아 뒀는데 실제로는 안 지켜지고 있었다. try가 HTTP 호출만 감싸고 JSON walk와 렌더는 밖이라, 상류 응답 구조가 바뀌면 파싱 예외가 크롤 루프까지 올라가 그 회차 50건이 저장 0건이 됐다. - threads/reddit/linkedin: attach 루프에서 파싱 실패를 잡고 다음 게시글로 넘어간다. HTTP 실패는 기존대로 fetch 안에서 조용히 None이 되고, 여기서 잡는 건 구조 변경이라 경고를 남긴다. - x: _reply_section은 _parse_tweets 루프 안에서 불려서 예외가 새면 트윗이 통째로 유실된다. 본체를 _build_reply_section으로 빼고 바깥은 절대 예외를 올리지 않는다. reddit 서킷브레이커 오탐도 같이 고쳤다. "유효 댓글 0건"과 "HTTP 실패"가 둘 다 None이라 구분되지 않아, 조용한 서브레딧에서 0건 글 3개가 연속되면 남은 게시글 전체의 댓글 수집이 중단됐다. 링크 게시물은 댓글이 사실상 본문이다. threads가 이미 쓰던 `comments < 1이면 건너뛴다` 가드를 reddit과 linkedin에도 넣었다. linkedin 댓글에 reddit과 같은 호스트별 요청 간격(1초)을 붙였다.
`fetch_feed`가 단발 요청이라 503 한 번이면 그 소스의 그날 수집분이 빈 리스트로 끝났다. 데일리가 고정 창으로 돌아 다음 날 창에는 그 항목이 다시 안 들어오므로 그대로 영구 유실이었다. ailabs에만 있던 retry 세션을 feed_utils로 올려 `make_retrying_session()`으로 공용화하고 fetch_feed와 enrichment가 함께 쓴다. 429의 Retry-After를 존중하도록 켰다. 세션 쿠키로 계정이 식별되는 API 크롤러 4종에는 붙이지 않는다. 거기서 자동 재시도는 차단 신호를 무시하고 계속 두드리는 것과 같다. 렌더 경로의 `thread.join()`에 상한이 없어서 playwright launch나 page.content()가 멈추면 크롤 프로세스가 무기한 정지했다. daemon 스레드 + 상한으로 그 항목만 포기한다. UA 상수가 4곳에 흩어져 Chrome/124를 들고 있었다. news.hada.io가 124를 403으로 막은 뒤 feed_utils만 139로 올려서, enrichment와 ailabs와 playwright 컨텍스트는 계속 막힌 버전을 쓰고 있었다. `feed_utils.USER_AGENT` 하나로 수렴시켰다.
…뉴스레터·한국 블로그) 후보 URL 23개를 실제로 찔러 보고 200 + 엔트리가 확인된 것만 넣었다. - arXiv: cs.AI 단독 -> cs.AI/cs.CL/cs.LG/cs.CV. 교차 등록 논문은 abs 링크로 dedupe한다. 크롤러가 합친 뒤 --count로 자르므로 수집량은 그대로고 커버리지만 넓어진다. URL을 http -> https로 바꿨다 (리다이렉트 중 feedparser가 빈 피드를 주는 경우가 있다). - Hacker News: newest에 Show/Ask 피드를 더했다. newest는 30점 문턱이 걸려 있어 본문이 알맹이인 Ask/Show가 대부분 걸러졌다. 크롤러는 이미 Ask/Show 본문 처리를 갖췄다. 같은 글이 두 피드에 오면 링크로 dedupe한다. - Lobsters(신규): RSS + 게시물 JSON 1건으로 댓글과 본문 폴백을 함께 받는다. JSON이 comment_plain/score/depth를 줘서 HN처럼 Algolia를 따로 부를 필요가 없다. - Bluesky(신규): 무인증 공개 XRPC라 계정이 노출되지 않는다. 리포스트와 작성자 self-reply는 뺀다. 볼 계정은 BLUESKY_ACCOUNTS에서 관리한다. - AI 랩 +4: Google DeepMind, Google Research, Hugging Face Blog, Mistral. - 뉴스레터 +4: Latent Space, Import AI, Simon Willison, TLDR AI. - 한국 기술 블로그 +4, GitHub 릴리스 +2. 떨어뜨린 것도 docs/TODO에 남겼다. Meta AI(400), DeepSeek(401), 그리고 요즘IT는 200에 엔트리 30건이 오지만 발행일 필드가 하나도 없어 fetch_feed가 전량 버린다. 등록하면 매번 0건인데 겉보기엔 멀쩡해서, 지금 고치고 있는 침묵 실패와 같은 부류다. merge 후 `skim source sync`와 `skim source list --emit markdown > docs/SOURCES.md`가 필요하다. tracked_sources는 DB에 있어 저장소 diff로는 안 따라온다.
`research --emit json`이 항상 content_markdown을 전문으로 실었다. 실측 530KB (원본 DB에서는 16.6MB). 중간 단계가 없어서 대안은 3줄짜리 `--emit summary`뿐이라, 스킬이 권하는 명령을 에이전트가 그대로 실행하면 컨텍스트가 터졌다. - `--fields`, `--max-chars`를 research와 bundle에 붙였다. 같은 질의가 530KB -> 1.1KB(--fields) / 4.2KB(--max-chars 500)로 줄어든다. 자른 글에는 `truncated`와 원래 길이를 남긴다. 조용히 자르면 받는 쪽이 본문이 그만큼인 줄 안다. - `bundle`을 topic 없이 부르면 최근 글을 본문까지 담는다. 예전에는 posts가 빈 배열이라 다이제스트 같은 소비자가 CLI를 우회해 sqlite3로 DB를 직접 열었다. `--group-by platform|source|date`로 summary.md에 묶음 목차를 만든다. 카테고리는 소비자마다 다르므로 skim에 특정 분류를 박지 않는다. - `skim export`: 정본 본문을 마크다운(YAML frontmatter) 또는 JSON으로 꺼낸다. 본문 없는 행은 건너뛴다 (vault를 빈 노트로 오염시키지 않는다). - `source export/import`: OPML 왕복. 내보내기만 있고 되읽는 경로가 없어서 DB가 깨지면 등록 소스를 손으로 다시 넣어야 했다. 21개 왕복을 테스트로 고정했다. OPML은 사용자 파일이라 신뢰 경계다. DOCTYPE이 있으면 파싱 전에 거절한다 (expat이 내부 엔티티를 펼쳐 billion-laughs가 된다. 외부 엔티티는 stdlib이 애초에 해석하지 않는다). OPML 스펙에 DTD가 필요 없어 정상 파일을 막지 않는다.
하루 200건대가 들어오는데 어디까지 봤는지 표시할 데가 없었다. `feedback(post_id, action)` 테이블과 `add_feedback()`이 스키마에는 있었지만 호출자가 0개, 행이 0개라 약속만 있고 구현이 없는 상태였다. `posts.is_read` 컬럼을 새로 만드는 대신 그 테이블을 쓴다. - `set_post_state(post_id, state)`: read/archived/None. 상태는 게시글당 하나라 바꿀 때 앞의 값을 지운다. - `skim mark <ids> --state read|archived|unread` - `skim export --unread`로 아직 안 본 것만 꺼낸다. - 데스크톱 리더에 토글 버튼. CLI와 같은 행을 쓰므로 어느 쪽에서 눌러도 같이 보인다. `uv run skim`을 띄우지 않고 DB에 직접 쓴다 (토글 한 번에 수백 ms가 걸리면 못 쓴다). 데스크톱은 파이프라인이 소유한 DB를 읽는 쪽이라, `feedback`이 없는 옛 파일에서는 상태 칸을 NULL로 채운다. prepare 단계에서 통째로 죽으면 목록이 아예 안 뜬다.
고친 버그마다 "다음 사람이 같은 자리에서 또 밟는다"는 공통점이 있어서, 코드가 아니라 규칙으로 남겨야 하는 것만 AGENTS.md에 넣었다. - 좁은 창에서 0건이 나오는 소스: 보정은 `days is None` 분기가 아니라 바닥값으로 넣는다. 데일리가 `--days 1`로 돌아 그 분기를 매번 덮어쓴다. - 거르는 기준 필드: 큐레이션 목록은 원문 발행일이 아니라 목록에 올린 날짜로 거른다. - 새 소스: `fetch_feed`가 날짜 없는 엔트리를 버리므로, 200에 엔트리가 와도 등록하면 0건일 수 있다. 실측 안 한 URL은 넣지 않는다. - 댓글: `comments > 0`일 때만 조회하고, 파싱까지 try 안에 넣는다. - db.py: 연결 여는 함수는 try/finally. `canonical_body()`가 정본 본문 판정의 단일 소스. 소비 상태는 posts 컬럼이 아니라 feedback 테이블. CI의 `uv sync`를 `uv sync --locked`로 바꿨다. 락파일과 어긋나면 조용히 갱신하고 통과하던 자리다.
uv.lock이 전역 uv 설정의 상대 exclude-newer(P1W)를 기록하는데 CI에는 그 설정이 없어, --locked가 lockfile 갱신 필요로 판정하고 실패했다. 락 그대로 설치한다는 원래 목적은 --frozen이 그대로 만족한다.
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.
TL;DR
--days 1에서 각각 50건이 들어온다.--fields/--max-chars), 마크다운 export, OPML 왕복, 읽음 상태를 붙였다.테스트 371 -> 439 (Python) + 20 (Swift).
just lint10.00/10,just e2e통과.1. 지금 멈춰 있던 것
크론 로그에
[!] 최근 14일간 수집되던 플랫폼이 0건입니다: ailabs, arxiv, huggingface가 이미 찍혀 있었지만, 바로 아래 줄의 오탐 4건에 묻혀 아무도 보지 않았다.원인이 서로 달랐다
arxiv — 주말 announce 공백을 메우는 요일 규칙이
days is None일 때만 걸려 있었다. 데일리는crawl all --days 1로 돌아 그 규칙을 매번 덮어썼다. 규칙을 바닥값으로 옮겨min_lookback_days()로 통일했다.huggingface —
MIN_LOOKBACK_DAYS = {"huggingface": 3}이라는 바닥값이 이미 있었다. 그래서 창 문제가 아니었다. API를 직접 찔러 보니 Daily Papers는 큐레이션 목록인데 크롤러가publishedAt(= arXiv 발행일)로 창을 잘랐고, 목록의publishedAt최댓값이 5일 전이라 3일 창에서 전량 걸러졌다. HF가 목록에 올린 날짜인paper.submittedOnDailyAt으로 거르게 바꿨다.전역
--days 3확대는 하지 않았다. enrichment가 이미 저장된 항목까지 매일 다시 돌아 비용이 3배가 된다.2. 조용한 실패
save_posts진입 전 Post를 보는데, API형 4종의 본문 승격은save_posts안에서 일어난다. 정상 저장돼도 매일 "50/50 실패"로 찍혔다.canonical_body()를 양쪽이 함께 쓴다save_posts락 누수commit()/close()가 try 밖이라sqlite3.Error가 아닌 예외에서 RESERVED 락이 남았다. try/finally + 같은 결함이 있던 6개 함수도 함께 고쳤다None이라 조용한 서브레딧에서 0건 글 3연속이면 남은 전체가 중단됐다.comments < 1가드fetch_feed가 단발이라 503 한 번에 그날 수집분이 사라졌다.make_retrying_session()으로 공용화 (세션 쿠키 쓰는 API 4종은 제외 — 계정 안전)thread.join()에 상한이 없어 playwright가 멈추면 프로세스가 무기한 정지skim backup(온라인 백업 + 회전) +doctor --strict/integrity/PATH 확인feed_utils만 139로 올려서 enrichment·ailabs·playwright는 계속 막힌 버전을 썼다데일리 스크립트에 중복 실행 락(macOS에 flock이 없어 mkdir 원자성), 로그 로테이션, 크롤 전 백업을 넣었다.
3. 소스 확장 (13 -> 15 플랫폼)
후보 URL 23개를 실제로 찔러 200 + 엔트리가 확인된 것만 넣었다.
추가된 소스
--count로 자르므로 수집량은 그대로고 커버리지만 넓어진다. http -> https떨어뜨린 것도
docs/TODO.md에 이유와 함께 남겼다. Meta AI(400), DeepSeek(401), 그리고 요즘IT는 200에 엔트리 30건이 오지만 발행일 필드가 하나도 없어fetch_feed가 전량 버린다. 등록하면 매번 0건인데 겉보기엔 멀쩡해서, 이번에 고친 침묵 실패와 같은 부류다.4. 소비 경로
research --emit json이 본문을 전문으로 실어 실측 530KB(원본 DB에서는 16.6MB)였다. 중간 단계가 없어 대안은 3줄짜리--emit summary뿐이라, 스킬이 권하는 명령을 에이전트가 그대로 실행하면 컨텍스트가 터졌다.자른 글에는
truncated와 원래 길이를 남긴다. 조용히 자르면 받는 쪽이 본문이 그만큼인 줄 안다.bundle을 topic 없이 부르면 최근 글을 본문까지 담는다 (예전엔 빈 배열이라 소비자가 sqlite3로 DB를 직접 열었다).--group-by platform|source|dateskim export: 마크다운(YAML frontmatter) 또는 JSONsource export/import: OPML 왕복. DOCTYPE은 파싱 전에 거절한다 (사용자 파일이라 신뢰 경계, billion-laughs)skim mark+ 데스크톱 읽음 토글: 죽어 있던feedback테이블 재사용 (호출자 0개, 행 0개였다).posts에 컬럼을 더하지 않는다5. 하지 않은 것
근거를 대서 배제했다. 자세한 이유는
/tmp/skim-gap-analysis-20260810.html.bundle의 파일 핸드오프가 같은 문제를 푼다uv sync --locked만Merge 후 필요한 것
tracked_sources는 DB에 있어 저장소 diff로 따라오지 않는다.검증
새 회귀 테스트 68건. 각각 이번에 고친 실패를 재현한다:
test_huggingface_daily_window,test_db_connection_safety,test_comment_failure_isolation,test_http_resilience,test_new_sources,test_consumption_paths,test_post_state.