# 문서 구체화 작업흐름 (source 확정 → 필드 일반화)

> 7개 소셜 audit 문서(`sources/*.html`)는 자동생성 초안 상태. 이제 사람 검토를 거쳐 "구체화"한다.
> 이 파일 = 그 구체화 단계의 방향 기록. 세션이 끊겨도 여기서 이어간다.

## 합의된 핵심 원칙 (왜 이 순서인가)

일반화엔 두 층이 있고 의존 방향이 반대다:

```
객체 모델(A, source무관)  ──잣대──▶  source 확정  ──확정 스키마──▶  필드 일반화(B, source종속)
   [이미 완료]                       [지금 할 것]                   [source 확정 후]
```

- **(A) 객체 모델 = source 무관, 이미 완료.** 플랫폼 자체 성질이라 어떤 API로 읽든 안 변함. source 후보를 심사하는 잣대로만 쓴다. 새로 하지 않는다.
- **(B) 필드/스키마 = source 종속.** twitterapi.io ≠ Apify ≠ 공식 API → 필드명·커버리지·mutable 목록이 통째로 다름. **그래서 source 확정 전에 필드를 일반화하면 헛수고.**
- 현재 문제: §4 필드들은 이미 특정 source(프로덕션 fetcher/스냅샷) 산출인데 **출처가 문서에 라벨링 안 됨** → "어떤 source 쓰는지 알 수 없음". source 확정 = 새 조사가 아니라 **이미 딸린 출처를 확정·라벨링하고 "이걸로 간다" 사인**.

## (A) 확정된 객체 모델 — 3객체 + 비객체 1버킷

| 객체 | 정의 | 소속 sourceType |
|---|---|---|
| **Creator** | 팔로워를 가진 정체성(사람/조직) | x_profile · tiktok_profile · instagram(profile) · youtube(channel) · reddit_user · github(user/org) |
| **Content** | 한 개의 창작 산출물 | x_tweet · tiktok_video · instagram(post/reel/story) · youtube(video) · reddit_post/reddit_share |
| **Venue** | 여러 명이 올리는 "장소" | x_community · reddit(subreddit) · tg_channel · tg_guard_group |
| (비객체) 네비게이션 | 엔티티 아님, 검색/탐색 경로 | tiktok_search · instagram(explore/search) · youtube(playlist=경계) · unknown |
| (별개) website | 프로젝트 자체 홈, 소셜 객체 아님 | website |

차이 3축: (A) mutable 지표가 **객체별로** 먼저 갈림(Creator=follower / Content=likes·views / Venue=member) → (B) Content 내부 구성요소가 **플랫폼별로** 다름(X quote/retweet, reddit comment tree 등) → (C) **어떤 객체 타입이 존재하냐** 자체가 플랫폼 차이(유튜브엔 Venue 없음 등).

**보완: 3객체는 NODE만 잡음 — "관계(edge)" 차원은 암묵적.** 노드 사이 관계(Creator↔Creator: tags/mentions/collab, Content↔Content: X quote·retweet, TG forward)는 별도 슬롯이 없고 Content 필드로 흡수됨. IG는 이걸로 충분하나, **일괄 단계에서 X(quote/retweet)·TG(forward)를 다룰 땐 edge를 명시**해야 카피캣/홍보네트워크 신호를 안 놓침. 또 Creator fetch가 `latestPosts[]`로 Content를 인라인 반환하는 브릿지 존재(IG 확인).

인스타 객체 확정: Post·Reel = 같은 shortcode 체계(같은 media 계열, 형식만 다름), Story = 별개 객체(숫자 pk, 24h 소멸). 라우터가 `/stories/.../id`→키를 `"stories"`로, `/user/reel/code`→키를 username으로 오추출하는 케이스 있음(§2에서 확정).

## 실행 계획

1. **인스타 파일럿** — instagram 하나에 대해 `source 확정 → 확정 source 응답으로 필드 재수집(B)` 전 과정을 먼저 돌려 흐름을 확정한다.
2. **일반화** — 파일럿에서 "source 확정 표 스키마 + 필드 재수집 방식"이 안정되면 그걸 템플릿으로 굳힌다.
3. **일괄 실행** — 나머지 6개(x·telegram·tiktok·reddit·youtube·github)를 같은 템플릿으로 쭉 돌린다.

## source 확정 표 (파일럿 산출물 형식 — 각 플랫폼당)

각 sourceType별로 아래를 채우고 사람이 사인:

| sourceType | 후보 source(공식→Apify 사다리) | 무료/유료 | adopted | 사람 컨펌 | 근거 |
|---|---|---|---|---|---|

- 원칙: 공식/무료 우선, 안 되면 Apify (agents.md 예산 가드 승계).
- reddit만 이미 사람 컨펌(Apify `harshmaur~reddit-scraper`). 나머지 6개는 자동생성 제안 상태 → 확정 필요.
- X/tiktok/instagram은 프로덕션 fetcher 존재 → "유지 vs 교체" 결정 형태.

## instagram source 확정 (2026-07-21)

볼륨 실측(backtest source-data): IG = 전체 소셜 URL의 **5.8%**(576/9,891), 일 ~80건 → **월 ~2,500건**(프로필 78% / 게시물·릴스 22%).

| 객체 | 확정 source | 결정 | 비용(월 ~2,500건) |
|---|---|---|---|
| Creator(프로필) | Apify, actor→`prodiger/instagram-scraper`(저가) 잠정, 현행 `apify~instagram-scraper`=검증 fallback | 사인 ✅ | $3.3(prodiger) vs $6.3(현행) |
| Content(게시물/릴스) | **동일 actor + post 파싱 추가**(source 교체 아님) | 사인 ✅ | 추가비용 0 |
| Story | **보류**(24h 소멸, 정식 fetcher 미투자) | 사인 ✅ | — |
| explore/search | 제외(비객체) | — | — |

- 탈락: 공식 무료(oEmbed=지표없음 / Business Discovery=익명계정 커버리지 낮음), HikerAPI 등 신규통합(저볼륨이라 절대 절감 월 $~5로 미미).
- **미결(코드 전환 전제조건)**: prodiger가 현행과 응답 스키마 동일한 drop-in인지 유료 1회 검증. 아니면 현행 actor 유지(비용차 월 $3로 무의미).
- 크로스-플랫폼 단일 벤더(EnsembleData 등)는 전체 소셜 합산 시 절감폭 큼(월 ~$80→~$20) → **일괄 단계에서 재검토** 플래그.
- 반영처: `scaffold/instagram.json`(acquisitionLadder 확정행 + userDecision 3건 + nextActions) → `sources/instagram.html` 재렌더 완료.

## 상태

- [x] **instagram 파일럿 — 객체 모델 확정**
- [x] **instagram 파일럿 — source 확정** (위 표, 문서 반영 완료)
- [ ] instagram — 확정 source(prodiger) drop-in 유료 1회 검증 후 코드 전환 여부 확정
- [x] instagram — §4를 **객체별 통합 뷰로 재편** (Creator+Content, 5열: 필드·설명·값변동성[immutable/mutable/derivable]·파싱상태[캡처/미파싱/없음]·트레이딩유용성). 캡처 6 / 미파싱 19 / 없음 2. 옛 분산표(4.z 퀀트·4.t 트레이더)는 통합표로 흡수, raw 스키마는 4-부록으로 이동. render.py에 `objectFields`+`r_object_fields` 추가(7개 재사용 템플릿)
- [x] 파일럿 리뷰 후 "source 확정 → 필드 통합" 템플릿 확정 (render.py objectFields 구조가 곧 템플릿)
- [x] **Phase A (source 리서치) Workflow 완료** — 5플랫폼(x/telegram/tiktok/youtube/github) 후보 카탈로그. 결과 `scaffold/_phaseA-raw.json`, 병합본 `scaffold/source-picker-data.json`
- [x] **인터랙티브 피커 생성** — `source-picker.html`(로컬, 라디오+추천프리셀렉트+JSON export). 빌더 `scaffold/build-picker.py`
- [x] **사람 선택 완료** — 전부 추천안 채택. `scaffold/source-selections.json`
- [x] **Phase B 완료** — x/youtube/tiktok/telegram/github objectFields 병합·재렌더(`scaffold/merge-phaseB.py`). instagram 포함 6개 통합뷰 완성. 확정행+상태 반영
- [x] **reddit** — reddit.html에 통합뷰 직접 삽입(3객체 25필드), 기존 Apify raw 스키마는 4-부록 보존. `scaffold/_reddit_objfields.py`
- [x] **7개 문서 전부 통합뷰 완성** (x/instagram/youtube/tiktok/telegram/reddit/github). 각 §4 = 객체별 5열 통합 + raw 스키마 부록 + 확정 source
- [~] (최종) 판정 확정분 코드 반영 — **5/7 완료**: youtube Data API ✅ · github REST ✅ · x 정지계정 감지 ✅ · **telegram `t.me/s/` ✅**(998199df) · website 저장결정 ✅(a23d3e10). **남음**: tiktok `apidojo` 전환(코드는 아직 `clockworks~tiktok-scraper`) · instagram `prodiger` 검증 후 전환

## §2.1 예시 URL 복사 버튼 (2026-07-22)

sourceType 적합도 표의 패턴마다 실측 URL(`samples/*.json`)을 `exampleUrl`로 채우고, 패턴 셀 안에 "🔗 예시 복사" 버튼(클립보드 복사, Clipboard API+textarea 폴백)으로 통합 — 별도 열로 넓어지는 문제 해결. 미관측 패턴은 지어내지 않고 비활성 "미관측" 버튼.
- render.py: `COPY_JS` + `.copybtn` CSS + `r_sourcetype` 패턴셀 통합 (7개 scaffold 플랫폼 공용)
- reddit.html(hand-authored)은 동일 CSS/JS 직접 삽입
- 재현: `scaffold/apply-example-urls.py` (pattern→exampleUrl 매핑, 전량 실측 매칭 x10/10·youtube15/15·tiktok14/14·telegram5/5·github5/5)

## 산출물 (이 단계 완료분)
- 문서 7개: `sources/*.html` (§4 객체별 통합뷰)
- source 확정: `scaffold/source-selections.json` (사람 선택), `scaffold/source-picker.html`(피커), `scaffold/source-picker-data.json`
- Phase A/B 원본: `scaffold/_phaseA-raw.json`, `scaffold/_phaseB-raw.json`
- 재현 스크립트: `scaffold/build-picker.py`, `scaffold/merge-phaseB.py`, `scaffold/_reddit_objfields.py`, `scaffold/render.py`(objectFields 렌더 추가)
- [ ] §1a 글로서리/§4를 3객체 뼈대로 통일 업데이트
- [ ] (최종) 판정 확정분을 라우터/fetcher 코드 + 회귀 테스트 반영

## 참고 파일
- 문서: `sources/*.html` (자동생성 초안)
- 스캐폴드: `scaffold/agents.md`(로스터·예산가드), `scaffold/context.md`(판정관 주입), `scaffold/render.py`(JSON→HTML), `scaffold/example.reddit.json`(계약 예시)
- 샘플 원본: `samples/*.json` (수정 금지)

---

# 소셜별 수기 audit 반영 — 표준 플로우 (Instagram에서 확립, 매 소셜 반복)

> `manual-audit-results.md`의 각 소셜 결정을 `sources/<social>.html`에 반영하는 표준 절차.
> **매 소셜 시작 시 이 체크리스트를 소리내어 따라가고, 넘어가기 전 아래 완료 rubric을 통과시킨다.**

## A. 반영 원칙 (불변)
- **Override-only**: audit은 이슈 제기분만 override. 침묵분 = 기존 `proposed` 판정 그대로 확정. "discard"는 명시 결정 있을 때만. 애매하면 살리는 쪽(정제 후 분류). [[feedback_audit_override_only]]
- **사람 판단도 실수 가능**: 반영 전 사용자 audit의 버그/누락/모순을 비판적으로 먼저 점검 → raise → 확인받고 반영. 무조건 따르지 않음.
- **가정 금지·실측 우선**: 조인키 id 제공 여부, 필드 존재 여부는 raw 데이터로 확인. "없다" 단정 전 응답 truncation 의심(curl 1회 저비용).

## B. 진행 체크리스트 (순서대로)
1. **구조 파악** — 대상 `scaffold/<social>.json`의 verdicts.sourceType/idFit/objectFields + 해당 fetcher/router 코드에서 조인키 후보(숫자 id vs handle) 제공 형태 확인.
2. **audit 비판 점검** — 해당 소셜 audit 항목을 (a)명시 변경 (b)침묵으로 분류. 버그/누락/모순 raise → 사용자 확인.
3. **소셜별 3대 질문 확정**:
   - 조인 키(unique & immutable & linkable) — content가 숫자 id를 추가쿼리 없이 주면 id, 아니면 handle. **실측 확인 필수.**
   - 신규 발견 필드 채택 여부(audit 작성 이후 확인분).
   - sourceType 통합 맵(침묵분=기존 proposed 유지).
4. **JSON 반영** — `meta.finalDecision`(조인키+통합맵+원칙) / `objectFields[].fields[].decision`(key/keep/remove/skip, 혼합행 분리) / `verdicts.sourceType[]·idFit[] userDecision`.
5. **재렌더** — `python3 scaffold/render.py scaffold/<social>.json sources/<social>.html`.
6. **카운트 검증** — 아래 rubric의 grep 카운트로 확인.
7. **qna 기록** — `qna/<social>.md`에 핵심 Q&A(조인키 근거 등) 남김.
8. **커밋** — audit 폴더만 스테이징, 비밀값 스캔, 브랜치 확인(main 아님).

## C. 완료 rubric (넘어가기 전 게이트 — 전부 PASS여야 다음 소셜)

각 소셜을 "완료"로 넘기기 전 아래 8항목을 모두 확인. 검증 가능한 건 grep 명령까지 명시.

| # | 항목 | 통과 기준 | 검증 방법 |
|---|---|---|---|
| 1 | 조인 키 실측 확정 | content가 숫자 id 직접 제공 여부를 raw로 확인하고 id/handle 확정 | `meta.finalDecision.joinKeys` 존재 + qna에 근거 |
| 2 | audit 비판 점검 완료 | 버그/누락/모순을 raise하고 사용자 확인받음(없으면 "없음" 명시) | 대화/qna 기록 |
| 3 | finalDecision 박스 | 렌더된 HTML에 요약 박스 1개 | `grep -c "✅ 최종 결정 (수기 audit 반영)" sources/<s>.html` = 1 |
| 4 | 필드 decision 전수 | objectFields의 **모든 필드**가 decision 보유(또는 unavailable로 "—" 정당) | `python3 -c` 로 decision null 필드 목록 확인 → 의도된 것만 |
| 5 | sourceType userDecision 전수 | verdicts.sourceType **전 항목** userDecision 채워짐 | `python3 -c "import json;d=json.load(open('scaffold/<s>.json'));print(sum(1 for x in d['verdicts']['sourceType'] if not x.get('userDecision')))"` = 0 |
| 6 | 리뷰 배지 | 판단 반영 섹션 제목에 배지 뜸(§2·§4 등) | `grep -c "👤 판단 반영" sources/<s>.html` ≥ 1 |
| 7 | JSON 유효 + 재렌더 무오류 | 파싱 성공, 렌더 성공 | `python3 -c "import json;json.load(open('scaffold/<s>.json'))"` + 재렌더 |
| 8 | 커밋 위생 | audit 폴더만, 비밀값 없음 | `git diff --cached --name-only` 전부 audit 하위 + 비밀 스캔 |
| 9 | **키 unique·immutable** | decision=key 필드가 전부 immutable(가변키 금지) + creator/content 양쪽에 조인키 존재(linkable) | `python3 -c "import json;d=json.load(open('scaffold/<s>.json'));print([f['name'] for o in d['objectFields'] for f in o['fields'] if f.get('decision')=='key' and f.get('variability')!='immutable'])"` = `[]` |
| 10 | **선정 메트릭 크로스체크** | (a)keep 메트릭 목록을 눈으로 완전성 확인 (b)skip/remove 중 이름이 메트릭류인 것 = 빠뜨린 메트릭 아닌지 재확인 | 10a: keep+mutable 목록 출력 / 10b: 아래 grep, 뜨는 항목마다 "의도된 skip" 확인 |
| 11 | **밈거래 유용정보 누락 크로스체크** | (a)skip/remove인데 traderValue=actionable → 각 제외 근거 명시됨 (b)raw 스키마엔 있는데 objectFields에 아예 없는 필드 = 진짜 누락 아닌지(라우터 메타 제외) | 11a·11b: 아래 python, 뜨는 항목마다 근거/의도 확인 |

**항목 10·11 검증 명령** (`<s>`=소셜):
```
# 10a keep 메트릭(가변 keep = 메트릭+가변속성) 목록
python3 -c "import json;d=json.load(open('scaffold/<s>.json'));[print(o['object'].split('·')[0].strip(),f['name']) for o in d['objectFields'] for f in o['fields'] if f.get('decision')=='keep' and f.get('variability')=='mutable']"
# 10b 메트릭 이름인데 skip/remove (빠뜨림 review)
python3 -c "import json,re;d=json.load(open('scaffold/<s>.json'));pat=re.compile(r'count|ratio|karma|stars|forks|member|view|like|fans|heart',re.I);[print(f['name'],f.get('decision')) for o in d['objectFields'] for f in o['fields'] if f.get('decision') in ('skip','remove') and pat.search(f['name'])]"
# 11a actionable인데 제외됨 (각 근거 확인)
python3 -c "import json;d=json.load(open('scaffold/<s>.json'));[print(f['name'],'|',f.get('traderValue')) for o in d['objectFields'] for f in o['fields'] if f.get('decision') in ('skip','remove') and str(f.get('traderValue','')).startswith('actionable')]"
# 11b raw엔 있는데 objectFields에 없는 필드 (라우터 메타 sourceType/sourceKey는 노이즈, 무시)
python3 -c "import json,re;d=json.load(open('scaffold/<s>.json'));of=set();[of.add(t.lower()) for o in d['objectFields'] for f in o['fields'] for t in re.split(r'[ /()\[\],]+',f['name']) if t];[print(sc['dataType'][:22],'::',f['name']) for sc in d.get('schemas',[]) for f in sc.get('fields',[]) if (toks:=[t for t in re.split(r'[ /()\[\],]+',f['name']) if t]) and not any(t.lower() in of for t in toks)]"
```

**게이트 실패 시**: 실패 항목만 고치고 재검증. 전부 PASS 전엔 다음 소셜로 넘어가지 않는다. 항목 10·11은 "0이어야 PASS"가 아니라 **뜨는 항목마다 근거/의도가 있는지 사람이 재확인**하는 review 게이트.

## D. 소셜별 진행 현황
- [x] **instagram** (2026-07-23) — 조인키=숫자 id, finalDecision+decision컬럼+배지, 커밋 `9ddf7b6a`
- [x] **youtube** (2026-07-23) — 조인키=channelId(video.snippet.channelId 직접), favoriteCount 죽은필드 drop, 누락4개(publishedAt×2·description·channelId) 추가, playlist=타입만보류. caveat: Data API 라이브 미검증(문서기반). 11항목 게이트 통과
- [x] **tiktok** (2026-07-29) — 조인키=authorMeta.id(handle은 30일마다 변경+옛handle 타인 재할당=충돌 위험이라 키 부적격, alias로 격하). 유료 3콜 실측으로 audit 미해결 2건 종결: ①embed/v2는 postId만으로 해석 가능(URL의 handle 완전 무시 확인) ②photo 구분자=isSlideshow(+photo의 canonical URL이 /video/로 정규화=동일 네임스페이스 실증). audit "통째 제거" 안에서 videoMeta.transcriptionLink·musicMeta.musicId·slideshowImageLinks 분리 존치, 신규필드 mentions[]·webVideoUrl 채택, name(handle) variability 오표기(immutable→mutable) 정정. shortlink는 **상류 수집 단계 canonical 정규화**로 결정(실측 6/6 HEAD 301, 무료 — 유료 게이트 때문에 fetcher 해석은 스킵분 dedup 분기 발생), tiktok_shortlink 타입은 안전망으로 격하. 11항목 게이트 통과
- [x] **x** (2026-07-29) — 조인키=숫자 id 3객체 전부(Creator id / Content tweetId+author.id / Venue community_id+creator.id). 유료 1콜(트윗 12건 일괄)로 절단됐던 덤프 재검증 → author.id 10/10 존재로 audit 조건부 지시 해제(+"profile 제거대상 id"도 key로 승격). **quote 재귀 전개(fan-out, 깊이2)** 결정 — quoted_tweet이 최상위와 스키마 100% 동일+author 완전 프로필이라 추가 fetch 0회로 audit 요구 충족. 트윗 응답 author가 33키 완전 프로필임을 발견(별도 profile 호출 상당 부분 불필요). audit override: 검색류→x_tweet_search 통합, intent/post→x_intent 의심플래그, trending→unknown 유지. audit 실수 1건(community Metrics에 트윗 필드 혼입) + rubric이 문서 누락 2건 적발(Content PK 행 부재, Venue creator 혼합행). render.py에 `structuralDecisions` 블록 신설. 11항목 게이트 통과
- [x] **reddit** (2026-07-29) — 조인키=username/parsedId+authorName/name. **Reddit username은 영구 불변+삭제해도 재사용 불가(재할당 없음)**라 TikTok·X와 정반대로 이름을 키로 써도 안전 → audit 판단 유효 확인. share 조건문은 실측상 "안된다"로 판정(`/s/`뿐 아니라 일반 subreddit·post·old.reddit **전부 403** = Reddit이 우리 IP 전면 차단, §3 about.json 403과 동일 원인) → shareId 유지, 유료 Apify 해석은 후속. 비대칭: `redd.it`은 301 정상 → reddit_post 편입. stickied/distinguished 설명 후 audit대로 remove. **rubric 11b가 문서 누락 2건 적발: `body/bodyHtml`(게시물 본문 통째 부재), `crawledAt`(as-of 앵커 — snapshotNote가 설명하면서 필드가 없었음)** → 추가. reddit만 scaffold JSON이 없어 `scaffold/_reddit_apply_audit.py`(재실행 안전)로 반영. 11항목 게이트 통과
- [x] **telegram** — audit 섹션이 없어 실측으로 근거를 만들어 진행(표본 69채널 라이브 + 수집 362건). 4객체·50필드·finalDecision 완료 + **코드 반영까지 완료**(`t.me/s/` 교체·관측유형 판정·신규필드 11·버그 3건). 근거: `system-ideations/telegram-guard-reuse.html`
- [x] **github** (2026-07-29) — 조인키=숫자 id(Creator id / Content repo id+owner.id). **audit의 "profile 제거대상: id"를 뒤집음** — audit consider 질문 2건을 실측으로 답: ①handle=owner=login 전부 동일(API 실체는 login 하나) ②변경 가능 + 옛 username 타인 선점 가능 + **리다이렉트도 새 주인의 동명 repo에 덮어써져 깨짐** → 키 부적격. repo 응답이 owner.id를 추가쿼리 없이 제공(실측). audit 지시대로 github_repo/github_owner 분리. README 조달 가능 확인(`/readme` 200, 단 별도 호출 1회). 문서 버그 2건 정정(login·owner variability immutable→mutable). **rubric 11a가 audit↔문서 충돌 적발: `archived`(문서=actionable 러그 tell vs audit=제거) → keep으로 뒤집음**(license는 remove 유지). 신규필드 fork·homepage·twitter_username·topics/language 채택. 검증 전부 무료 API(유료 0). 11항목 게이트 통과
- [ ] (최종) 공통 타입(search/empty/html_fragment) 통합 정의 + 코드 반영

---

# TODO — 전체 잔여 작업

> **2026-07-30 갱신** — 코드 반영(E-1 타입·라우터, E-2 fetcher 매핑)이 **완료**됐다.
> 6개 소셜 전부 조인키 캡처 · 테스트 164/164 · 타입 에러 0.
> 커밋: `2c7c3fc9`(라우터+X) · `0adf54dc`(youtube) · `6e5afd58`(tiktok/instagram/github)
> · `e24863af`(중첩 인용 통일) · `acf25a0e`(reddit)
>
> **B(Tier 1 구조 확정) 완료** (2026-07-30) — 다음 관문은 **C(구조 선택 4건)** 과 모델 신설.
> 검증 방법·남은 수동 작업은 `HANDOFF-manual-tasks.html` 참조.

> audit 6소셜 반영 완료 후 남은 것 전부. 출처: `system-ideations/schema-v3-fitness-review.html` §⑦ + 이 문서 §D.
> **순서가 의미 있음** — 앞 항목이 뒤 항목의 전제인 경우가 많다.

## A. audit 본체 잔여

- [x] **telegram audit 완료** — 관측 대상이 Venue 2종이 아니라 **4종**이었다(가드포털 52% · 빈껍데기 30% · 비공개 7% · 활성채널 6%). 조인키 3단(`guardChatId` → `username` 폴백 → 초대전용 조인불가), rubric #9 의 **유일한 의도된 예외**(username 가변). 커밋 `2cee1ab4`(문서) · `998199df`(코드)
- [x] **website(Infra 축) 해소** — 4번째 객체를 만들지 않고 **`tokens.web{}` 임베드 + `tokens.fingerprints[]`** 로 확정(작성자·발행시각·참여지표가 없어 Content 가 아니다). 필드 인벤토리·소거 근거는 `sources/website-image.html`, ERD 는 v3 §④ 반영 완료. ~~4번째 객체 Infra 신설~~ — ⚠️ 이 문서 §A 객체 모델이 website를 **"(별개) 프로젝트 자체 홈, 소셜 객체 아님"**으로 의도적으로 3객체 밖에 뒀다(소셜 audit 대상이 아닌 게 맞음). 문제는 **스키마 v3가 그 별개 축의 자리를 안 만든 것**. fetcher는 이미 17필드(`urlscanScore`·`brands`·`trackingIds`·`bodyHashes`·RDAP 도메인등록일)를 수집 중이고, v2 `fingerprint`(Q4 공장규모)와 §5.4 Trust Divergence의 **유일한 공급원**. → **4번째 객체 Infra 신설**(fitness-review §⑥(d))이 조치. 필드 확정엔 website fetcher 산출 조사가 선행. 샘플에 `website.json`이 없어 관측량 미상
- [ ] **공통 타입 통합** — `search`·`empty`·`html_fragment` 등 크로스플랫폼 정의를 마지막 패스로

## B. 스키마 확정 (코드 반영 전 필수) — Tier 1

- [x] **`content_raw` 분리** — `_id`(=contentKey)만, 보조 인덱스 없음 — 무손실 raw가 `contents`에 붙어 있어 목록 조회마다 수십 KB를 같이 읽음(문서 단위 I/O·캐시). 나중에 옮기면 전량 마이그레이션
- [x] **상태 SoT 확정** — 이력=links, contents.fetchStatus=최신값 캐시 — `contents.fetchStatus` vs `links.status`가 같은 사실을 두 곳에. `observedAt`이 단수라 `ok → deleted` 전이가 소실됨
- [x] **`sourceType` → objectType → 컬렉션 매핑 정의** — SOURCE_TYPE_OBJECT(상수)+CORE_MAP(어댑터) — 3객체 모델(Creator/Content/Venue)과 라우터 산출의 연결 고리가 통째로 없음. shortlink처럼 해석 전엔 종류를 모르는 타입 처리 포함
- [x] **`platform` 키 충돌 수정** — platform(구분)+ext(네임스페이스) — v3 문서에서 한 문서에 같은 키가 두 의미(소셜 구분 문자열 / L2 네임스페이스 객체)

## C. 스키마 선택 (결정만) — Tier 2

- [ ] `core.` 유지 vs 평면화 — 기대했던 이득(L2 충돌 방지)이 실제로 없음이 확인됨
- [ ] `venues` 별도 vs `accounts` 통합(`kind: user|org|venue`) — 300건 규모·필드 유사·크로스 컬렉션 참조 발생
- [ ] `CORE_MAP` 레지스트리 명시 — 지표만 `METRIC_SPEC`으로 모으고 L1 매핑은 표로만 둔 비일관
- [ ] `accounts` 마스터 갱신 규칙 — author 스냅샷은 쌓이는데 언제 마스터를 갱신하는지 미정

## D. 문서 정합 — Tier 3

- [ ] v3 §⑩에 **"폐기된 쿼리" 표** 추가 — Category B 3개 + §5.5가 v2 결정(winRate·account_metrics_ts 제거)으로 조용히 죽어 있음
- [ ] **Q10 처리 결정** — 이미지 해시 사전공모 쿼리. audit이 미디어 URL을 대부분 skip해 입력 소실. "URL만 담기" vs "명시 보류"
- [ ] 3홉 현황 기록 — v1 "1~2홉" 원칙과 실제(A-3)가 어긋남

## E. 코드 반영

### E-1. 타입·라우터
- [x] `SocialSourceType` enum 확장 (현재 14종 → audit 확정 20종+)
- [x] `classifyGithub` — `github_owner`/`github_repo` 분리, `GITHUB_RESERVED_ROOTS`(sponsors·orgs·apps…) 방어, `gist.github.com` host 추가
- [x] `classifyReddit` — `reddit_post`/`reddit_user`/`reddit_share` 분리, `redd.it` 편입
- [x] `classifyX` — `x_tweet_search`(search+hashtag 통합)·`x_intent`(의심 플래그) 신설, `/i/user/{numericId}` 편입
- [x] `classifyTiktok` — `embed/v2/{postId}` 편입, `vt.tiktok.com` host whitelist 추가
- [x] `classifyYoutube` — playlist 리터럴 키 버그 교정(`playlistId` 추출)
- [x] instagram — `/{handle}/reel/{shortcode}` shortcode 추출 수정, 예약경로(stories·explore·accounts) 배제

### E-2. fetcher 매핑 확장 — **실제 작업량의 대부분**
- [x] **x** — SDK 타입은 이미 완비(`Tweet.id`·`author.id`·`quoteCount`·`bookmarkCount`·`conversationId`·`inReplyTo*`·`entities`·`communityInfo`·`quotedTweet` 전부 존재). **`tweetData()`/`nestedTweetData()`가 버리는 게 문제** — 특히 `nestedTweetData`가 `t.id`·`t.author.id`를 안 담아 **quote fan-out이 성립 불가**
- [x] **x** — 정지 계정 `unavailable` 쉐입 미감지 버그(타입엔 있음). `createdAt.toISOString()`에서 TypeError 위험
- [x] **tiktok** — `authorMeta.id`(조인키)·`createTime`·`signature`·`bioLink`·`isSlideshow`·`mentions[]`·`webVideoUrl`·`videoMeta.transcriptionLink`·`musicMeta.musicId`·`collectCount`·`repostCount`
- [x] **instagram** — `id`/`ownerId`(조인키)·`caption`·`alt`·`hashtags[]`/`mentions[]`·`type`/`productType`·`externalUrls[]`
- [x] **github** — `id`/`owner.id`(조인키)·`fork`·`homepage`·`twitter_username`·`topics[]`·`language`·`archived`, README는 별도 호출(`/repos/{o}/{r}/readme`)
- [x] **youtube** — oEmbed → **Data API v3 전환**(API 키 발급 필요). 현재 `channelId`를 못 받아 조인키 확보 불가
- [x] **reddit** — about.json 100% 403 → **Apify actor 전환**. 현재 실캡처 0

### E-3. 신규 계층
- [ ] 상류 **canonical 정규화** — TikTok shortlink(`vm`/`vt`/`/t/`)를 저장 전 HEAD 301로 펼침(무료, 실측 6/6). 유료 게이트 때문에 fetcher 단 해석은 스킵분에서 dedup이 깨짐
- [ ] `CORE_MAP` / `METRIC_SPEC` 레지스트리 — 소셜 차이를 아는 파일을 한 곳으로
- [ ] 모델 신설 — `contents` / `accounts`(+venues?) / `content_token_links` / `ContentMetricSnapshot` / `content_raw`
- [x] 회귀 테스트 — 라우터 분류 케이스(audit의 sourceType 표가 곧 테스트 벡터)

## F. 선행 조사 필요 (지금 결정 불가)

- [ ] **미정 3건** — 지표 관측 주기 / 백분위 코호트 정의 / `entities[]` 라벨 생성 방법. 조건부 쿼리 5개(C-1·D-1·D-3·5.1-a·Q9)가 전부 여기 걸림. **실험이 필요하지 결정으로 못 품**
- [ ] **실데이터 성능 측정** — 문서 크기 분포 + 주요 쿼리 explain. "최적" 주장의 전제이며 코드 반영 이후에만 가능
- [ ] **인덱스 설계** — 구조를 안 바꾸는 순수 추가라 보류. 단 `content_token_links`의 `(tokenAddress, linkedAt)` 진입 인덱스는 핫 경로(A-1·Q6·Q7)라 코드 반영 시 필수
