- 읽은 자료 1건
- 정본 반영 1개 파일
- 레이더 0건
1Phoenix 랭커의 backbone·재순위 헤드 두 층 구조가 처음 코드로 드러났다 — X
자료 xai-org/x-algorithm@85ac72a — 공개 소스코드, 2026-09-02 19:46 UTC, X.AI Corp. 이 노트들은 4세대 공개 뒤의 X 커밋에 차례로 번호를 붙여 부르며, 이 커밋은 4세대+14다.
배경 X의 랭킹 모델 Phoenix는 4세대 공개(2026-08-13)에서 문서당 24개 참여 확률을 뽑는 트랜스포머라는 사실이 알려졌다. 4세대+11(2026-08-28, bc8e5f0)에서는 응답 스코어에 rerankerHeadTag(재순위 헤드 태그) 필드가 붙으면서 "여러 재순위 헤드가 존재한다"는 정황이 처음 드러났다. 다중-태스크 랭커에서 backbone은 공용 인코더/타워를 가리키는 표준 용어이고, 그 출력이 여러 태스크 헤드(재순위·다양성·slate 등)로 갈라진다. 다만 proto 응답에 backbone 층 출력이 노출되지 않아, Phoenix의 헤드들이 올라가 있는 아래 층이 무엇을 출력하는지는 canon(이 저장소의 정본 노트)에 없었다. 이번 커밋(4세대+14)은 그 backbone 출력 필드를 응답에 노출하고, home-mixer(X의 피드 응답을 조립하는 서버)가 5개 피드 전체에서 이를 강제로 요청하게 했다.
새로 알게 된 것A. home-mixer가 5개 피드 요청 모두에서 backbone 점수를 함께 요구한다
home-mixer가 5개 피드 엔드포인트 모두에서 backbone 점수 요청 플래그를 하드코딩으로 켜기 시작했다 확인 —
home-mixer/server.rs.- 켜는 줄은
query.return_backbone_scores = true다. - 다섯 gRPC 엔드포인트가 모두 요청을 Phoenix로 넘기기 직전에 이 플래그를 켠다.
- ScoredPostsService(내부용 스코어드-포스트)
- ForYouFeedService(For You 메인)
- ForYouFeed의 폴링 변형
- RankedFollowingFeedService
- FollowingFeedService
- 파라미터 게이팅이 없다. 즉 프로덕션 기본값이 on이다.
let RequestContext { b3_info, mut query, root_span, } = ctx; query.return_backbone_scores = true; let output = self.get_for_you_feed(query).instrument(root_span).await?;—
home-mixer/server.rs, 2026-09-02- 켜는 줄은
backbone 점수를 요청할지 정하는 bool 필드가 요청 구조체에 새로 생겼다 확인 —
home-mixer/models/query.rs.- 구조체는
ScoredPostsQuery이고, 필드는return_backbone_scores: bool이다. - 기본 생성자(
ScoredPostsQuery::new)에서는false로 초기화되고, 서버 진입점에서만true로 세팅된다. - 즉 내부 pipeline이나 테스트 경로에서는 기본 off이고, gRPC 서버로 들어온 5개 피드 요청만 backbone scores를 요구한다.
- 구조체는
Phoenix 요청 빌더는 이 플래그를 그대로 실어 넘긴다 확인 —
home-mixer/util/phoenix_request.rs.return_backbone_scores: query.return_backbone_scores한 줄로 request context의 값이 proto 필드로 넘어간다.
새로 알게 된 것B. Phoenix 응답에 backbone 출력이 따로 실리고, 후보는 두 층의 점수를 함께 들고 다닌다
Phoenix proto가 요청과 응답 양쪽에 backbone 필드를 새로 실었다 확인 —
phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.- 요청 필드 하나와 응답 필드 두 개가 짝을 이룬다.
- 요청:
returnBackboneScores(#20) - 응답:
backboneTopLogProbs(#10,repeated float) - 응답:
backboneContinuousValues(#11,repeated float)
- 요청:
Top은 discrete-head의 상위 로그확률(예: action 어휘의 top-K 로짓)을,Continuous는 dense 회귀 출력을 가리키는 것으로 읽힌다 추정.- 근거는 이름 규약이고,
NextActionDistribution에 이미 있는topLogProbs(reranker head가 출력하는 discrete 값)와 대응한다는 점이다 추정. - reranker head가 이 backbone 위에 올라가 미세조정된 헤드라는 구조는 canon 이전 판이 관찰한
rerankerHeadTag필드와 정합적이다 추정.
message PredictNextActionsRequest { ... PageDecodeParams pageDecode = 19; bool returnBackboneScores = 20; } message NextActionDistribution { ... SlateContext slateContext = 9; repeated float backboneTopLogProbs = 10; repeated float backboneContinuousValues = 11; }- 요청 필드 하나와 응답 필드 두 개가 짝을 이룬다.
후보 객체에 backbone 점수 필드가 새로 생겼고, scorer가 응답을 두 층으로 파싱해 저장한다 확인 —
home-mixer/models/candidate.rs,home-mixer/scorers/phoenix_scorer.rs.- 새 필드는 PostCandidate의
backbone_scores: Option<PhoenixScores>다. - Scorer는 같은 예측 응답을 두 번 파싱한다.
candidate_scores는 reranker head 출력(기존phoenix_scores)이고,candidate_backbone_scores는 이번에 새로 노출된 backbone 출력이다. - 즉 candidate는 한 예측 라운드에서 두 층의 스코어를 함께 들고 다음 단계로 나간다.
// candidate.rs pub struct PostCandidate { ... pub reranker_head_tag: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub backbone_scores: Option<PhoenixScores>, ... } // phoenix_scorer.rs .map(|c| PostCandidate { phoenix_scores: predictions.candidate_scores(&c.get_original_tweet_id()), backbone_scores: predictions.candidate_backbone_scores(&c.get_original_tweet_id()), ... })- 새 필드는 PostCandidate의
새로 알게 된 것C. backbone 점수를 어디에 쓰는지는 이번 커밋에서 아직 보이지 않는다
- scorer가 저장까지만 하고, downstream 소비처는 이번 diff 밖에 있다 추정.
- 다만 backbone 출력을 candidate에 계속 들고 다닌다는 사실은 다음 가운데 하나 이상을 뜻한다 추정. 근거는
backbone_scores가PostCandidate의 필드로 들어가 있고, 스캐폴딩상 값을 버리는(dropped) 경로가 없다는 점이다.- 재순위·다양성 단계가 backbone 로짓을 부가 특성으로 소비한다.
- 광고/피드 mixer가 backbone 출력을 debug/attribution용 payload로 기록한다.
- 다른 스코어러가 backbone 출력을 입력으로 쓴다. 예를 들면 VMRanker의 slate 재정렬이나 광고 블렌더 MultiRiskAdsBlender다.
- 오프라인 분석·A/B 판정에서 "reranker head가 아닌 backbone이 얼마나 움직였는가"를 볼 수 있게 하는 관측용 채널이다.
- 새 소비처는 다음 커밋들에서 보일 가능성이 높다 추정.
새로 알게 된 것D. Phoenix retrieval의 유저 히스토리 집계 기본값이 긴 dwell 쪽으로 바뀌었다
Phoenix retrieval이 유저 히스토리를 집계하는 기본값이 짧은 dwell(체류 시간)에서 긴 dwell로 바뀌었다 확인 —
home-mixer/params/param.rs.- 파일 상단 헤더의 sync 시각이
2026-09-01T16:42:25Z→2026-09-02T16:03:45Z로 갱신됐으므로, 이 값은 프로덕션 feature-switch가 하루 사이에 실제로 바뀌었고 소스 미러가 그것을 따라잡은 것이다. - 파일 상단 주석에는 이렇게 적혀 있다: "mirrored from config feature-switch defaults; last sync ...".
- long-dwell 집계는 유저가 오래 머문 후보들만 히스토리 임베딩에 합류시키는 방식으로, retrieval의 시그널을 좀 더 "고품질 참여 쪽으로 편향"시키는 방향이다 추정. 근거는 X가 4세대+6(2026-08-21) 이후 광고 conversion 헤드를 대폭 확장하면서 dwell-based positive signal에 무게를 옮겨 온 흐름과 정합적이라는 점이다.
param!( PhoenixRetrievalAggregationType, String, "rust_home_mixer_phoenix_retrieval_aggregation_type", "DENSE_WITH_LONG_DWELL" // was "DENSE_WITH_SHORT_DWELL" );- 파일 상단 헤더의 sync 시각이
새로 알게 된 것그 밖의 변경
Phoenix 서빙의 배치 준비 코드가 버퍼를 채우기 전에 zero로 초기화하는 방어를 두 곳에 넣었다 확인 —
phoenix/crates/serving/xai-recsys-engine/src/python.rs.- 초기화하는 곳은 candidate embedding 슬라이스와 search-query embedding 슬라이스다.
- 이전에는 재사용 버퍼에 이전 배치의 값이 남아 있다가,
copy_len < embedding_row.len()인 경우(채운 길이가 슬롯 크기보다 작을 때) 잔여 바이트가 유출됐다. copy_len > 0조건도 함께 걸어, 복사할 길이가 0이면 복사를 건너뛴다. 커밋은 이유를 적지 않았다.- 양쪽을 같은
copy_len으로 자르므로 길이 0 복사도 panic하지 않는다 확인 — Rustslice::copy_from_slice문서. 이 함수는 두 슬라이스 길이가 다를 때만 panic한다.
- 양쪽을 같은
- 4세대+11(2026-08-28, bc8e5f0)에서 emb_table gRPC 수신 파이프라인을 정리한 흐름의 후속으로, 배치 재사용 경계에서 생기던 잠재적 correctness 이슈를 막은 것이다.
.par_chunks(chunk_size) .zip(candidate_embeddings_slices.par_iter_mut()) .for_each(|(items_chunk, shard_slice)| { shard_slice.fill(f16::ZERO); shard_slice .par_chunks_exact_mut(row_size) ... }); ... if search_query_embedding_dim > 0 && let Some(sq_slice) = ... { sq_slice.fill(0.0); sq_slice .par_chunks_exact_mut(candidate_seq_len * search_query_embedding_dim) ... }
정본 반영 services/x.md의 "안전성 / visibility-filtering / labeling 파이프라인" 절에 "4세대+14 (2026-09-02, 85ac72a) — home-mixer가 Phoenix backbone scores 요청 + scarecrow XReview 인테이크 신설 + VF rules Box<dyn> 소거 + 체크포인트 in-envelope 파생 마스터 키" 소절을 새로 만들었다. "한 줄 요약"과 세대 대비 헤더에 4세대+14 행을 추가했다. "Phoenix 학습·인덱스" 절에는 backbone scores 응답 필드와 xrecsys_two_tower 하이퍼파라미터 변경을 반영했다. "## 출처"의 커밋 sha 목록에 85ac72a(2026-09-02, 4세대+14)를 추가하고, 관련 파일 인용 8건을 새로 올렸다. 최종 갱신은 2026-09-03 → 2026-09-04로 올렸고, git 로그 직접 확인 줄도 2026-09-03 → 2026-09-04, 커밋 13개 → 14개로 고쳤다.
의미 Phoenix 랭커의 층 구조가 코드로 확정됐다.
- 4세대 공개 이후 canon은 "Phoenix가 후보당 24개 확률을 뱉는 트랜스포머"라는 최상위 시맨틱만 알았다. 4세대+11의
rerankerHeadTag필드로 "여러 재순위 헤드가 존재"까지 관찰했고, 이번 4세대+14로 "그 헤드들 아래에 backbone 층이 있고 그 출력이 별도 응답 필드"라는 것을 확정한다. 다중-태스크 랭커의 표준 형태(shared bottom과 task-specific heads)가 X 프로덕션에도 그대로 있다는 뜻이다. return_backbone_scores는 파라미터 게이팅 없이 5개 피드에서 강제로 켜진다. 이는 이 응답 추가가 A/B 실험 단계가 아니라 이미 프로덕션 기본값으로 서빙되고 있다는 강한 신호다. Phoenix 서빙 측이 backbone 출력을 계산해 응답에 실을 준비가 되어 있고, 지연 예산에도 여유가 있다는 뜻이다.- "backbone score를 candidate에 들고 다닌다"는 것은 다음 층에서 소비할 수 있게 해 둔 예약이다. 이 커밋에서는 저장까지만 보이지만, MultiRiskAdsBlender, VMRanker, DPP re-rank(후보 다양성을 반영한 재정렬) 같은 downstream이 backbone 로짓을 부가 특성으로 쓰는 코드가 곧 붙을 가능성이 높다. 이는 4세대+11의
rerankerHeadTag와 짝을 이뤄, "hydra 헤드 사이의 스위칭·앙상블·attribution" 코드가 후속 커밋에서 열릴 관전 포인트를 만든다. - Phoenix retrieval의 dwell 기본값이 long으로 옮겨 간 사실은 4세대+6 이후 광고 conversion 헤드 확대 흐름과 정합적이다. 두 층 랭커에서 backbone이 학습하는 신호와 retrieval이 참조하는 유저 히스토리 집계가 같은 방향(long-dwell-heavy)으로 정렬되는 셈이다.
2scarecrow(구 Botmaker)가 XReview에 자동 신고를 넣는 경로가 생겼다 — X
자료 xai-org/x-algorithm@85ac72a — 공개 소스코드, 2026-09-02, X.AI Corp. 관련 파일은 3개 신규, 1개 수정이고 총 333줄이다. 이 노트들이 붙인 번호로는 4세대+14, 내용으로는 canon에 처음 들어오는 항목이다.
배경 scarecrow(구 Botmaker)는 X 내부의 "규칙 기반 자동 탐지·조치" 시스템이다. 옛 Twitter의 Botmaker(트래픽에 룰을 매기고 조치를 취하는 엔진)를 X.AI가 인수 후에도 유지·확장하고 있는 것이 scarecrow이고, 이번 커밋의 Scala 파일에도 com.twitter.botmaker 패키지 유산이 그대로 남아 있다. XReview는 신고·모더레이션 큐의 이름(2025~2026 X 내부 명칭)이고, 유저가 UI에서 "신고"를 누르면 리포트가 XReview로 흘러가 리뷰 대상이 된다. 이번 커밋 전까지 canon에는 scarecrow와 XReview 사이의 프로그램적 연결이 없었다. 이번 커밋은 scarecrow의 자동 탐지 봇이 XReview에 리포트를 "유저가 신고한 것처럼" 프로그램적으로 넣는 gRPC 파이프라인을 새로 연다.
새로 알게 된 것A. scarecrow 룰이 호출하는 함수 두 개로 XReview에 신고를 넣는다
scarecrow에서 XReview로 리포트를 보내는 함수 유닛 두 개가 새로 생겼다 확인 —
scarecrow/legacy/XReviewIntakeRegistry.scala(179줄 신규).- 두 함수는
CreateXReviewReportProd와CreateXReviewReportStaging이다. scarecrow의 룰 DSL에서 호출할 수 있고, 각각 프로덕션과 스테이징 XReview로 리포트를 보낸다. - scarecrow 룰이 이 함수를 호출할 때 넘기는 인자는 7개다.
entityType: "post" 또는 "profile"entityId: 트윗 id 또는 유저 iduserId: 신고 대상 유저reportType: XReview allowlist 값(예:bystander_impersonation)botId: 이 신고를 만든 자동 탐지 봇- 선택적
note - 선택적
victimId: 피해자 유저 id
- CreateXReviewReportProd 클래스에 붙은 자체 설명은 아래 인용과 같다.
"Submits a report to production XReview Intake over gRPC. entityType must be post or profile. reportType must be an XReview-allowlisted value. Impersonation bots should use bystander_impersonation (lane tags key on that type, not generic impersonation). Each evaluation includes detection_timestamp_ms so intake's content-hash report id is unique; evidence rollup then applies." —
XReviewIntakeRegistry.scala, CreateXReviewReportProd 클래스의description- 두 함수는
다운스트림 서비스 목록에 XReview 인테이크가 등록됐다 확인 —
scarecrow/legacy/DownstreamServices.scala.- 등록된 이름은
XReviewIntake다.
case object XReviewIntake extends DownstreamService { override val name = "XREVIEW_INTAKE" }- 등록된 이름은
스테이징용 클라이언트가 따로 있고, 스테이징 함수는 실제 조치가 없는 실험용으로 표시된다 확인 —
XReviewIntakeRegistry.scala.CreateXReviewReportStaging은 action level을NO_ACTION으로 설정한다.- scarecrow의 action level은 이 함수가 실제 조치인지 dry-run인지를 표시하는 메타데이터다. 스테이징 버전은
NO_ACTION으로 표시돼, 룰이 실전에 반영되지 않는 실험용임을 명시한다. - 프로덕션 버전은
PROD_AGENT_WORKFLOW_ACTION이다. "agent workflow"라는 표현은 자동 조치 파이프라인의 일부라는 뜻이다.
개발자 로컬 환경이나 개인 계정 인스턴스에서는 실제 XReview 서버를 호출하지 않는다 확인 —
XReviewIntakeRegistry.scala.localMode나personalAccountMode에서는 noop으로 대체된다.
if (localMode() || personalAccountMode()) { XReviewReportSubmitter.noop(statsReceiver) } else { ... }scarecrow는 mTLS로 XReview intake에 붙는다 확인.
- flag
xreview.tls.ca-cert가 CA 인증서 경로를 받고,SharedXReviewIntakeClientModule.provideXReviewIntakeClient가 이를 소비해 mTLS 연결을 만든다. - 클라이언트는
com.twitter.useng.common.xreview패키지에 있는 공용 모듈에서 온다. useng는 X 사내 공용 라이브러리 네임스페이스다. - 즉 scarecrow만이 아니라 다른 시스템도 XReview intake에 프로그램적으로 접근한다는 정황이다.
- flag
새로 알게 된 것B. 봇 신고는 유저 신고와 같은 표면으로 들어가되, 자동 탐지라는 출처가 붙는다
리포트 페이로드는 XReview의 SubmitReportRequest gRPC proto를 그대로 채운다 확인 —
scarecrow/legacy/XReviewReportBuilder.scala(59줄 신규).- 키 몇 개가 이 파이프라인의 성격을 그대로 보여 준다.
report_surface = "x_app": 유저 신고와 같은 표면에서 들어온다.report_source = "proactive": 유저 신고("user")가 아니라 자동 탐지("proactive")임을 XReview에게 알린다. 즉 XReview는 어떤 리포트를 사람이 눌러서 만들었고 어떤 리포트를 봇이 만들어 넣었는지를 이 필드로 구분한다.subject = "Botmaker report": 리포트의 최상단 문구가 고정된다.reporter_id = "0": 봇 리포트에는 신고자 유저 id가 없다. 대신detection_bot_id에 어떤 봇이 만든 리포트인지가 들어간다.
val ReportSurfaceValue = "x_app" val ReportSourceValue = "proactive" val Subject = "Botmaker report" val ReporterIdValue = "0" ... val reportFields = Seq( pb(XReviewReportKeys.Subject, Subject), pb(XReviewReportKeys.ReportSurface, ReportSurfaceValue), pb(XReviewReportKeys.ReportType, reportType), pb(XReviewReportKeys.ReportSource, ReportSourceValue), pb(XReviewReportKeys.ReportedEntityType, entityType), pb(XReviewReportKeys.ReportedEntityId, entityId.toString), pb(XReviewReportKeys.ReportedUserId, userId.toString), pb(XReviewReportKeys.DetectionBotId, botId.toString), pb(XReviewReportKeys.DetectionTimestampMs, detectionTimestampMs.toString) ) ++ note.filter(_.nonEmpty).map(n => pb(DescriptionKey, n)).toSeq ++ victimId.filter(_ != null).map(id => pb(XReviewReportKeys.VictimUserId, id.toString)).toSeq- 키 몇 개가 이 파이프라인의 성격을 그대로 보여 준다.
entityType 화이트리스트는 "post"와 "profile"로 제한된다 확인.
normalizeEntityType함수가 잘못된 값이면IllegalArgumentException을 던져, scarecrow 룰이 임의의 entity type을 XReview에 넣지 못하게 막는다.
새로 알게 된 것C. 중복 신고는 XReview가 사후에 묶고, 신고 유형이 리뷰 lane을 정한다
- report id의 고유성은 내용 해시와 timestamp가 함께 정한다 확인.
- 위 인용문에는 이렇게 적혀 있다: "intake's content-hash report id is unique; evidence rollup then applies". XReview 서버가 내용 해시와
detection_timestamp_ms로 report id를 만든다는 뜻이다. - 같은 봇이 같은 콘텐츠를 두 번 감지해도 timestamp가 다르면 별개의 report가 되고, XReview가 사후에 evidence를 rollup(집계)한다.
- 즉 봇 리포트에서는 자연스럽게 중복 감지가 여러 번 생기고, 그 중복을 XReview가 처리하는 구조다.
- 위 인용문에는 이렇게 적혀 있다: "intake's content-hash report id is unique; evidence rollup then applies". XReview 서버가 내용 해시와
- 사칭(impersonation) 탐지 봇은
bystander_impersonation을 써야 한다 확인.- 자체 설명은 이렇게 적었다: "Impersonation bots should use bystander_impersonation (lane tags key on that type, not generic impersonation)."
- 여기서 lane tag는 XReview 큐 라우팅(어떤 인간 리뷰어 팀·SLA로 갈지)을 뜻하는 것으로 읽힌다.
- 즉 XReview에는 리포트를 리뷰어 팀으로 라우팅하는 "lane" 개념이 있고,
report_type값이 lane을 결정한다. - 봇이 잘못된
report_type을 쓰면 잘못된 lane으로 라우팅돼 검토가 지연되거나 무시된다.
새로 알게 된 것그 밖의 변경
- 관측(metrics) 카운터는 5종이다 확인 —
XReviewReportSubmitter.scala(91줄 신규).attempted: 호출 수submitted: 성공skipped: prodClient/stagingClient가 없어서 건너뜀unsupported_entity: entityType 화이트리스트 밖rpc_failed: gRPC 실패- 실패는
warn로그로 남기고 예외를 다시 던진다.
정본 반영 services/x.md의 "안전성 / visibility-filtering / labeling 파이프라인" 절 아래에 4세대+14 소절을 새로 만들고, 위 1절의 내용과 함께 묶었다. scarecrow → XReview 인테이크 파이프라인은 canon에 처음 들어오는 항목이라 별도 소절 "scarecrow → XReview 자동 신고 파이프라인 (2026-09-02 신규 공개)"에 두었다. 파일 3개(XReviewIntakeRegistry·XReviewReportBuilder·XReviewReportSubmitter)와 DownstreamServices 수정본을 "## 출처"에 올렸다.
의미 X의 모더레이션 파이프라인이 이번 커밋으로 확장됐다.
- canon 이전 판이 알던 안전 레이어는 두 층이었다.
- visibility-filtering은 스코어링·서빙 경로에서 inline으로 후보를 drop하거나 interstitial(경고 화면을 덮어씌우는 조치)을 붙이는 층이다.
- grox PtOS는 Grok LLM이 트윗을 요약하고 라벨을 붙이는 콘텐츠 라벨링 층이다.
- 이번 커밋(4세대+14)은 세 번째 층인 scarecrow → XReview를 코드로 노출한다. 자동 탐지 봇이 유저 리포트인 척 인테이크를 통해 XReview 큐로 리포트를 밀어 넣고, 이후 XReview가 evidence rollup과 인간 리뷰어 라우팅(lane tag)을 맡는다. 이것은 inline drop이 아닌 비동기 신고·집계 파이프라인이다.
report_source = "proactive"필드가 있다는 것은, XReview에 유저 리포트("user")와 봇 리포트("proactive")가 같은 큐로 들어가되 소스가 태깅된다는 것을 시사한다. 이는 XReview 대시보드나 리뷰어 UI가 "유저 압박이 있는 사례"와 "봇이 감지한 사례"를 구분해 표시하고, 아마도 evidence rollup 로직도 두 소스에 가중치를 다르게 준다는 것을 뜻한다 추정. 이 부분은 XReview 서버 측 로직이 공개되어야 확정된다.bystander_impersonation타입이 있다는 것은 사칭 대응 라인이 세분화되어 있다는 뜻이다. 근거 문구는 이렇다: "bystander_impersonation (lane tags key on that type, not generic impersonation)". 봇이 발견한 사칭 케이스는 "제3자가 신고한 사칭"으로 라벨링해 별도 lane으로 라우팅한다. "generic impersonation"을 쓰지 말라고 명시한 이유는 lane tag 라우팅이 report_type 자체를 discriminator로 쓰기 때문이다. 잘못 태그하면 잘못된 팀·SLA·심각도로 라우팅된다.- scarecrow가 쓴 XReview 클라이언트는
useng.common.xreview공용 모듈, 즉com.twitter.useng.common.xreview패키지에 있다. 이는 이 인테이크가 scarecrow 전용이 아니라 X 내부의 여러 백엔드가 공용으로 호출한다는 뜻이다. 예컨대 grox PtOS 파이프라인도 이 라이브러리를 통해 XReview에 리포트를 넣을 수 있다 추정. 다음 커밋들에서 이 사용처가 더 드러날 가능성이 높다 추정. - ⚠ 주장 보강: 이전 canon은 X의 자동 탐지 → 인간 리뷰 핸드오프 구조를 "grox PtOS가 라벨을 붙이면 policy service가 조치를 취한다" 정도로 이해했다. 이번 커밋으로 그 핸드오프의 한 축이 XReview 큐를 유저 신고와 같은 채널로 공유한다는 사실이 코드로 확정됐다. 즉 XReview는 유저 신고만 처리하는 큐가 아니라 자동 탐지의 종착지 중 하나이기도 하다.
3노출 판정 규칙의 정책이 힙 조립 대신 정적 테이블에 실린다 — X
자료 xai-org/x-algorithm@85ac72a — 공개 소스코드, 2026-09-02. 이 노트들이 붙인 번호로는 4세대+14다.
배경 visibility-filtering(VF)은 포스트마다 이 뷰어에게 보여도 되는지를 판정하는 서비스다. 4세대+13(2026-09-01, 7ba7768)에서 VF 규칙 표현이 struct-per-rule에서 data-driven RuleSpec enum으로 옮겨졌다. 다만 RuleEngine(당시 이름 Policies)은 여전히 Vec<Box<dyn Rule>> 세 개를 힙에 조립해서 들고 있었다. 규칙 자체는 데이터 리터럴이 됐지만, 정책(safety level별 규칙 시퀀스)은 런타임 vec으로 남아 있었던 것이다. 이번 커밋(4세대+14)은 재구조화의 phase 2로, 그 마지막 층을 걷어 냈다.
새로 알게 된 것A. 세 정책이 힙에 조립하는 벡터 대신 프로그램 이미지의 정적 테이블이 됐다
규칙 trait과 힙에 쌓던 규칙 벡터가 코드에서 사라졌다 확인 —
visibility-filtering/rules/mod.rs.- 사라진 것은
Ruletrait과Vec<Box<dyn Rule>>이다. - 함께 제거된 것은 아래 셋이고, mod.rs에서 −155줄이다.
Ruletrait 정의(name·evaluate 두 메서드)Box<dyn Rule>컬렉션을 순회하며 verdict를 만들던evaluate_rules함수- 관련 unit test 5개
- 사라진 것은
세 정책은 이제 프로그램 이미지의 rodata 섹션에 정적 테이블로 실린다 확인 —
visibility-filtering/rules/registry.rs.- 테이블 형태는
&'static [&'static [RuleSpec]]이다. - 세 정책(
FILTER_ALL_POLICY·TIMELINE_HOME_POLICY·TIMELINE_HOME_RECOMMENDATIONS_POLICY)이 모두static이다. RuleEngine::new()는 이제 힙 할당을 하지 않고,NsfwGatingCountries참조만 감싼다.
pub(super) struct Policy { rules: &'static [&'static [RuleSpec]], additional_rules: &'static [&'static [RuleSpec]], } static TIMELINE_HOME_SHARED_RULES: [&[RuleSpec]; 9] = [ author_rules::AUTHOR_STATE_DROPS, author_rules::SOCIALGRAPH_DROPS, tweet_rules::TWEET_LABEL_DROPS, tweet_rules::NULLCAST_DROP, tweet_rules::TES_HOME_DROPS, tweet_rules::SENSITIVE_VIEWER_DROPS, tweet_rules::EXCLUSIVE_TWEET_DROP, tweet_rules::NSFW_MEDIA_INTERSTITIALS, tweet_rules::NSFW_AUTHOR_INTERSTITIAL, ]; static TIMELINE_HOME_RECOMMENDATION_ONLY_RULES: [&[RuleSpec]; 5] = [ tweet_rules::RECS_MEDIA_DROPS, author_rules::OON_NSFW_AUTHOR_DROPS, tweet_rules::OON_TWEET_FLAG_DROPS, tweet_rules::OON_TWEET_LABEL_DROPS, author_rules::OON_USER_LABEL_DROPS, ]; static TIMELINE_HOME_POLICY: Policy = Policy::new(&TIMELINE_HOME_SHARED_RULES); static TIMELINE_HOME_RECOMMENDATIONS_POLICY: Policy = Policy { rules: &TIMELINE_HOME_SHARED_RULES, additional_rules: &TIMELINE_HOME_RECOMMENDATION_ONLY_RULES, };- 테이블 형태는
Policies의 이름이RuleEngine으로 바뀌었다 확인.- 이유는 이 struct가 더 이상 규칙 벡터를 소유하지 않기 때문이다. 옛 이름은 더 이상 상태(collection)를 반영하지 않는다.
- 새 이름은 dispatch 층임을 명시한다.
새로 알게 된 것B. 규칙을 평가하는 순서와 판정 방식은 그대로다
정책은 규칙을 앞에서부터 차례로 평가한다 확인 —
visibility-filtering/rules/registry.rs.- 평가 함수는
Policy::evaluate이고, rule iterator를 순서대로 돈다. - 시맨틱은 그대로다.
Drop은 즉시 short-circuit한다.Interstitial이 먼저 나와도, 이후 rule이Drop을 반환하면Drop이 이긴다.Allow가 마지막까지 유지되면decided_by = None이다.
pub(super) fn evaluate(&self, context: &RuleContext<'_>) -> Verdict { let mut action = VfAction::Allow; let mut decided_by = None; for rule in self.rules() { match rule.evaluate(context) { VfAction::Drop(reason) => { return Verdict { action: VfAction::Drop(reason), decided_by: Some(rule.name()), }; } VfAction::Interstitial(reason) if matches!(action, VfAction::Allow) => { action = VfAction::Interstitial(reason); decided_by = Some(rule.name()); } VfAction::Allow | VfAction::Interstitial(_) => {} } } Verdict { action, decided_by } }- 평가 함수는
새로 알게 된 것그 밖의 변경
- 15개 slice에 걸린 규칙 이름 전체의 유일성을 검증하는 테스트가 새로 생겼다 확인 —
visibility-filtering/rules/tweet_rules.rs.wired_rule_names_are_unique_and_nonempty는 tweet 축 슬라이스 11개와 author 축 슬라이스 4개, 모두 15개 slice 안의 규칙 이름이 non-empty이고 중복이 없음을 보장한다.- 규칙 이름이 감사·메트릭·golden corpus에서 리터럴로 참조되기 때문에 이 어설션이 중요하다.
정본 반영 services/x.md의 4세대+13 소절 뒤에 4세대+14의 VF 리팩터 부분을 짧게 이어 붙인다. 4세대+13 소절이 재구조화 phase 1을 이미 자세히 설명하므로, phase 2는 바뀐 부분만 적는다.
의미 정책이 런타임 힙에서 rodata로 옮겨졌고, 판정 시맨틱은 그대로다.
- 규칙 그래프 전체를 이제 프로그램 시작 시점에 아무 alloc 없이 dispatch할 수 있다. VF 서버는 fork 후 즉시 evaluate할 수 있고, 규칙 목록은 프로그램 이미지에서
readelf로 그대로 관찰할 수 있는 데이터로 남는다. - 홈 공통 9 slice와 추천 전용 5 slice의 구성은 4세대+13과 같고, 규칙 개수도 그대로다. 다만
RuleEngine::rule_counts()는 벡터 길이를 세는 대신 staticPolicy::len()을 부른다. Policies→RuleEngine개명으로, 4세대+11부터 4세대+14까지 네 커밋에 걸쳐 진행된 이 리팩터의 지향점이 분명해졌다.- 규칙을 코드에서 데이터로 옮긴다.
- 정책을 힙에서 rodata로 옮긴다.
- 이름을 dispatch 관점으로 정리한다.
- 다음 단계는 규칙 리터럴을 config 파일(TOML/JSON)에서 로드하는 것이고, 그렇게 되면 코드 배포 없이 규칙을 바꿀 수 있다 추정. 다만 이는 rust 정적 dispatch의 이점(inlining, jump table)을 포기하는 방향이라, 실제로 이 방향으로 갈지는 앞으로의 커밋을 봐야 안다.
4학습 체크포인트를 사이드카 키 파일 없이 파일 헤더에서 키를 꺼내 복호화할 수 있게 됐다 — X
자료 xai-org/x-algorithm@85ac72a — 공개 소스코드, 2026-09-02. 5개 파일을 고쳤다(총 +305/−88). 이 노트들이 붙인 번호로는 4세대+14다.
배경 2026-09-01 커밋(4세대+13)은 xrex 학습 코드에 envelope 방식으로 암호화하는 Orbax checkpoint writer를 새로 넣었다. KMS로 감싼 DEK(Data Encryption Key)를 _DEK 사이드카 파일에 저장하고, 그 DEK로 checkpoint 파일들을 대칭 암호화하는 표준 envelope 패턴이었다. 이번 커밋(4세대+14)은 사이드카 파일이 없어도 checkpoint 파일 자체의 envelope 헤더에서 마스터 키를 뽑아 복호화할 수 있게 한다. 또 저장 시점에 encryption_context에 필수 필드를 강제하고, mint nonce를 자동으로 붙인다. 즉 파일 포맷·키 관리·context 스키마를 모두 하나씩 정돈한다.
새로 알게 된 것A. 사이드카 키 파일이 없으면 파일 헤더에서 키를 풀어 체크포인트를 읽는다
DEK를 읽는 함수는 사이드카 파일이 있으면 그것을 쓰고, 없으면 envelope 헤더에서 키를 푼다 확인.
- 함수 이름은
adopt_tree_dek다. - 2026-09-01 커밋(4세대+13)에서 만든
_DEK사이드카 방식 checkpoint와, 앞으로 만들 in-envelope 파생 방식 checkpoint를 모두 같은 함수로 읽는다. - 마이그레이션 경로가 backward-compatible이다.
def adopt_tree_dek(path: pathlib.Path, kms_client) -> bytes: ... if dek_path.exists(): entry = _read_wrapped_dek(dek_path) or {} raw = bytes(xai_kms.nfs.unwrap_shared_dek(kms_client, dek_path)) key_id = entry.get("key_id") else: raw = _unwrap_header_master(kms_client, path) key_id = "header" rank_logger.info("KMS unwrap OK for %s (key_id=%s)", path, key_id) return raw- 함수 이름은
checkpoint 파일의 envelope 헤더는 4096바이트이고, magic
XAIENC01과 wrapped_len/context_len 필드로 구성된다 확인 —phoenix/python/training/xai-checkpointing/xai_checkpointing/dek.py.- 헤더 구조는 이렇다.
- 처음 8바이트는 magic
XAIENC01이다. - 28~32 바이트 offset에 wrapped 필드 길이(big-endian u32)가 있다.
- 32~36 바이트 offset에 context 길이(u32)가 있다.
- 이어서 4096바이트 안에 wrapped 필드와 context JSON이 들어간다.
- 처음 8바이트는 magic
- wrapped 필드가
xai-dek1:<salt>:<master>형식이면 "derived" 방식으로 취급하고, 그렇지 않으면 legacy per-file 감싸기로 판정해 예외를 던진다.
_ENVELOPE_HEADER_SIZE = 4096 _ENVELOPE_FIXED_LEN = 36 _DERIVED_PREFIX = "xai-dek1:" def _header_wrapped_master(path: pathlib.Path) -> tuple[str, dict]: envelope = _first_envelope(path) head = envelope.read_bytes()[:_ENVELOPE_HEADER_SIZE] if len(head) < _ENVELOPE_FIXED_LEN or head[:8] != b"XAIENC01": raise ValueError(f"not an envelope: {envelope}") wrapped_len = int.from_bytes(head[28:32], "big") context_len = int.from_bytes(head[32:36], "big") start = _ENVELOPE_FIXED_LEN field = head[start : start + wrapped_len].decode() context = json.loads(head[start + wrapped_len : start + wrapped_len + context_len]) if not field.startswith(_DERIVED_PREFIX): raise ValueError( f"envelope at {envelope} carries a legacy per-file wrapped key, " "not the xai-dek1 derived form" ) _salt, master = field[len(_DERIVED_PREFIX) :].split(":", 1) ...- 헤더 구조는 이렇다.
읽기 경로가 tensorstore 드라이버
xai_encrypted를 직접 쓰는 방식으로 바뀌었다 확인 —phoenix/python/training/xai-checkpointing/xai_checkpointing/load.py.- 이전(2026-09-01 커밋, 4세대+13)에는 KMS-proxy KvStore(
xai_kms.KvServe)가 매 IO마다 KMS를 호출했다. 이제는 DEK를 한 번 unwrap한 뒤xai_encrypted드라이버가 대칭 복호화를 직접 수행한다. - 이 방식은 KMS 왕복 지연을 IO 경로에서 걷어 낸다. DEK는 한 번만 unwrap하고, 이후에는 로컬에서 대칭 복호화한다.
- Prev/next 파일 포맷을 모두 지원한다.
raw = adopt_tree_dek(path, kms_client) kvstore_base = { "driver": "xai_encrypted", "base": {"driver": "file", "path": path.as_posix() + "/"}, "dek_b64": base64.b64encode(raw).decode(), } metadata_json = json.loads(_read_encrypted_file(path, "_METADATA", kvstore_base, ts_context)) return kvstore_base, metadata_json, _names_from_tree_metadata(metadata_json), ts_context- 이전(2026-09-01 커밋, 4세대+13)에는 KMS-proxy KvStore(
새로 알게 된 것B. 암호화 저장은 필수 context 키를 요구하고, 경로 해시를 nonce로 자동으로 넣는다
저장 시점에 encryption_context가 두 키를 필수로 요구한다 확인 —
phoenix/xrex/train/checkpoint_write.py.- 두 키는
domain과run이다. - 이름으로 보아
domain은 이 checkpoint가 속하는 시스템 도메인(예: "recsys", "grox")이고,run은 학습 run id다. - 이 두 값이 KMS 호출의 encryption_context로 실려, 감사 로그에 "어떤 domain의 어떤 run이 어떤 checkpoint를 언제 unwrap했는가"가 남는다.
- 즉 KMS 감사 흐름이 이 코드에서 강제된다.
missing_context = {"domain", "run"} - self.checkpoint_config.encryption_context.keys() if missing_context: raise ValueError( "encrypted checkpoint context is missing " + ", ".join(sorted(missing_context)) )- 두 키는
저장할 때
mint_nonce가 encryption_context에 자동으로 추가된다 확인.- 값은 체크포인트 경로의 sha256 해시이고,
encryption_context의mint_nonce값으로 자동 삽입된다. 식으로 쓰면mint_nonce = sha256(path)다. - 이는 KMS가 unwrap 시점에 context 매칭을 검사하는 표준 AWS-KMS 패턴을 이용해, 같은 KMS 키로 다른 체크포인트를 unwrap하려는 시도를 실패시킨다.
- 예컨대 공격자가 체크포인트 파일을 다른 경로로 복사하고 unwrap을 시도하면, mint_nonce가 어긋나 KMS가 거부한다.
encryption_context = dict(self.checkpoint_config.encryption_context) encryption_context["mint_nonce"] = hashlib.sha256(os.fsencode(path)).hexdigest() kms_client = xai_kms.KmsClient.from_cluster_env( key_id=self.checkpoint_config.encryption_key_id, encryption_context=encryption_context, )- 값은 체크포인트 경로의 sha256 해시이고,
새로 알게 된 것그 밖의 변경
암호화 옵션과 tensorstore 저장 방식이 분리됐다 확인 —
phoenix/xrex/train/trainer.py._uses_tensorstore_save가 더 이상 encrypt를 강제하지 않는다.- 이전에는
encrypt=True가 tensorstore save 방식을 자동으로 켰다. - 이제는
checkpoint_write.save_checkpoint가kms_client를 인자로 받아 자체적으로orbax_encrypted.get_encrypted_checkpointer를 조립한다.
def _uses_tensorstore_save(self) -> bool: return self.checkpoint_config.save_method == "tensorstore"평문 저장으로 내려가는 것(downgrade)을 막는 오류 메시지가 구체적으로 바뀌었다 확인.
- 이전 메시지는 "restored from an ENCRYPTED checkpoint but orbax writer cannot produce encrypted saves"였다.
- 새 메시지는 "restored from an ENCRYPTED checkpoint but checkpoint_config.encrypt is false — refusing to downgrade to plaintext saves; set checkpoint_config.encrypt=true with encryption_key_id/encryption_context"다.
- 새 메시지는 "왜 실패했고 무엇을 하면 되는가"를 명시한다.
_AsyncCheckpointer가 orbax_encrypted 모듈의 wait_until_finished까지 기다리도록 확장됐다 확인 —phoenix/xrex/utils/checkpointing.py.- checkpoint save의 flush가 끝나는 시점에 암호화 IO도 모두 끝났음을 보장한다.
정본 반영 services/x.md의 4세대+13 소절에 있는 "envelope-encrypted Orbax checkpoint writer" 블록 뒤에, 4세대+14에서 바뀐 부분을 이어 붙인다.
의미 암호화 checkpoint의 읽기·쓰기 경로는 갖춰졌지만, 프로덕션 기본값으로 켜졌다는 증거는 아직 없다.
- 이번 커밋은 write path의 검증(
domain/run강제와mint_nonce자동 삽입)과 read path의 두 포맷 지원을 완성한다. 그러나CheckpointConfig.encrypt = True가 실제 트레이닝 config에서 기본값으로 켜졌는지는 이 diff에 없다. 이는 여전히 관전 포인트다. - 파일 포맷이 문서화됐다. 4096바이트 헤더,
XAIENC01magic, wrapped_len/context_len 오프셋이 코드에 남았기 때문에, 미래의 외부 도구(체크포인트 뷰어, 감사 툴)가 이 포맷을 파싱할 수 있다. xai-dek1:prefix는 키 파생 방식(key derivation scheme)을 나타낸다 추정. "derived master"라는 표현은 salt로부터 파생된 마스터 키를 헤더에 담고, 감쌈은 wrapped 필드에 담는 KDF 기반 방식을 시사한다 추정. 이는 legacy per-file wrapped key(각 파일마다 KMS-wrapped DEK를 별도 저장)와 대비된다 추정. X.AI가 만든 자체 KDF 스펙("xai-dek1")의 세부는 코드에 없다. 아마xai_kms.nfs모듈에 있다 추정.- KMS-proxy KvStore를 걷어 낸 것은 성능 면에서 의미가 있다. 이전에는 매 IO(ocdbt operation 하나)가 KMS 호출을 필요로 했지만, 이제는 DEK를 한 번 unwrap한 뒤 로컬에서 대칭 복호화한다. KMS 왕복이 IO마다에서 한 번으로 줄어드므로 checkpoint restore 지연도 줄 것이다 추정.
- 한 번 unwrap하는 경로는
adopt_tree_dek가 맡는다 확인 —dek.py.
- 한 번 unwrap하는 경로는
5two-tower 학습의 기본 설정이 logit 상한 대신 QK-norm을 쓰게 됐다 — X
자료 xai-org/x-algorithm@85ac72a — 공개 소스코드, 2026-09-02. 이 노트들이 붙인 번호로는 4세대+14다.
배경 two-tower는 유저와 아이템을 각각 다른 인코더(타워)로 임베딩해 그 유사도로 후보를 찾는 retrieval 모델 구조다. 4세대+6(2026-08-21, 28e414f)에서 two-tower의 nano config에 qk_norm=True·attn_logit_cap=-1·right_anchored_rope=True 세 옵션이 처음 도입됐다. 다만 그때 조합 base config의 기본값은 그대로 qk_norm=False·attn_logit_cap=80.0이었다. 이번 커밋(4세대+14)은 그 기본값을 뒤집어, QK-norm을 실제로 조합 two-tower 학습의 기본으로 만들었다.
새로 알게 된 것A. 조합 two-tower의 기본 config가 QK-norm을 켜고 attention logit 상한을 껐다
조합 two-tower의 base config에서 attention 관련 기본값 두 개가 바뀌었다 확인 —
phoenix/xrex/configs/xrecsys_two_tower.py.qk_norm: False → True: attention의 query·key 벡터에 정규화(L2)를 추가한다. QK-norm은 트랜스포머 학습 안정화의 표준 기법 중 하나다.attn_logit_cap: 80.0 → -1: attention softmax 이전에 로짓 값을 clip하던 상한(80)이 -1(=off)로 바뀌었다. QK-norm이 켜지면 QK 내적이 이미 bounded되므로 별도 로짓 상한이 불필요해진다.
def _xrecsys_two_tower_combined_base() -> dict: return { ... "learning_rate": 2e-3, "emb_learning_rate": 0.1, "qk_norm": True, # was False "attn_logit_cap": -1, # was 80.0 "primer_norm": True, ... }
정본 반영 services/x.md의 4세대+14 소절에 이 하이퍼파라미터 변경을 짧게 기록했다.
의미 two-tower retrieval의 학습 안정화 방식이 logit clamp에서 QK-norm으로 옮겨졌다. 두 기법은 같지는 않지만 비슷한 목적(soft attention의 dynamic range 제어)에 쓰인다. 4세대+6(2026-08-21, 28e414f)의 옵션 도입에서 이번 4세대+14의 기본값 전환까지 12일이 걸린 것은, 아마 짧은 A/B에서 QK-norm 쪽이 학습 loss나 다운스트림 metric에서 우세를 보였기 때문일 것이다 추정.
6신규 논문 30건 가운데 레이더 기준을 통과한 것은 0건이다 — arXiv cs.IR
arXiv cs.IR 신규 30건(파싱 성공, 조회 정상) 가운데 radar 수용 기준을 통과한 것은 0건이다. 수용 기준은 플랫폼 기업 저자 또는 프로덕션 배포·A/B 진술, 피드/랭킹 메커니즘의 신규성, 대규모 스케일 결과다. 이날 Meta와 Netflix의 신규 자료는 없었다.
- ReST (From Language to Behavior: Scaling Sequence Transformers for Industrial Recommendation Ranking with Rec-Native Designs)는 이번 triage 창에 잡혔지만, 어제 2026-09-03 daily에서 이미 radar.md 이번 달 신규에 올렸다. 중복 등록하지 않는다.
- hLLM은 생성형 rerank 방향의 흥미로운 decoding 최적화지만, radar 기준을 넘지 못해 탈락했다.
- 제목은 "Single Pass Decoding for Generative Reranking"이고, 저자는 Emil Laftchiev·Prachi Agrawal·Moe Kayali 외다.
- LoRA fine-tuning과 Hungarian algorithm으로 N개 순위를 O(1) forward pass에 뽑는 decoding scheme이며, 28ms end-to-end에 도달했다고 주장한다.
- 방법론 논문이고 프로덕션 배포 진술이 없다.
- 저자 소속은 open-review author profile로 확인해야 하지만, 초록만으로는 플랫폼 기업 시그니처가 없다.
- 앞으로 플랫폼 기업에서 이 방향의 프로덕션 진술이 나오면 다시 검토할 대상이다.
- SPAR는 Generative POI Recommendation 논문이다. POI에 한정된 연구라 원칙적으로 탈락했다(레이더 도메인 제한).
- GenCAR도 탈락했다. OOD recommendation을 다룬 학술 실험이다.
- 나머지도 모두 탈락했다. 확인한 사유는 세 갈래다.
- benchmark/RAG 방법론(플랫폼 프로덕션 진술 없음)
- 도메인 제한(POI·법률·의료·과학)
- 저자 소속이 학술 기관 위주
정본 반영 없다(radar 신규 등록 없음).
검증에서 뒤집힌 것
없음.
레이더
없음.
다음에 볼 것
- backbone 점수의 downstream 소비처를 본다 — 이번 커밋에서는 home-mixer가 candidate에 저장만 하고, 소비하는 코드는 아직 열리지 않았다. 다음 커밋들에서
MultiRiskAdsBlender,VMRanker, DPP re-rank, side-effect 로거 중 어느 것이 backbone_scores를 소비하는지 관찰하면 backbone/head 두 층 아키텍처의 실제 목적이 드러난다. - 어떤 scarecrow 봇이
CreateXReviewReportProd를 호출하는지 본다 — 함수 유닛은 새로 생겼지만, 이를 호출하는 실제 scarecrow 룰 정의(bystander_impersonation감지 로직 등)는 아직 리포에 없다. 다음 커밋들에서 룰 DSL이 열리면 어떤 봇이 어떤 조건에서 자동 리포트를 넣는지가 드러난다. useng.common.xreview라이브러리의 다른 사용처를 본다 — grox PtOS, home-mixer, phoenix 중 어느 쪽이 이 라이브러리를 통해 XReview에 리포트를 넣는지 확인한다. 특히 grox의 adult content cross-validation 결과가 XReview lane으로 흘러 들어가는지가 핵심이다.CheckpointConfig.encrypt=True가 프로덕션 config에 나타나는 시점을 본다 — 4세대+13(2026-09-01)에서 연결됐고, 이번 4세대+14에서 write-path 검증과 read-path가 갖춰졌다. 다음은 config 기본값 전환이다. 트레이닝 config에서 켜지는 시점을 보면, X가 학습 checkpoint를 실제로 저장 상태에서 암호화(rest 암호화)하기 시작한 시점이 확정된다.- XReview 서버의 lane tag 라우팅과 evidence rollup 로직을 본다 — 이번 커밋은 클라이언트 측 스키마만 노출했다.
report_type→ lane tag 매핑, evidence rollup 알고리즘(같은 콘텐츠에 대한 N개 리포트 → 판정) 등은 XReview 서버 코드가 리포에 노출되어야 확정된다. xai-dek1:KDF 스펙을 본다 — envelope 헤더에 담기는 파생 마스터 키의 salt→master 파생 알고리즘이다.xai_kms.nfs모듈이 공개 리포에 나오면 확정된다.