v4 전수조사(메모 22건 + 추가 발견 7건)의 결과가 v5 에 반영돼 있다. 필드 구성·인덱스·정체성 축이 달라졌으므로 이 문서를 근거로 코드를 읽거나 고치지 말 것.
이 문서는 v4 시점의 기록으로 남긴다 — v5 의 각 판정이 "무엇을 바꾼 것인지" 를 확인할 때 참조한다. 판정 근거는 docs/features/social-recording-refactor/schema-refactor-plan.md 에 있다.
토큰 ↔ 소셜 연결의 확정 명세. v3 대비 링크 도착점 구조와 전 컬렉션 필드 판정이 바뀌었습니다. 미결은 웹 하나뿐이고 별도 작업으로 이관했습니다. 근거 문서는 §7 문서 지도에 전부 연결해 두었습니다.
v3 는 엣지가 contents 만 가리키게 설계했습니다. 그 결과 토큰이 건 링크 중 Content 가 아니거나
fetch 에 실패한 것은 기록 자체가 남지 않았습니다 — audit 실측 7,742 URL 중 약 38.3% 가 종류 때문에,
그 위에 유료 게이트·IP 차단으로 상당수가 더 탈락합니다. v4 는 모든 URL 을 객체로 만들고,
엣지가 contents · accounts · venues 를 다형으로 가리키게 바꿉니다.
정체성은 문자열 비즈니스 키 대신 _id 대리키가 맡아, 나중에 정체가 밝혀져도 링크가 끊기지 않습니다.
이 절이 답하는 것 — 컬렉션 7개가 서로를 어떻게 가리키는가.
첫째, 링크의 도착점이 3종입니다. v3 에서 contents 하나였던 것이 object +
object_id 다형 포인터로 바뀌었습니다. 프로필·채널 링크가 이제 기록됩니다.
둘째, 위성 3종(content_raw · metric_series · token_links)이 같은 방식으로
객체를 가리킵니다. 참조 규칙이 하나라 새 객체 종류가 생겨도 위성은 바뀌지 않습니다.
셋째, accounts 만 subtype 이 없습니다. platform 을 아는 순간 종류가
결정되기 때문입니다(gh→owner, rd→user). venues 는 Telegram 이 같은 URL 에서 channel·portal·shell 로
갈리므로 유지합니다.
이 절이 답하는 것 — 새 필드가 생겼을 때 어디에 놓을지 어떻게 정하는가.
질문 1 · 이 필드로 쿼리하는가? (filter · sort · join · group · index) 아니오 → 개별(data) 예 → 질문 2 질문 2 · 값의 의미가 모든 subtype 에서 같은가? 아니오 → 개별(data) 예 → 질문 3 질문 3 · 이 값의 원본(SoT)이 다른 곳에 있는가? 예 → 최적화용 아니오 → 공용
| 구분 | 정의 | 계약 |
|---|---|---|
| 공용 | 최상위 필드. 우리가 이름을 정하고 타입을 강제한다 | snake_case · 타입 명시 · 쿼리 축이면 인덱스 |
| 개별 | data 안. 플랫폼이 준 원래 이름 그대로 | 이름을 바꾸지 않는다. 쿼리 대상이 아니다. 네임스페이스 없음 — platform 이 최상위 필드라 접두어가 중복 |
| 최적화용 | 원본이 다른 곳에 있는 사본·파생값 | 원본 위치·갱신 시점·사용 제한을 반드시 명시. 1차 필터 전용, 정밀 판단 금지 |
채움률은 기준이 아닙니다. account_created_at 은 Instagram 이 구조적으로 주지 않지만
"계정 나이는 rug 1순위 신호" 라 공용입니다. 반대로 채움률이 100% 여도 의미가 갈리면 개별입니다 —
views 는 TikTok 이 자동재생, X 가 노출, YouTube 가 30초 시청이라 이름을 통일하면 오염됩니다.
_id 로 끝나면 → 항상 ObjectId. 우리가 발급하고, 우리 DB 안의 다른 행을 가리킨다
_key 로 끝나면 → 항상 외부 문자열. 남이 발급하고, 우리가 만들지 않는다
_id · creator_id · venue_id · parent_content_id · object_id ObjectId
platform_key '2070140594496626759' 그 플랫폼 안에서 이 대상을 가리키는 값
url_key 'foo.com' URL 에서 뽑은 식별자 (sites 용, 웹 작업)
예외 token_address — address 는 ObjectId 로 오인될 수 없어 이름을 그대로 둔다
두 종류를 섞으면 함수 시그니처가 (id: string) 일 때 둘 다 통과해서 조용히 틀립니다.
필수 ● 값이 없으면 저장 자체가 안 된다
기본값 필수지만 기본값이 있어 항상 존재한다 (배열 → [], 객체 → {})
선택 ○ 없을 수 있다
"기본값" 을 따로 두는 이유는 인덱스 때문입니다. 필드가 항상 존재하면 partial index 를 만들 수 없습니다 —
partialFilterExpression 은 $ne 도 $size 도 지원하지 않아
"빈 배열 제외" 를 표현할 방법이 없습니다.
이 절이 답하는 것 — 이 스키마가 성립하기 위해 파이프라인이 지켜야 할 약속.
unknown 도 객체로 저장한다.platform_key 까지 풀어 기록하고, 불가능하면 URL 로 알 수 있는 데까지만 채운다._id 로 연결한다. 어디에서(object) + 어떤 것(object_id).source_urls[]._id 참조는 우리가 관측한 대상에만 쓴다. 관측하지 않은 언급(mentions[])은
값 그대로 저장한다 — 그러지 않으면 트윗 3,879건의 멘션마다 빈 계정 행이 생긴다.1. source_urls.url 로 찾는다 이미 본 URL 이면 여기서 끝 2. 없으면 platform_key 로 찾는다 다른 URL 로 같은 대상을 본 경우 3. 그래도 없으면 새 행
Instagram 처럼 나중에 platform_key 가 shortcode → 숫자 pk 로 바뀌어도 1번이 먼저라 기존 행을 찾습니다.
관측 시점에 해결하지 못한 매핑은 나중에 소급 확정하지 않습니다. t3 에
tiktok.com/@doge 를 봤는데 그때 fetch 를 못 했다면, 그 시점 소유자가 누구였는지는
어떤 스키마로도 복원되지 않습니다. 관측하지 않은 사실이기 때문입니다.
이 절이 답하는 것 — 각 컬렉션이 어떤 필드를 갖고, 각 필드가 왜 그 자리에 있는가.
contents — 플랫폼 콘텐츠 공간의 좌표
트윗·영상만이 아니라 검색·미분류·단축링크까지 담습니다. TikTok 에서 유행하는 것은 search 로도
video 로도 표현되는데, 나누면 "이 소셜에서 무엇이 주목받나" 를 보려고 두 컬렉션을 확인해야 합니다.
그리고 unknown 은 모르는 사이트가 아니라 "아는 플랫폼인데 그 안 어디인지 모르는 것" 입니다 —
라우터가 모르는 호스트를 전부 website 로 떨어뜨리기 때문이고, 실측 398건이 전부
x.com(282)·tiktok.com(97)입니다.
| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | 정체성. 링크·원본·시계열이 이 값을 가리킨다 |
| platform | enum | ● | 공용 | x·tt·ig·yt·rd·gh + web(잠정, §6) |
| subtype | enum | ● | 공용 | tweet·video·photo·post·reel·tv·story·repo·gist·search·intent·shortlink·unknown + site(잠정). 기존 form·productType 을 흡수한다 |
| platform_key | string | ○ | 공용 | 그 플랫폼 안에서 이 대상을 가리키는 값. 응답이 주면 응답 값, 없으면 URL 에서 뽑은 값. search 는 검색어 |
| source_urls[] | object[] | ● | 공용 | { url, status, observed_at }. 기존 행을 찾는 1차 수단 |
| creator_id | ObjectId | ○ | 공용 | → accounts. 검색·미분류·fetch 실패는 작성자가 없다 |
| venue_id | ObjectId | ○ | 공용 | → venues. X community · Reddit subreddit 만 |
| parent_content_id | ObjectId | ○ | 공용 | → 자기 참조. 계보 역추적 인덱스가 account_edges 컬렉션을 대체한다 |
| parent_relation | enum | ○ | 공용 | quote·retweet·fork. 참조하는 쪽의 속성 — 원본 트윗은 자기가 인용당했는지 모른다 |
| published_at | Date | ○ | 공용 | 발행 시각. 저장 시각(created_at)과 반드시 구분 |
| observed_at | Date | ● | 공용 | 회귀 방지 가드 |
| text.primary | string | ○ | 공용 | ig caption · yt/rd title · tt/x text · gh description. 검색어·해시태그도 여기 들어간다 — 그래서 모든 subtype 이 값을 가지므로 Q2 를 통과한다 |
| text.body | string | ○ | 공용 | yt description · rd body. GitHub README 는 당분간 채우지 않는다 — 별도 호출 1회가 드는데 비용 대비 가치가 미측정이다. 필드는 두고 값만 비운다 |
| text.from_media | string | ○ | 공용 | ig alt · tt transcriptionLink. 시각 콘텐츠의 유일한 무료 진입점 |
| links[] | string[] | 기본값 | 공용 | 본문 아웃바운드 링크. drainer·CA 축 |
| tags[] | string[] | 기본값 | 공용 | 해시태그 · GitHub topics |
| mentions[] | object[] | 기본값 | 공용 | { platform_key, screen_name }. _id 참조 아님(전제 6) |
| author{} | object | ○ | 최적화 | 관측 시점 작성자 스냅샷. accounts 와 의도된 중복 — 마스터는 덮이므로 "그때 팔로워 수" 를 잃는다 |
| metrics_latest | Mixed | ○ | 최적화 | SoT 는 metric_series. 1차 필터 전용 |
| link_stats{} | object | ○ | 최적화 | { link_count, first_linked_at, computed_at, input_cutoff }. computed_at 없이 읽으면 안 된다 |
| data | Mixed | 기본값 | 개별 | 플랫폼 원래 이름 유지. conversationId·musicId·pushedAt·moderation.* 등 |
| created_at / updated_at | Date | ● | 공용 | BaseModel |
제거 — content_key(정체성이 _id 로 이동) · source_type(platform+subtype 으로 분해) · source_key(source_urls[] 로 대체) · ext(data 로 개명, 네임스페이스 제거) · form · fetch_status(source_urls[].status 와 중복) · data.productType
accounts — 누가| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | |
| platform | enum | ● | 공용 | subtype 없음 — platform 이 종류를 결정한다 |
| platform_key | string | ○ | 공용 | x id · gh login. Reddit 은 username 문자열 — 불변·재사용 불가라 이름 자체가 id |
| source_urls[] | object[] | ● | 공용 | |
| handles[] | object[] | 기본값 | 공용 | { value, first_seen, last_seen }. 구간이 핵심 — TikTok 은 핸들을 회전시키고 옛 핸들을 재할당한다 |
| account_created_at | Date | ○ | 공용 | rug 1순위 신호. 6개 중 5개가 준다. Instagram 만 구조적 부재 |
| display_name | string | ○ | 공용 | 사실상 x·yt |
| bio | string | ○ | 공용 | 지갑 주소가 실제로 여기 노출된다 — 실측 "… BNB: 0x0Ae024D3C20…" |
| links[] | string[] | 기본값 | 공용 | 프로필 아웃바운드 링크 |
| declared_handles[] | object[] | 기본값 | 공용 | { platform, value }. 사실상 gh twitter_username |
| known_wallets[] | string[] | 기본값 | 공용 | 온체인 ↔ 소셜 조인 축. bio 에서 추출한 파생 키 — 원본·파생 관계라 bio 로 대체 불가 |
| metrics.followers | number | ○ | 공용 | 의미가 전 플랫폼 동일. contents.author.followers 의 원본 |
| moderation.unavailable | boolean | ○ | 공용 | 계정 정지·삭제. "런치 후 계정이 사라졌다" |
| observed_at | Date | ● | 공용 | 중첩 스냅샷은 부모 문서 값을 상속 |
| data | Mixed | 기본값 | 개별 | following·totalKarma·heart·viewCount·stars·content_count·badges·account_type·source_ref |
| created_at / updated_at | Date | ● | 공용 |
제거 — subtype · moderation.unavailable_reason · account_key · ext.
account_type(gh User/Organization)과 source_ref(프로필 직접 조회가 아님을 남기는 값)는 data 로 내렸다 — 조직 계정 여부가 신호인지 검정 기록이 없고, 신뢰도 정보는 쿼리 축이 아니다. metrics.content_count 도 data 로 내렸다 — Reddit 이 안 주고 세는 대상이 플랫폼마다 다르다(x statusesCount 는 리트윗 포함).
venues — 어디서| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | |
| platform | enum | ● | 공용 | x(community) · rd(subreddit) · tg |
| subtype | enum | ● | 공용 | community·subreddit·channel·portal·shell·guard_group. 유지하는 이유 — tg 가 같은 URL 에서 셋으로 갈린다(포털 54%·빈껍데기 25%·활성 4%) |
| platform_key | string | ○ | 공용 | x community id · rd slug(불변) · tg 는 groupId. 채널명은 mutable 이라 조인키로 부적합(이관 실측 2건) |
| source_urls[] | object[] | ● | 공용 | |
| name | string | ○ | 공용 | tg title. 가변이므로 as-of 판단에 쓸 때 주의 |
| description | string | ○ | 공용 | 토큰 CA 가 실제로 여기 노출된다 |
| venue_created_at | Date | ○ | 공용 | "런치 직전 급조" 신호. Reddit 은 현재 수집 경로 없음 |
| creator_id | ObjectId | ○ | 공용 | → accounts. 핸들 fallback 금지 — 응답 키가 userName/screen_name 으로 갈린다 |
| metrics.members | number | ○ | 공용 | tg subscriberCount · rd subscribers |
| observed_at | Date | ● | 공용 | |
| data | Mixed | 기본값 | 개별 | moderators · adminHandle |
| created_at / updated_at | Date | ● | 공용 |
제거 — channel_state(subtype 이 담당) · data.nameMatch(token_links 로 이동) · data.subredditSubscribers(metrics.members 와 중복) · venue_key · ext
token_links — 토큰 ↔ 객체 · append-only
절대 덮어쓰지 않습니다. status 가 ok → deleted 로 바뀐 시각이 신호이고,
그건 append 로만 얻어집니다. 리포지토리에 update/upsert 메서드를 아예 만들지 않아
타입 레벨에서 막습니다. contents.fetch_status 를 제거했으므로 상태 이력은 여기에만 남습니다.
| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | |
| token_address | string | ● | 공용 | mint 주소. ObjectId 를 쓰지 않는다 — 토큰은 재분류·병합이 없어 대리키가 할 일이 없다 |
| object | enum | ● | 공용 | contents·accounts·venues. $lookup 의 from 이 상수여야 하므로 이 값으로 먼저 $match 한다 |
| object_id | ObjectId | ● | 공용 | {object_id, linked_at} 이 "이 객체를 건 토큰 수" 의 유일한 경로 |
| source_url | string | ● | 공용 | 재할당의 안전장치. object_id 는 바뀌어도 이 값은 안 바뀐다 — 그래서 재할당 이력 컬렉션이 따로 필요 없다 |
| linked_at | Date | ● | 공용 | as-of 절단축. 덮어쓰면 순번이 복원 불가 |
| observed_at | Date | ● | 공용 | |
| status | enum | ● | 공용 | ok·deleted·unavailable·unresolved. 상태 이력의 SoT |
| name_match | boolean | ○ | 공용 | tg 채널명에 토큰명이 들어있나. title 이 가변이라 나중에 재계산 불가 → 관측 시점에 남긴다 |
| platform | enum | ● | 최적화 | 대상에서 복사. 플랫폼별 노출 집계를 이 컬렉션 1회 조회로 |
| subtype | enum | ● | 최적화 | 대상에서 복사. "tiktok search vs video 비교" 가 여기서 끝난다 |
| first_transfer_at | Date | ○ | 최적화 | Δt 계산 조인 0회. token_created_at 을 대체 — 입력 계약에 발행 시각은 없다 |
| lineage[] | ObjectId[] | 기본값 | 최적화 | 자기 + 조상, 길이 상한 2. 자기를 0번에 넣어 $or 를 없앤다 |
| link_depth | number | ● | 최적화 | lineage.length − 1. 인자로 받지 않는다 — 둘이 어긋나면 에러 없이 틀린 수가 나온다 |
| created_at / updated_at | Date | ● | 공용 |
제거 — content_key(유실 문제의 원인) · source_field(발행자가 칸을 구분해 쓰지 않는다 — website 칸에 Reddit 18·GitHub 15·TikTok 검색 6건. discovered 는 출처 불명, adhoc 은 실측 0건)
metric_series — 지표 시계열 · 대상당 1행관측마다 새 행이 아니라 한 행의 배열에 쌓습니다. 속도 계산이 읽기 1회로 끝나고 배열이 이미 순서대로입니다. 한 포인트가 약 150 B 라 설계 상한인 200 포인트여도 30 KB 입니다.
| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | |
| object / object_id | enum / ObjectId | ● | 공용 | 다형 확정. 콘텐츠뿐 아니라 계정 팔로워·베뉴 멤버 시계열까지 한 컬렉션이 덮는다 — B-3(팔로워 급증 Z-Score 선행 탐지)이 계정 시계열을 요구하고, 대상당 1행 구조라 추가 비용이 없다 |
| platform | enum | ● | 최적화 | 대상에서 복사. 코호트 필터 |
| points[] | object[] | 기본값 | 공용 | 관측마다 $push |
| points[].at | Date | ● | 공용 | 관측 시각. rd·tt 는 actor 의 crawledAt — 저장 시각을 쓰면 배치 지연이 속도에 섞인다 |
| points[].metrics | Mixed | ● | 개별 | 플랫폼 원래 이름. 쓰기 시점 키 화이트리스트 필요 — Mixed 는 검증하지 않아 오타 키가 조용히 저장된다 |
| created_at / updated_at | Date | ● | 공용 |
감수한 것 — 플랫폼 코호트 백분위를 인덱스 range scan 으로 못 낸다($unwind 필요). 백분위는 원래 파생 레이어(metric_ranks)에서 주기적으로 계산할 값이다.
content_raw — 무손실 원본 · 1:1| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | |
| object / object_id | enum / ObjectId | ● | 공용 | 기존 object_key+object_type 을 대체 |
| payload | Mixed | ● | 개별 | 응답 전체. 기존 이름 data 에서 개명 — contents.data 와 겹쳤다. 원본 보존이 곧 재해석 가능성: audit 이 alt·isSlideshow·author.id 를 놓친 원인이 잘라 저장한 덤프였다 |
| observed_at | Date | ● | 공용 | upsert 유지 — 최신 1건만. append 로 바꾸면 원본 이력이 쌓이지만 문서 하나가 수십 KB 라 관측 횟수만큼 곱해진다 |
| created_at / updated_at | Date | ● | 공용 |
제거 — platform(키 단건 조회만 하는 컬렉션이라 필터 필드가 필요 없다) · object_key · object_type. 컬렉션 이름이 내용과 어긋난다 — content_raw 인데 content·creator·venue 셋을 담는다. object_raw 로 개명할지는 §6.
tokens — 얇은 상태| 필드 | 타입 | 필수 | 구분 | 설명 |
|---|---|---|---|---|
| _id | ObjectId | ● | 공용 | 참조는 address 로 한다 |
| address | string | ● | 공용 | mint 주소 unique. token_links.token_address 의 조인 대상 |
| symbol | string | ○ | 최적화 | SoT 는 tracker. 표시와 name_match 계산 입력 |
| first_transfer_at | Date | ○ | 최적화 | 나이 앵커. FetchOptions.launchAt 주석이 "age anchor (= firstTransferAt)" |
| discovered_at | Date | ● | 공용 | 수집·백필 배치의 진입점 |
| fingerprints[] | string[] | ○ | 미결 | 웹 작업까지 현행 유지. domain:X 가 도메인 공유 카운트의 축이다 — §6 잠정 규칙 |
| web{} | object | ○ | 미결 | 웹 작업까지 현행 유지. §6 |
| created_at / updated_at | Date | ● | 공용 |
제거 — trigger_reason · unclassified_links[](모든 URL 이 객체가 되므로 역할 소멸 — 이 필드의 존재 자체가 "비객체를 담을 곳이 없다" 는 증상이었다) · price·ath_price·alpha_score(계속 변하므로 사본은 쓰는 순간 낡았고 백테스트를 오염시킨다)
이 절이 답하는 것 — 확정된 쿼리 축에서 직접 따라 나오는 인덱스.
| 컬렉션 | 인덱스 | 답하는 질문 |
|---|---|---|
contents | { platform, subtype } | "이 플랫폼에서 무엇이 주목받나". draft 의 핵심 요구 |
{ source_urls.url } multikey unique | 기존 행 찾기 1차. URL 중복 방지 | |
{ platform, subtype, platform_key } | 기존 행 찾기 2차 | |
{ creator_id, published_at −1 } | "이 KOL 의 최신 콘텐츠" — 연쇄 배포 탐지 | |
{ parent_content_id } partial | 계보 역추적. 대부분 null | |
{ venue_id } partial | 커뮤니티별 콘텐츠. 두 플랫폼만 값이 있다 | |
accounts | { source_urls.url } multikey unique | 기존 행 찾기 |
{ platform, platform_key } unique | 대상 동일성 | |
{ handles.value } multikey | 핸들 → 계정 역조회. 반드시 구간으로 as-of 해석 | |
{ known_wallets } multikey | 지갑 → 계정. partial 불가 — 기본값 []라 항상 존재 | |
venues | { platform, subtype, platform_key } | 대상 동일성 |
{ creator_id } partial | "이 계정이 반복 개설한 커뮤니티" | |
token_links | { object_id, linked_at } 주 경로 | "이 객체를 건 토큰 수" as-of |
{ token_address, linked_at } | "이 토큰의 모든 소셜" | |
{ lineage, linked_at } multikey | "실질 재사용 횟수". lineage 가 유일한 배열이라 복합 인덱스 합법 | |
{ platform, subtype, linked_at } | 플랫폼별 노출 집계. 비정규화 복사본 덕에 조인 0회 | |
metric_series | { object, object_id } unique | 대상당 1행 보장. 다형이라 object 를 앞에 둔다 |
{ platform } | 코호트 필터 | |
content_raw | { object, object_id } unique — 이것만 | 문서가 수십 KB 라 목록 조회를 만들면 캐시가 오염된다 |
tokens | { address } unique | 조인 대상 |
{ fingerprints } multikey | 도메인 공유 카운트. 웹 작업까지 유지 | |
{ discovered_at −1 } | 최근 유입 토큰 |
이 절이 답하는 것 — 아직 정하지 않은 것과, 구현 전에 고쳐야 할 코드.
1. 웹 URL 도 contents 에 넣는다. platform='web' · subtype='site'
2. tokens.web{} · fingerprints[] 는 현행 그대로 둔다
3. contents(web) 에 도메인 층 필드를 만들지 않는다 (site_id 도 아직 없음)
2번이 핵심입니다. 잠정 기간에도 도메인 공유 카운트가 기존 경로로 계속 작동합니다 —
검증된 주력 신호(knowyourmeme.com 22건 rug 100% · axiom.trade 21건 90% ·
orynth.dev 18건 0%)가 끊기지 않습니다.
왜 이 순서가 안전한가. "귀속" 을 기본값으로 두면 나중의 "분리" 가 전부 덧붙이기입니다 —
sites 를 만들고 contents(web) 행에 site_id 를 채우는 백필이면 끝이고
링크는 손대지 않습니다. 반대 순서는 파괴적입니다.
남는 근거. 웹은 지문의 출처부터 두 층입니다 — RDAP(도메인: domainCreatedDate 81.7%)와
HTML/OG·urlscan(페이지: isNews 76.0%, rug 80/89/100%). 도메인으로만 접으면 페이지 층이 뭉개지고
페이지로만 두면 도메인 지문이 복사됩니다. 확정하지 못한 이유는 website URL 중 apex 만인 비율을 모르기
때문이고, audit 샘플에 website 파일이 없어 지금은 측정할 수 없습니다.
필드 판정 문서(§7)를 얼린 뒤 남아 있던 미결입니다. 이 문서가 최신이고, 판정 문서에는 "보류" 로 남아 있습니다.
| 항목 | 확정 | 근거 |
|---|---|---|
contents.text.primary | 공용 | 검색어·해시태그도 여기 들어가므로 모든 subtype 이 값을 갖는다 → Q2 통과 |
contents.text.body | 공용 | 필드는 두되 GitHub README 는 당분간 채우지 않는다 — 별도 호출 1회 대비 가치가 미측정 |
accounts.account_type | 개별 | 조직 계정 여부가 신호인지 검정 기록이 없다 |
accounts.source_ref{} | 개별 | 값의 신뢰도 정보이지 쿼리 축이 아니다 |
metric_series.object | 공용 · 다형 | B-3 이 계정 시계열을 요구한다. 대상당 1행 구조라 추가 비용이 없다 |
content_raw.platform | 제거 | 키 단건 조회만 하는 컬렉션이라 필터 필드가 불필요. upsert 도 유지 — append 는 수십 KB × 관측 횟수 |
normalize() 가 실제로 정규화하지 않는다.
social-fetcher.router.ts:110 의 주석은 "소문자 host · www 제거 · trailing 정리" 인데,
host 변수만 처리하고 반환값 url 은 u.toString() 원본입니다.
v4 는 source_urls.url 에 unique 인덱스를 걸고 이를 "기존 행 찾기 1차 수단" 으로 쓰므로,
www.foo.com/promo 와 foo.com/promo 가 다른 문자열이면 같은 대상이 두 행이 됩니다.
함수 하나 수정이지만 v4 의 중복 방지 전제가 여기 걸려 있습니다.
Reddit 의 /r/{sub}/search?q=X 가 아직 subreddit(venue) 으로 분류됩니다
(reddit.fetcher.ts:66 — /r/<sub>[/new|/search 등] 이 venue 로 떨어짐).
v4 에서 subtype: search 가 contents 에 생겼으므로, 이제 검색어를 키로 하는
contents(search) 로 가야 맞습니다. 지금은 ?q=wendys 와
?q=wendys+dumpster 가 같은 venue 행으로 접힙니다.
| 위치 | 시점 | 내용 |
|---|---|---|
router.ts normalize() | 필수 | 반환값 url 에 www 제거·추적 파라미터 정리·trailing 정리가 반영되지 않는다 |
reddit.fetcher.ts classify() | 필수 | /r/{sub}/search?q= 를 venue 가 아니라 search 로 분류해야 한다 |
fetcher mentions[] | 필수 | {id, screenName} 쌍으로 채워야 한다. screenName 만 담으면 같은 KOL 이 인용 경로와 멘션 경로에서 다른 키로 잡힌다 |
METRIC_SPEC | 필수 | 쓰기 시점 키 화이트리스트. Mixed 는 검증하지 않아 오타 키(playCont)가 조용히 저장된다 |
router.ts apexDomain() | 웹 작업 | 마지막 두 라벨만 봐서 shop.foo.co.uk → co.uk. Public Suffix List 필요 |
reddit.fetcher.ts catch-all | 별건 | 코드 주석이 스스로 경고한다 — reddit.com/settings 가 가짜 subreddit 키가 되고 Reddit 은 단가가 가장 높은데다 이 키는 subreddit 이라 그 1건이 $0.04(글 URL 은 $0.022) 다. 예약어 목록을 둘지는 관측 집계가 없어 미정 |
audit 샘플에서 발견했던 classifier 오접기 2건은 현재 코드에서 이미 수정되어 있습니다. 샘플이 옛 구현 시점의 데이터라 샘플만 보면 여전히 버그로 보입니다.
instagram /stories/{handle}/{storyId} sourceKey = segments[2] storyId 로 수정됨
reddit /r/{sub}/s/{shareId} sourceType = reddit_share share 로 분리됨
교훈 — samples/ 는 관측 시점 스냅샷이지 현재 동작이 아닙니다. 코드 판정은 코드로 확인해야 합니다.
tokens.unclassified_links[] 로만 남은 것들은 원본 소셜 필드가 다른 레포에 있어 재수집 없이는 복원되지 않는다이 절이 답하는 것 — 어느 판단이 어느 문서에 근거를 두고 있는가.
urls 미채택 · source_field 제거 · 배열 아닌 별도 컬렉션), 쿼리 적합성 15경로.
이 문서가 다루지 않는 것 — 모델·리포지토리 클래스 설계(기존 be-system-design.md §3 이 유효),
마이그레이션(실데이터가 없어 해당 없음), 파생 계층(metric_ranks·엔티티 해석·지문 클러스터).