Skip to content

Latest commit

 

History

History
83 lines (80 loc) · 107 KB

File metadata and controls

83 lines (80 loc) · 107 KB

WORKLOG — AI 파트

시간순 작업 로그입니다. 유형별 폴더(spec/proposals/implements/troubleshooting) 분산으로 인한 "내 작업 추적" 비용을 시간축 인덱스로 상쇄합니다. 이후 작업마다 한 줄씩 추가합니다.

이 파일은 merge=union입니다(.gitattributes). 여러 브랜치가 각자 줄을 추가해도 병합 충돌이 나지 않고 양쪽 줄이 모두 남습니다. 대가로 두 가지를 감수합니다.

  • 줄 순서가 보장되지 않습니다. 시간순으로 읽을 때는 표의 날짜 컬럼을 기준으로 삼습니다.
  • 기존 줄을 수정하면 중복이 생길 수 있습니다. 두 브랜치가 같은 줄을 각각 고치면 두 버전이 나란히 남습니다. 이미 있는 줄을 정정할 때는 병합 결과에 중복이 없는지 확인하세요. 줄을 추가하는 평소 작업에서는 신경 쓸 필요가 없습니다.

GitHub은 이 설정을 반영하지 않습니다. 로컬에서 충돌 없이 병합되는 상황도 PR 화면에서는 여전히 CONFLICTING으로 표시됩니다. 따라서 dev를 브랜치에 머지해 다시 올리는 절차 자체는 그대로 필요합니다. 달라지는 것은 그 머지에서 손으로 충돌을 풀 일이 없어진다는 것입니다(back·ai 관련 Jira 작업에서 실측).

날짜 작업 관련 문서
2026-08-03 단어형 질의로 컷 격자를 다시 훑어 τ_abs 를 질의 길이로 갈랐다(Jira 작업, 단어형 0.24 / 문장형 0.30 · r=0.60 불변). -255 가 「#87 은 컷 조정만으로 회복된다」로 판정한 것의 후속이고 이 티켓이 ai#87 을 닫는다. -255 의 22건으로는 답할 수 없었다 — 그것은 세 이슈의 원인을 가르기 위한 진단 집합이라 무관 통제가 치과 하나인데, 컷 값 판단은 「무관 대역이 어디까지 올라오는가」가 전부다. -213 이 정확히 같은 오류를 범한 기록이 있다(personal-search.md §6 이 무관 1건으로 「간격 +0.2120, 컷 불필요」를 결론 냈고 15건으로 늘리자 -0.0176 이 되어 뒤집혔다). 무관 통제를 45행으로 늘리자 역전이 -0.0515 → -0.1687 로 깊어졌다. 기대 정답을 손으로 짝짓지 않았다 — 단어형은 정답이 여럿이라(신한→6건 · 라멘→2건) 손으로 정하면 판정자 재량이 결과를 만들므로 본문 문자열 포함으로 계산했고, 이 기준이 ai#87 의 요구(「본문에 있는 말로 검색하면 나와야 한다」)와 정확히 일치한다 — -255 가 실측한 「완전 일치는 유사도에 유의미하게 기여하지 않는다」는 재는 쪽의 기준이 틀렸다가 아니라 임베딩이 그 기준을 못 따라간다는 뜻이다. 부작용이 이득이었다 — 같은 질의를 소유자 셋에게 던지면 정답 있는 행과 없는 행이 동시에 생겨 통제 표본이 추가 측정 없이 늘었다(정답 66행 · 교차 96행 · 무관 45행). 통제를 두 층으로 갈랐다. cross(같은 어휘인데 그 소유자엔 없다)는 같은 생활권이라 판별력이 약하고, 합치면 「무관 통과」 기준이 약해진다. 라벨을 요구하지 않는 대신(207행은 손 라벨이 비현실적이다) 꼬리 제거율을 포기하고 무관 통제를 늘렸다. 계약의 전제를 정정한다 — 「문장형 12건 회귀가 주 리스크」는 틀렸다. 격자 240조합 전량에서 문장형 정답 누락 0 이다(이 티켓이 움직이는 방향이 컷을 푸는 쪽이라 구조적이고, 문장형 정답 최솟값 0.3642 는 격자 상단보다 위다). 진짜 손실은 다른 열에 있었다. 문장형 무관 질의의 무노출이 -213 의 15건 중 11건(11/15)에서 0.24 단일값이면 15건 중 5건(5/15)으로 절반 아래로 줄어든다. 이 정정이 채택안을 바꿨다 — 손실이 「정답 누락」이 아니라 별도 축이라 「가르면 그 손실이 0 이 된다」는 셋째 길이 열렸다. 채택값을 정한 것은 회복률이 아니라 「1위 손실」이다(측정 도중 추가한 지표) — 0.26 은 회복 68/71(96%)로 충분해 보이지만 컷 전 1위인 정답 3건을 0건으로 만든다 (비건→플랜트 0.2583 1위인데 결과 0건 · 스팟→동교공원 0.2438 1위 · 야경→언덕위 0.2552 1위). 「1위인데 0건」은 컷이 유사도 순서를 존중하지 않는다는 뜻이고 회복률 96% 뒤에 숨는다. 0.24 는 「컷 전 1위 정답을 하나도 잃지 않는 가장 높은 값」이고 0.25 부터 깨진다(격자를 0.01 간격으로 좁힌 이유 — 후보가 데이터점 하나 스팟 0.2438 에 붙어 -213 의 0.02 격자에서는 0.24~0.26 사이가 통째로 안 보인다). r 은 가르지 않았다 — 무관 통과를 어느 값에서도 한 건도 못 줄이고(-213 이 문장형에서 본 「1위를 언제나 남긴다」가 단어형에서 그대로 재현) 단어형 손실도 r=0.75 까지 0 이라 이 교환에 참여하지 않는다. 갈랐다면 재지 않은 축을 코드가 만드는 셈이다. τ(문장)=0.34 도 기각했다(무관 통과가 1/15 로 더 좋지만 이 티켓은 문장형을 재지 않고 matrix.json 을 재사용했을 뿐이라 남의 측정 위에서 남의 마진 판단을 바꾸지 않는다). 결과 — 단어형 회복 61/71 → 71/71 · 1위 손실 5 → 0 · 정답 누락 11/66 → 2/66, 문장형 완전 불변, 대가는 단어형 무관 통과 10/45 → 26/45. AI API 임베딩 배치 1회(69건) · 격자 240조합은 AI API·DB 미호출. 실서버 대조 87/87 PASS — 재구성 쪽에 길이 분기를 다시 적어(구현 import 금지, -213 규칙) 「서버가 분기를 실제로 타는가」가 관측되게 했고, 두 하한에서 결과가 갈리는 60행만 골랐다(갈리지 않는 행은 서버가 무엇을 하든 통과한다). ai#87 증상 회복을 실서버로 직접 확인 — 「그네」 0건→6건(동교어린이공원 3위) · 「스팟」 0→2건(1위) · 「비건」 0→1건(1위) · 「산책」 0→4건. 441건 통과(테스트 함수 11건 추가 — 단위 9 + API 관통 2). 검토 확정 10건 중 A·B·C 를 반영했다 — 배선 고정(#4 major)·킬스위치를 분기 앞으로(#2)·recall_probe.py 분기 반영(#3)·경계 5자 고정(#8)·공백을 유니코드 전체로(#1·#5)·끊긴 참조 셋(#9·#10)·수치 정정(#6). 넷을 변이 드릴로 확인했다 — 「테스트를 더했다」와 「그 테스트가 그 결함을 잡는다」는 다른 명제라 실제로 어긋내 보고 대응하는 테스트만 실패하는 것을 봤다. recall_probe.py 가 이 티켓이 고친 증상을 계속 「① 컷이 잘랐다」로 읽고 있었다 — 진단 도구가 닫힌 증상을 미해결로 보고하는 상태였고, 분기를 넣으니 그네·스팟·산책·라멘 이 「— 통과」로 바뀌고 신한·부캠 은 ③ 그대로다. 그 도구의 --replay커밋된 행렬을 판정 결과로 덮어쓰던 것도 함께 막았다(기본은 쓰지 않고, --out 이 입력과 같으면 멈춘다) — 행렬은 AI API 를 불러야 다시 뜨므로 그 손실이 판정보다 무겁다 · line 99.83%/branch 98.99%(search_service.py 100%). 길이 분기의 호출부 배선을 API 관통 테스트로 고정했다_cut(rows, query)query 기본값 때문에 , query 가 사라져도 커버리지 100% 인 채로 모든 단어형이 0.30 으로 돌아가 ai#87 이 재발한다(RED 드릴로 확인). 유사도를 두 하한 사이 [0.24, 0.30) 에 두는 것이 요점이고 기존 검색 테스트는 질의와 저장 텍스트가 같아 유사도 1.0 이라 이 배선을 못 잡는다 — 커버리지가 안전을 보증하지 못하는 자리다. **-228T68(임베딩이 같은 배치 구성으로도 비결정적 · ` Δsim
2026-08-03 운영에서 「본문에 그대로 있는 말로 검색해도 안 나온다」가 셋 올라와(ai#86 신한 · #87 그네 · #88 부캠) 원인을 실측으로 갈랐다(Jira 작업). 고치지 않았다 — 컷 값도 모델도 코드도 그대로다. 측정보다 먼저 배제한 것은 티켓이 「넷째일 수 있다」로 남긴 가능성이다 — 검색 Query 가 embedding_status = 'COMPLETED' 를 요구하므로 그 Context 가 FAILED 로 굳었으면 유사도를 재는 일 자체가 무의미한데, 전량 42/42 COMPLETED · 벡터 42건 · is_deleted 0 으로 정상이었다. 결론은 「셋 다 같은 원인」이 아니다그네 는 컷이 잘랐고 풀면 3위(), 신한 은 풀어도 6위 · 부캠 은 8위()다. 가른 것은 계약의 최소 비교군에 더한 무관 기준선이다 — 어느 본문에도 없는 2자 치과 의 top-1 이 0.2953 인데 본문에 그대로 있는 스팟0.2438, 그네 0.2671, 신한 0.2301 로 셋 다 그보다 낮다.완전 일치는 유사도에 유의미하게 기여하지 않는다#86 이 「완전 일치인데 안 걸린다」로 가장 심각하다고 본 전제가 실은 이 현상의 한 사례다. 본문을 고정하고 질의 길이만 바꾸자 경계가 깨끗하게 드러났다 — 본문 A·B 모두 3자 이상은 전부 ≥0.3189, 2자는 전부 ≤0.2818 이고 τ_abs=0.30 이 그 사이에 있다. 다만 「2자면 안 된다」는 틀렸다우주 0.4199 · 공원 0.3670 이 2자로 통과하며, 갈림은 「그 단어가 본문의 주제를 짚는가(우주·공원)와 부수 정보인가(스팟·신한)」로 보이나 그것을 통제한 측정은 없다(사후 해석이다). #87 의 BPE 가설은 반증됐다 — 프로파일이 text-embedding-3-small 이라 토크나이저가 cl100k_base 로 특정되어 실제로 봤고, 그네[49706, 76242, 97] 와 그네팟[49706, 76242, …] 의 앞 두 토큰이 같다(신한신한 부트캠프, 부캠부트캠프 도 공유). 한국어가 바이트 단위로 잘게 쪼개져 접두 토큰이 그대로 남으므로 「한 덩어리로 묶여 멀어진다」가 성립하지 않는다 — 그네 는 부분어라서가 아니라 다른 2자 질의와 같은 대역이라서 안 된다. 반면 #88 의 진단은 확증됐다부트캠프 는 카츠요 1위 0.3254 인데 부캠 은 8위 0.2105 이고, 그러면서 부캠 이 그대로 있는 쿠로코를 1위(0.4102)로 짚는다. 표기는 알고 부캠→부트캠프 의미 연결만 못 한다. 그래서 처방이 셋 다 다르다 — #86 은 원리상 임베딩으로 못 풀고(2자 대역이 무관 질의와 겹침이 아니라 역전이라 어떤 임계값으로도 못 가른다) 키워드 검색이 맞고, #88 은 의미 연결이라 모델 교체(-199)의 영역이며, #87 은 컷 조정만으로 회복된다. 신한 이 카츠요에서만 낮은 기제는 답하지 못했다 — 0건이 아니라 3건이 나오고 셋 다 「신한」 보유라 검색은 「신한」을 알아보는데, 상위 셋은 「신한 부캠」이고 카츠요만 「신한 부트캠프」다. 표현 일치 효과는 실측했으나(신한 부캠 질의에서 카츠요 0.3189 ↔ 신한 부트캠프 질의에서 0.3793) 왜 「신한 부캠」이 「신한」 단독에 더 가까운지는 단정하지 않는다. 본문 길이는 원인이 아니다 — 플랜트(30자) 0.3889 와 카츠요(32자) 0.2301 이 같은 길이에서 갈리고 순위 상관도 약하다(단어형 ρ -0.12 · 문장형 +0.07, 17점 표본이라 유의하지 않다). -213 의 한계와 같은 구조이고 더 심하다 — 저쪽은 문장형에서 정답↔무관이 -0.0176 겹쳤고 이번은 단어형에서 -0.0515 로 역전이다. 「컷 값으로는 더 못 간다」가 강화된다(단어형을 살리려면 τ_abs 를 0.24 아래로 내려야 하는데 그 값은 무관 대역 0.2953 을 통째로 통과시킨다). AI API 임베딩 배치 1회 · 코드 변경 없음 · 로컬 시연 DB(:15432)만 사용. 실서버 대조는 하지 않았다(-213verify_live.py 가 같은 재구성 규칙으로 27/27 을 이미 확인했고 이 티켓은 규칙이 아니라 질의만 바꿨다 — 다만 이 질의 집합으로 실서버를 때린 것은 아니다) (Jira 작업) 재현율 판별 리포트 · tools/search_cut/ · ai#86·#87·#88
2026-08-03 DB 실패에도 응답 본문이 embedding upstream ...를 답하던 것을 층 이름별로 가른다(Jira 작업). -221이 상태 코드(503/502)와 로그는 정확히 맞혔지만 본문만 사실과 어긋난 채 남겨 뒀고, 그 티켓이 *"핸들러를 바꾸지 않는다"*로 확정하며 본문 개정을 후속으로 미뤘다 — 이 티켓이 그 후속이다. -221이 이미 둔 하위 타입(DatabaseTransientError/DatabasePermanentError)을 app/main.py의 기존 두 핸들러 안에서 isinstance로 보고 database unavailable/database rejected the request로 가르되, 원인 값은 여전히 싣지 않는다-220이 세운 "예외 메시지를 본문에 옮기지 않는다"와 -205가 실측으로 확정한 누출 경로 차단 원칙을 재확인만 했다. 새 핸들러를 추가하지 않아 -220의 "변환은 예외 핸들러 한 곳에서만" 원칙도 그대로다. 공용 계약 static/05_AI_설계.md는 반영하지 않았다 — ①그 문서는 failure-recovery.md를 파트별 세부 문서로 참조만 하고 응답 본문 문구 자체를 담고 있지 않으며, ②back 레포 AiSearchClient.translate를 직접 읽어 502/503 등 이 티켓이 다루는 상태 코드는 본문을 파싱하지 않고 상태 코드만 보고 버림을 확인했다(serverProfile 필드를 보는 422만 예외). 검증은 -220·-221의 원칙을 그대로 지켰다 — Fake 없이 실제 asyncpg 풀을 실패시켜 관통하는 계약 테스트에 신규 2건(DB 축 새 문구)과 회귀 2건(AI API 문구 불변)을 더했다. RED로 신규 2건이 여전히 embedding upstream ...를 답함을 먼저 확인했고, 핸들러 수정 뒤 GREEN. 전체 425건 통과, line 99.83%/branch 98.99%(게이트 80/80 유지). 실서버 대조는 별도로 하지 않았다 — 문구만 바뀌었고 -221의 관통 테스트가 이미 실제 DB 실패 경로를 검증하고 있어 새로 확인할 것이 없다고 판단했다 (Jira 작업) 오류 문구 분리 리포트 · spec/failure-recovery §2.5 「응답 본문」
2026-07-31 검색 중 DB 실패를 일시 오류로 분류해 503 으로 내보낸다(Jira 작업). -220TransientError→503·PermanentError→502 핸들러를 넣으며 500 을 「우리 코드의 결함」 전용으로 비워 뒀는데, DB 실패만 아무도 분류하지 않아 그대로 500 이었다 — 커넥션 풀 고갈이나 DB 재기동처럼 기다리면 낫는 상황이 「코드가 깨졌다」와 같은 코드를 썼고 그만큼 -220 의 전제가 깨져 있었다. 명세는 이미 이 동작을 규정하고 있었다failure-recovery.md §2.1 이 "DB 연결 실패, 잠금 타임아웃, 직렬화 실패" 를 일시 오류로 못박고 있었으므로 이 티켓은 새 정책이 아니라 명세와 코드의 간극을 메운 것이다. 가장 값진 발견은 티켓 전제가 어긋났다는 것이다 — 티켓은 "asyncpg 예외를 분류" 라고 적었지만 실측하면 DB 접속 실패는 asyncpg 예외가 아니다. 포트 거부(ConnectionRefusedError)·DNS(socket.gaierror)·접속 타임아웃·커넥션 풀 획득 타임아웃이 전부 stdlib OSError 이고, 08xxx PostgresConnectionError이미 붙어 있던 연결이 끊길 때의 SQLSTATE 다. 문구대로 asyncpg 만 분류했으면 테스트는 전부 통과인데 티켓의 완료 조건만 조용히 거짓으로 남았을 것이다(T53). 같은 이유로 분류를 repository 함수가 아니라 세션 경계(app/core/db.py)에 걸었다 — 데코레이터 방식은 pool.acquire() 가 그 밖이라 접속 실패가 애초에 들어오지 않는다. 경계는 한 줄이다 — 서버·연결의 상태 때문에 실패했으면 일시적, 우리가 보낸 질의 때문이면 우리 결함. SQLSTATE 군 단위로 긋고 군 안에서 성격이 갈리는 셋(25xxx·55xxx·42xxx)만 잎으로 집었다(Transient OSError(접속 단계)·08·40·53·57·58·55P03·25P03·25P04 / Permanent 28·3D000·42501). 미분류(500) 목록이 분류 목록만큼 중요하다42xxx 나머지·22xxx·23xxx·XX000 은 그대로 500 에 남긴다. 넓게 잡으면 버그가 「일시적으로 사용할 수 없습니다」 뒤에 영구히 숨고 재시도해도 안 되는데 알림이 안 울린다. 가장 논쟁적인 것은 asyncpg.InterfaceError 를 분류하지 않은 것이다 — 한 타입이 connection is closed(수명주기)와 the server expects 2 arguments, 1 was passed(우리 결함)를 함께 쓰고, 통째로 감싸면 후자가 숨는다. 수명주기 쪽은 정상 경로에서 나오지 않는다(풀에서 갓 받은 커넥션이 죽어 있으면 질의는 08003 이고 그쪽은 이미 일시 오류다 — 실측). OSError접속 단계에서만 번역한다 — 커넥션을 넘겨준 뒤의 블록에는 호출부 코드가 함께 들어와 무관한 OSError 가 503 으로 둔갑할 수 있다. -220 의 핸들러를 한 줄도 고치지 않았다DatabaseTransientError(TransientError) 하위 타입이라 기존 핸들러가 그대로 받고, 덤으로 핸들러 로그의 타입 이름이 「게이트웨이가 아니라 DB」를 말한다. 예외 메시지에는 타입 이름과 SQLSTATE 만 담는다 — DSN 에 DB 비밀번호가 있고 접속 실패 예외에 host·port 가 섞이며, -220 이 게이트웨이 응답 200자 누출을 발견하고 세운 기준이 그대로 적용된다. SQLSTATE 를 남기는 것은 "풀이 고갈됐다""DB 가 재기동 중이다" 를 가르는 유일한 값이라서다(app.core.db 로거, 일시 WARN·영구 ERROR). 검증은 -220 의 원칙을 지켰다 — DB 도 Fake 로 바꾸지 않는다. 닫힌 포트를 가리키는 min_size=0 풀·서버가 실제로 죽인 백엔드(08003)·실제 statement_timeout 취소(57014)·실제 3D000 으로 냈고(계약 8건), 표 전체는 DB 없이 분류 함수로 고정했다(단위 40건). min_size=0 이 필요한 이유가 T54 다 — 운영 min_size=1 풀은 접속이 깨지면 기동에서 실패해 요청 경로에 닿지 못한다. RED 4건 → 전체 356건 통과, line 99.82% branch 98.84%. 실서버 대조는 전용 pgvector 컨테이너를 docker stop 해서 냈다(시연 DB 를 건드리지 않았다) — origin/dev 500 + 트레이스백 ↔ 이 브랜치 503 {"detail":"embedding upstream unavailable"}. 재시도는 넣지 않았다(DB 재시도는 커넥션 풀 동작과 얽혀 별건). 남은 한계 둘 — 응답 본문이 DB 실패에도 embedding upstream ... 을 답하고(상태 코드·로그는 정확하며 back 은 본문을 읽지 않는다. 본문 변경은 -220 계약의 개정이라 후속), 53100 디스크 가득 참을 일시적으로 둔 것은 재시도로 낫지 않아 표에서 가장 약한 칸이다 (Jira 작업) DB 오류 분류 리포트 · spec/failure-recovery §2.5 · T53~T55
2026-07-31 판정 프롬프트로 「본문에 근거 없으면 미선택」을 세우려 했으나 두 개정안 모두 채택하지 않았다(Jira 작업). -210 이 후보 선정 층(τ)으로는 못 푼다고 끝내며 이 층을 지목했고, 진단은 맞았으나 처방이 따라오지 않는다. B(무엇이 근거가 아닌지를 유형으로 지정)는 오분류 Δ -0.60행으로 효과가 없었고, B 가 조항으로 이름 붙여 대상으로 삼은 넷(265 WALK·274/289 DRINK·284 WITH_FAMILY)이 5회 중 5회 그대로였다. 축을 바꾼 C(후보 목록의 출처를 고지하고 「고르지 않는 것이 기본」을 세움)는 5회에서 Δ -2.60 이었으나 표본을 10회로 늘리자 -1.60 으로 줄었고(사전 기준인 범위 비중첩도, 보강한 순열검정 p=0.081 도 미달) — 5회에서 멈추고 기준을 낮췄다면 부풀린 값을 채택했을 것이다. 교환비 0.19 는 -210 이 τ 에서 기각한 1.22 보다 훨씬 좋지만, 교환비는 「효과가 있다」가 먼저 서야 의미가 있는 값이고 그 앞 질문을 못 넘었다. 결정적인 것은 셋이다 — ①사용자가 보는 손실(fit 0건 Context)이 A·B·C 모두 8.00 ± 0.00 으로 회차 운으로도 안 움직인다, ②C 가 대상으로 삼은 유형 다섯이 오히려 늘고 셋이 줄어 규칙과 실제 변화 사이에 대응이 없다, ③오분류 30종 중 10/10 으로 고정된 것은 2종뿐(그마저 같은 본문 한 쌍이라 실질 하나)이고 나머지 25종은 회차마다 구성원이 바뀌는데 흔들리는 fit 은 6종뿐이다. 즉 정상 판정은 안정적이고 오분류만 흔들린다. 프롬프트 규칙은 「항상 붙는 것」을 대상으로 삼는 도구인데 그 대상이 사실상 하나뿐이라 잔액이 노이즈와 같은 크기로 나온다. τ 가 분포가 겹쳐서 못 갈랐다면, 프롬프트는 고정된 대상이 없어서 효과가 없다. A 회차끼리의 불일치 중앙 11/42(26%)가 -210 의 대조군 값과 정확히 일치해 다른 하네스·다른 회차로 재현됐다. 측정 설계에서 넷을 막았다 — 벤더 폴백을 끄고 단일 고정(-175 에서 벤더별 일치도가 0.53~0.93 으로 갈렸다), labels.yaml 원본 불변 + labels_extra.yaml 로 커버리지만 확장(재판정은 τ 스윕과 달리 없던 행을 만든다 — 24종), 라벨링 시 조건 은닉(보고 붙이면 라벨이 결론을 따라간다), 사전 기준을 지우지 않고 자를 더하기. 실제 판정 모델은 gpt-4o-mini 였다.envPINLOG_JUDGE_MODEL=gemini-2.5-flash-175PINLOG_JUDGE_CHAIN 으로 대체해 읽히지 않는 키인데 값이 그럴듯해(체인 2순위) -210 리포트 §7 의 조건 표가 그것을 읽은 것으로 보인다(T43). 측정 중 confidence 가 유사도가 못 한 분리를 보여(하한 0.70 에서 unfit 4 제거·fit 유실 0) 후속으로 올릴 뻔했으나, 같은 조건으로 한 번 더 받으니 분리가 사라졌다 — 판정은 선택뿐 아니라 confidence 도 비결정적이다(T47). 그래서 후속은 프롬프트 문구가 아니라 판정 분산 자체를 줄이는 쪽(n회 판정 다수결)으로 제안한다. AI API 판정 1,092회·실패 0·코드 변경 없음 판정 프롬프트 리포트 · T43~T49 · tools/prompt_ab/
2026-07-29 Jira 작업 — protected Environment 기반 3 runtime owner 값 + Infra PR credential handoff를 canonical workflow로 정렬 runtime Secret handoff
2026-07-30 판정 LLM 이 Gemini 한 경로에 묶여 있던 것을 벤더 어댑터로 풀고 폴백 체인을 넣었다. 429 는 게이트웨이 전역이 아니라 프로바이더 경로별로 걸린다-176 재측정에서 같은 시각·같은 키로 Gemini 만 92% 였고 OpenAI·Anthropic 은 12/12 였다(임베딩 49회가 안 막힌 것과 같은 그림). 순서는 실측대로 gpt-4o-mini(100%·0.91s) → gemini-2.5-flash(기준선 유지) → claude-haiku-4-5(프로바이더가 셋째라 동시 장애 가능성이 가장 낮다)이고, gpt-4.1-nano(일치도 0.53)·flash-lite(58%)는 기각, 일치도 1위 gpt-4.1-mini(0.93)는 응답 1.37s·최대 4.67s 라 속도를 택했다(그 차이는 판정 비결정성 범위 안). 시도 예산을 체인 길이에 곱하지 않았다 — 벤더마다 3회면 최악 3×3×90s = 810s 로 §3.2 상한(PROCESSING 만료 600s)을 넘어 재스캔이 살아 있는 판정을 중복 실행한다. attempts 를 판정 1건의 총 시도로 두고 n번째 시도가 n번째 벤더를 쓰게 하니 최악이 270s 로 폴백 이전과 같고, 부수 효과가 오히려 본질이었다 — 막힌 벤더에 백오프를 걸고 다시 던지는 것보다 다른 경로로 즉시 넘어가는 편이 성공 확률이 높다(위 실측). 체인을 하나로 줄이면 시도가 그 벤더로 모여 폴백 이전과 정확히 같아지므로 롤백이 설정 한 줄이다. PermanentError(400·401·403)는 폴백하지 않는다 — 키·설정 문제는 다른 벤더에서도 같은 답이고 넘어가면 AI API 호출만 3배가 된다(-121 결함 3 의 재발 형태). 구조화 출력 위반은 폴백 사유에 넣었다(방식이 벤더마다 달라 한쪽이 절단·차단으로 깨져도 다른 쪽은 성공할 수 있다). 프로브 스키마를 그대로 옮기지 않았다 — 프로브는 keywordId 하나만 요구하는 축약본이었고 운영은 confidence·unmatchedConcepts 까지 받아야 하는데, OpenAI strict 는 모든 객체에 additionalProperties: false 와 전 property required 를 요구해 Gemini 스키마를 재사용하면 400 이고 400 은 영구 오류라 폴백 없이 판정이 실패로 끝난다. 티켓 범위는 토큰 로그의 vendor 였지만 model_profile 도 함께 고쳤다 — 폴백이 생긴 순간 설정 1순위와 답한 모델이 갈라지고, 그러면 "어떤 모델의 판단이었는지 구분"(keyword-preset §5.2)이 거짓이 된다. PINLOG_JUDGE_MODELPINLOG_JUDGE_CHAIN 이 대체했고(모델 하나로는 순서를, 벤더 없이는 경로·인증 헤더를 표현할 수 없다) 배포 봉인 대상이 아니어서 영향 범위는 로컬 .env 뿐이다. 형식은 config 가, 지원 벤더인지는 어댑터 레지스트리만 알 수 있어 클라이언트 생성이 본다(둘 다 기동 시점). RED 드릴 2회 실측 — 폴백 무력화 6건 실패, PermanentError 폴백 허용 14건 실패(신규 3건 + 기존 재시도 계약 11건). 239 passed · line 99.78% branch 98.46%. 실 AI API 는 부르지 않았다(MockTransport 로 세 봉투를 만든다). 스모크는 체인 전체를 증명하지 않는다. 1순위가 429 면 2순위로 넘어가 통과하므로, 3순위가 동작 불능인데 모르는 상태가 가능하다(벤더별 확인은 probe_vendors.py 수동, 상시 관측은 -96 범위). 임베딩 폴백은 제외다 — 프로바이더가 바뀌면 벡터 공간이 달라져 기존 데이터와 비교가 불가능하고, embedding_profile 이 바로 그것을 막는 장치다 (Jira 작업) 벤더 폴백 리포트 · spec/failure-recovery §3.4 · tests/README
2026-07-23 AI 공용 설계를 docs static/05 단일 원본으로 확립 (docs#2) proposals (P16)
2026-07-23 FastAPI AI 서버 구현 명세 작성 + version→deletion race 리네임 (ai#1) spec/
2026-07-23 Keyword Preset seed 27개 (ai#2) implements, spec/keyword-preset.md
2026-07-23 architecture 구조도(Mermaid) 4종 (ai#4) implements, spec/architecture.md
2026-07-23 Keyword 매칭 평가 A/B/C-1 — 하한 0.30·프롬프트 확정 (test/keyword-matching-eval) implements, P26
2026-07-23 작업 기록 신설 + 문서 재구조화(spec/proposals/implements/troubleshooting + WORKLOG, ADR→P) (ai#4) 이 트리 전체
2026-07-23 eval C-2 3사 모델 비교 완료 — 판정 모델 gemini-2.5-flash(thinkingBudget=0) 확정, M4 종결 (ai#3) implements, P26
2026-07-23 FastAPI 서버 scaffold + 개인 검색(/search) + Preset 부트스트랩 + 운영 비용 추정 (ai#5) spec/personal-search.md, spec/cost-estimate.md
2026-07-23 /context/process 처리 파이프라인 + 상태머신(부분 재개·저장 불변식·gemini-2.5-flash 판정) (ai#6) spec/context-processing.md, spec/state-machine.md
2026-07-23 FastAPI 구현 리포트(I19) + spec 9종 "구현 반영" 표시 갱신 (ai#7) implements, spec/
2026-07-23 문서 gap 마감 — eval 리포트 C-2 반영 + 구현 트러블슈팅 T16~T18 등재 (ai#8) implements, troubleshooting
2026-07-24 파트간 요구사항 참조 static/05-1로 갱신 (ai#9) spec/
2026-07-24 M2 종결 — Context 목록 created_at A안 확정(백엔드 V2~ 블로커 해소) proposals
2026-07-24 /search 응답에 contextId 추가(DISTINCT ON) — Spring matchedContext 조립용, 구현+spec 동반 spec/personal-search.md
2026-07-24 ai 레포 협업 컨벤션 이식(.github 템플릿·CI·ruff) (ai#12) proposals (P15)
2026-07-24 M5 종결 — 09_유저플로우 draft/static 중복 해소 반영 (ai#13) proposals
2026-07-23 E3-PR1 — 테스트 하네스(Testcontainers)+저수준 27케이스+Dockerfile+ai-ci 정비(Jira/lock)+Python 3.12 통일 tests/, Dockerfile
2026-07-24 troubleshooting·implements 기록 보존 원칙 + 상태 헤더 소급 (ai#15) implements, troubleshooting
2026-07-24 ai-ci 핫픽스 — lock 플랫폼 종속(pywin32) 마커 + pytest pythonpath (ai#16) troubleshooting, implements
2026-07-27 전수 조사 갭 정합화 — 절번호 드리프트 3·인덱스·WORKLOG·proposals P40·P41·시나리오5 주석 (ai#19) 이 트리
2026-07-27 문서↔코드 정합 감사 + M3 계약 개정 — spec 5종 코드 대조·정정(architecture §6.2·§5 등), M3 COMPLETED→PENDING 운영 재처리(state-machine + 계약 §6.3·§7.3) (ai#21·docs#13) spec/, state-machine.md
2026-07-27 E2E(-58) 발견 합류 — README 기동 절차(Flyway·DSN·docker run·psql) 정정 + 검색 컷오프 실측 + 판정 비결정성 명시 + implements 유형 컬럼 (ai#22) README, spec/keyword-preset.md
2026-07-27 E3-PR2 — 파이프라인 시나리오 20개(test_pipeline.py, 19함수/20시나리오) (ai#18) spec/integration-tests.md
2026-07-27 코드 주석 §참조·search_path 서술 정정 (ai#20) troubleshooting
2026-07-27 E3-PR2 완료 반영(리포트·spec 헤더 갱신) + preset_cache_ttl_sec dead config 제거(§5 정합) (ai#24) implements, spec/integration-tests.md
2026-07-27 E2E 실경로 검증(-58) — 실제 AI API 프리셋 27 적재·파이프라인 8건·검색 품질(분리도 +0.2120)·하네스 동등성 9/10·권한 경계 실증(-61 근거) + 검증 드라이버 tools/e2e/ implements, troubleshooting (T22~T24), tools/e2e/
2026-07-28 ai#25(인프라 CI 계약 테스트 6) 병합으로 pytest 46→52 확대 → 문서 정합(수치 AI 검증 46 + CI 계약 6·CI 계약 각주·pgvector 불일치 표시) (ai#28) tests/README, spec/integration-tests.md
2026-07-28 S1 구현 판단 맥락 복원 — 설계선택 19·불변식·구현결함 불일치·인프라 미복원(I22), 워킹트리·env캐시(T25·T26), 판단변경·기각(P43), partial-resume §3 재조회 판단변경 (ai#29) implements, troubleshooting, P43
2026-07-28 AI 레포 협업 운영 기준과 Jira→PR 리뷰 절차 수립, app branch coverage 비차단 측정 도입 (Jira 작업) CONTRIBUTING, P44, development
2026-07-29 dev 배포 게이트 3종(I23) — GET /ready(DB SELECT 1 + Preset ≥1, AI API 미호출) · GMS_BASE_URL /gmsapi/ 기동 fail-fast(값 미노출 위해 SettingsError) · app.smoke.gms_roundtrip 양방향 실호출, pytest 52→66 (ai#33 ← ai#32 인프라 요청) implements, README
2026-07-29 AI 소유 값의 클러스터 전달 경로를 만들었다(I24). GitHub Actions Secret 은 Pod 에 자동 전달되지 않으므로 kubeseal --raw 로 값 7종을 개별 봉인해 encryptedData 만 artifact 로 넘긴다 — kubectl create secret --dry-run 경로는 중간에 평문 base64 YAML 을 만들어 쓰지 않았다. EMBEDDING 넷은 비밀이 아니지만(정본 P32) Infra 가 주입 경로를 하나로 요구해 같은 경로로 다룬다. 앱의 기동 검사(/gmsapi/ 형식·profile 정합)를 봉인 시점으로 앞당겨 배포 전에 실패시키고, controller 인증서는 SHA-256 지문으로 고정했다 — 엉뚱한 공개키로 봉인하면 복호화 실패가 배포 시점에야 드러난다. 실제 봉인은 미실행(Actions Secret 등록 후 가능) (Jira 작업) handoff 리포트, ai#32
2026-07-29 봉인 workflow 의 kubeseal 설치가 체크섬 검증에서 실패하던 것을 고쳤다. curl -o kubeseal.tar.gz 로 받아 놓고 manifest 는 kubeseal-0.27.1-linux-amd64.tar.gz 를 가리켜, sha256sum -c 가 manifest 에 적힌 이름으로 파일을 열다 실패했다 (No such file or directory / FAILED open or read). 다운로드·검증·해제가 같은 변수를 쓰도록 파일명을 하나로 묶었다. Infra 가 실제로 실행해서 발견했다 — 병합 전 검증이 YAML 파싱과 bash -n 까지였고 그 둘은 이 결함을 잡지 못한다. 이번엔 실제 다운로드·검증·해제를 로컬에서 돌려 통과를 확인했고, 원 버전이 같은 오류로 실패하는 것도 재현했다 (Jira 작업) handoff 리포트, ai#32
2026-07-29 발표 시연 데이터를 back API 경로로 만드는 시딩 도구 tools/demo_seed/(I25)와 E2E 재확인. SQL 직접 INSERT를 택하지 않았다 — 데이터는 있는데 파이프라인은 안 돈 상태를 만들고, 그러면 "새 Context를 지금 추가하면 Keyword가 붙는다"를 발표 당일에 처음 실행하게 된다. API로 만들면 -102의 PENDING 생성과 /context/process 호출이 매 건 타므로 시딩이 곧 통합 검증이다. SQL은 API가 없는 두 곳(OAuth로만 생기는 core.member, soft delete뿐이라 불가능한 --reset hard delete)에만 쓰고 demo-seed provider 표식으로 범위를 좁혔다. AI API 29회(Context 14 × 2 + 프리셋 1)로 잡은 근거는 비용이 아니라 429 — 판정이 분당 약 2건만 통과한다(실측). 429는 PROCESSING 잔류 + PROCESSING_EXPIRY_SEC 600초 때문에 Context 하나를 10분 동안 처리 불가로 만들므로, 시딩이 PENDING으로 되돌려 회수하는 루프를 넣었다(로컬에 없는 재스캔 대행). --reset 2회 완주로 재현 확인, 시연 3종(검색 4질의 전원 1위 · 피드 8장 소유자 4명 · Keyword 32행) 전부 통과. tools/e2e/ 4종 중 2종 정상(검색 수치가 I21과 소수점 넷째 자리까지 동일 — 임베딩은 결정적), run_equivalence는 429로 중단(회귀 아님). back 소관 발견 2건(탐색 슬롯이 최고점 후보를 흡수해 팔로우 소유자가 하단 배치 · Record 상세 keywords 고정 빈 배열)은 back 코드를 고치지 않고 계약 질문으로 올린다 (Jira 작업) 데모 시딩 리포트, tools/demo_seed/
2026-07-29 설정값을 비밀/공개로 가르고 공개 값의 정본을 코드로 옮겼다(P45). Embedding Profile 넷만 기본값이 없어 배포 설정에만 존재했고, 그 결과 Profile 교체가 git 이력도 리뷰도 남기지 않았다 — 기존 임베딩을 전부 조회 대상에서 빼는 결정인데 콘솔 편집 한 번으로 가능했다. 값 자체는 공용 계약 05 §7.1 표에 공개돼 있어 숨겨진 적도 없다. 주입을 덮어쓰기로 낮추면 원 논거("배포 설정 누락이 조용한 불일치가 된다")의 전제인 '누락'이 사라지고, 불일치 탐지는 기동 시 _profile_consistency 와 런타임 §3.1 이 그대로 맡는다. config/*.yaml 계층 도입은 설정 소스가 셋이 되어 기각했다. 공용 계약 개정(docs#27)이 선행이다 — 처음에 하위 명세만 고쳤다가 CONTRIBUTING"공용 계약과 충돌하면 소유 파트와 합의한 뒤 양쪽 문서를 갱신" 을 어긴 것을 발견하고 되돌렸다. 봉인 대상도 7종 → 3종으로 줄었다(정본이 이미지에 있으면 주입할 것이 없다) (Jira 작업) P45, model-profile, docs#27
2026-07-29 테스트 pgvector 를 운영·back 실제 버전에 digest 까지 정합화했다. conftest.py0.8.1-pg16 인 채로 남아 테스트가 운영과 다른 DB 에서 돌고 있었다 — back#31 이 compose.yaml0.8.5-pg16@sha256:1d53… 로 올렸고 운영 pgvector 도 이미 0.8.5(infra#41)였으므로 어긋난 쪽은 ai 였다. digest 를 back/compose.yaml 현행에서 재확인해 티켓 본문 값(2026-07-28 관측)과 동일함을 확인하고 고정했다. 태그만 올리지 않은 이유는 롤링·재태깅 시 같은 태그가 다른 이미지를 가리키기 때문이다. 실측으로 0.8.1-pg16 → pgvector ext 0.8.1·PostgreSQL 16.12, 0.8.5-pg16@digest → 0.8.5·16.14 로 두 층이 함께 움직였음을 확인했고 69 테스트 전부 통과해 동작 변화는 드러나지 않았다. 불일치 경고가 남아 있던 tests/README·implements 근거·P41·P43·s1-recovery 미결 행도 실제와 맞췄다. CI 러너에서 digest pull 이 network policy 상 통과하는지는 로컬에서 확인 불가 — PR CI 가 첫 검증이다 (Jira 작업) tests/README, implements, P43
2026-07-30 infra 공용 action pin 을 병합 후 정본으로 옮기고, 보존 규칙 위반을 복원하고, dev 2단 브랜치를 준비했다. pin 84458bf3infra#82 병합 전 브랜치 커밋이라 infra main 과 diverged 였다(ahead 6·behind 6, action.yml 내용도 달랐다) — 16bfae0d 로 옮기기 전에 대상 SHA 의 입력 계약(policy·revision)과 출력이 byte 단위로 같은지 직접 대조했고, 유일한 차이는 back OAuth CLIENT_ID 3종의 unset 방어 추가였다. workflow 와 contract test 의 ACTION 상수는 문자열이 정확히 일치해야 step 을 찾으므로 한쪽만 바꾸면 StopIteration 으로 깨진다(실측 RED 2건). 또 ai#39 가 handoff 리포트를 86줄→42줄로 덮어써 implements 보존 원칙("삭제하지 않고 상태 표시만 갱신")을 위반한 것을 복원했다 — 현재 내용을 지우지 않고 상태 노트를 상단에, 인프라와 합의한 설계 근거 4건(--raw 선택·중간 평문 YAML 회피·strict scope·기동 검증)을 하단 보존 절로 얹었다(+101/−1, 유일한 삭제는 갱신된 SHA 줄). dev 는 트리거만 넓히고 image publish 는 main 전용으로 남겼다infraai-image-update.yaml 이 source branch 를 main 으로 어서션하므로 넓히면 GitOps 반영이 거부된다 (Jira 작업) handoff 리포트, CONTRIBUTING, ai#40
2026-07-30 BD-39 가 "두 값이 같다" 로 명시한 명제를 사람의 눈에서 CI 로 옮겼다. Embedding Profile 정본을 backapplication.yml 리터럴로 두는 (a)안은 그대로 두고 대조 장치만 붙인다 — back#98 리뷰가 "그 명제를 지키는 장치가 현재 사람의 눈뿐"이라고 지적한 것에 대한 값싼 응답이다. 두 레포가 모두 public 이라 raw endpoint 를 무인증으로 읽는다. 토큰을 쓰지 않은 것은 편의가 아니라 경계다 — 주면 ai CI 가 타 레포 접근 권한을 상시로 들고 다닌다(대신 back 이 private 이 되면 조회 실패로 exit 1 이며 조용히 통과하지 않는다). 런타임 값이 아니라 선언된 리터럴을 비교한다 — 양쪽 다 환경변수 덮어쓰기를 허용하므로 프로세스 환경이 결과를 바꾸면 CI 가 무엇을 검증하는지 알 수 없게 된다. ai 쪽은 Settings() 를 인스턴스화하지 않고 필드 선언만 읽는다(인스턴스화는 환경변수와 profile 정합 검증을 함께 돌린다). 조회 실패도 exit 1 로 두었다. 실제 두 값이 같은 동안 이 잡은 늘 통과라 대조기의 탐지 능력 자체는 검증되지 않은 채 남으므로, 네트워크를 타지 않는 14 케이스로 그것을 못박고 CI 에서 실제 RED 를 관측했다(run 30507610160 — 리터럴을 v1v2 로만 어긋내 config.py 의 profile 정합 검증은 통과시킨 채 새 잡만 실패하게 만들었다). 리뷰가 함께 제안한 "기동 시 FastAPI 조회 대조"는 채택하지 않았다app/api/probe.py"credential·endpoint·profile 값을 어떤 분기에서도 싣지 않는다" 로 비노출을 명시하므로 Profile 노출 엔드포인트 신설은 그 결정의 개정이 되고, 이 티켓 범위 밖이다. BD-39 문서 자체도 개정하지 않았다. ai#40 줄 누락도 함께 메웠다 (Jira 작업) ai#40, back#98 리뷰, probe.py
2026-07-30 두 클라이언트가 HTTP 상태 코드를 정반대로 분류하던 것을 명세에 맞췄다. embedding>= 500 만 Transient 로 보아 429 한 번에 Context 가 영구 실패했고(failure-recovery §2.1 은 Transient), llm모든 non-200 을 Transient 로 보아 인증 실패가 재스캔 주기 5분마다 AI API 호출을 만들었다(§2.2 는 400·401·403 을 Permanent). 원인은 두 클라이언트가 각자 상태 코드 표를 들고 있었던 것이라, 매핑을 errors.pyclassify_http_status 한 곳으로 모았다. §2 가 "errors.py 에서 분류한다" 로 지목한 파일이다. 429>= 500 보다 먼저 판정해야 4xx 로 떨어지지 않으므로 그 순서를 회귀 테스트로 못박았다. 재시도(§3.1, 총 3회·0.5→1.0s·상한 4.0s·full jitter)는 client/retry.py 에 넣고 재시도 대상을 TransientError 한 종류로 뒀다 — §3.1 의 대상 목록이 §2.1 의 Transient 집합과 같으므로 재시도용 상태 코드 표를 두 번째로 만들지 않았다. 상태 코드 표가 두 곳에 있으면 서로 어긋나게 되고, 어긋난 결과가 위 두 오분류였다. 백오프 값을 Settings 로 열지 않은 것은 §3.2 의 상한("두 호출의 타임아웃 합 + 재시도 시간 < PROCESSING 만료 600s")에 묶인 값이기 때문이다 — env 로 열면 그 상한이 배포마다 달라진다. 현재 최악값 3 × (60 + 90) + 2 × 1.5 = 453s 이고 부등식 자체를 테스트가 지킨다. 대신 RetryPolicy 를 생성자 인자로 받아 테스트가 sleep·jitter 를 주입한다(실제로 잠드는 테스트를 만들지 않는다). 구조화 출력 위반은 SchemaViolationError(TransientError) 로 두어 재시도 중엔 Transient 로 동작하고 소진 시 judgePermanentError승격한다 — LLM 출력은 비결정론적이라 재요청에 성공 여지가 있고, 승격을 client 안에서 끝내야 service 가 보는 분류가 §2 의 두 종류로 유지된다(하위 타입인 채 새면 except TransientError 가 먼저 잡아 무한 재판정이 되므로 그것도 테스트로 막았다). Embedding 응답 형식 위반은 프로바이더가 같은 형식으로 답하므로 재시도 없이 즉시 Permanent 다. 결함의 근본 원인은 테스트였다fakes.pyraise_exc 가 어느 테스트에서도 쓰이지 않아(grep 0건) Transient/Permanent 파이프라인 경로가 한 번도 실행된 적이 없었다. 그 경로를 실제로 주입하자 티켓에 없던 결함이 하나 더 드러났다: keyword_servicePermanentError 핸들러가 아예 없어(context_processing 광범위 except 없음, context.pyBackgroundTasks.add_task) llm_client 만 고치면 401 이 BackgroundTasks 까지 새어 트레이스백만 남기고 단계는 PROCESSING 에 머문다 — 고치려던 무한 재시도가 경로만 바꿔 남는 구조였다(되돌려 RED 확인). embedding_service 는 이 결선을 이미 갖고 있어 비대칭이던 쪽만 맞췄고 상태 전이 규칙은 바꾸지 않았다. 로그 레벨도 §2.1 WARN·§2.2 ERROR 로 맞췄다(둘 다 INFO/WARNING 이어서 일시 장애가 묻히고 배포 설정 문제가 알림에 오르지 않았다). client HTTP 계층 테스트는 신설했고 이는 integration-tests §4.2("HTTP 레벨 목이 아니라 인터페이스 레벨 Fake")와 충돌이 아니다 — §4.2 는 파이프라인이 client 를 무엇으로 대체하는가 의 규칙이고, 인터페이스 Fake 는 client 를 통째로 대체하므로 _embed_batch 가 429 를 어떻게 분류하는지 볼 수 없다. docs/spec/ 은 고치지 않았다(§4.2 의 계층 구분 명문화는 후속 별건). 132 passed(착수 baseline 74), Docker 29.6.1 Testcontainers 전량. Circuit Breaker 와 타임아웃 60/90 재산정은 티켓 제외 범위 — 전자는 인증 실패 같은 전 서비스 영향 오류가 개별 Context FAILED 로 누적되는 문제를 남기고, 후자는 재시도 도입으로 최악값이 150s → 453s 가 된 것과 함께 별건이다 (Jira 작업) 구현 리포트, spec/failure-recovery.md, tests/README, ai#44
2026-07-30 dev 전환의 마지막 조각 — CONTRIBUTING.md 가 2단 구성으로 개정된 뒤 main 을 가리킨 채 남은 문서 6곳을 정합시켰다(docs/development/workflow.md 4·8·30·33·53행, P44 52행). image publishmain 기준 서술은 유지했다infraai-image-update.yamltest "$SOURCE_BRANCH" = main 으로 어서션하므로 이것까지 dev 로 바꾸면 GitOps 반영이 끊긴다. workflow.md 는 이제 "dev 에 병합하는 실행 순서" 이므로, 그 문서만 읽는 사람이 main 이 언제 무엇을 받는지 모르게 되는 것을 막기 위해 §5 에 릴리스 병합(devmain)과 publish 가 거기서 일어난다는 서술을 한 줄 넣었다. P44상태: Accepted 인 결정 문서라 표 값만 조용히 바꾸지 않고, 작성 시점(2026-07-28)에는 main 단일 구성이었고 2단 전환이 -154·-158 에서 이뤄졌다는 각주를 함께 남겼다 — implements/ 와 달리 proposals/ 에 삭제 금지 원칙은 없지만, Accepted 결정문을 이력 없이 고치면 무엇이 언제 정해졌는지가 사라진다. ai-ci / checkai-ci / embedding profile parity 둘 다 required 로 확인했으나(main·dev 동일) CONTRIBUTING.md 병합 조건 절은 여전히 ai-ci / check 하나만 적고 있다 — 정본 재개정은 이 티켓 범위 밖이라 문서를 정본에 맞춰 두고 갭만 올린다 (Jira 작업) workflow, P44, CONTRIBUTING
2026-07-30 required status checks 가 둘인데 문서가 하나만 적던 것을 정정했다. ai#42 병합 후 main·dev 양쪽 protection 에 ai-ci / embedding profile parity 가 추가됐으나 정본이 갱신되지 않아, 낡은 값이 CONTRIBUTING.mdworkflow.mdP44 로 퍼졌다 — 직전 작업(위 줄)에서 갭을 관측했지만 정본 재개정이 범위 밖이라 하위 문서를 낡은 정본에 맞춰 둔 상태였다. 이번에 정본을 먼저 고치고 하위를 거기에 맞췄다. 실제 설정을 API 로 직접 대조한 결과 main·dev완전히 동일했다(strict·검사 둘·미해결 대화 차단·관리자 포함·승인 0). 그래서 검사 목록만 고치지 않고 관리자 포함 보호main 전용으로 적던 서술도 함께 정정했다 — 이것도 틀린 값이었고, 원인은 같은 조건을 두 bullet 에 중복 기재해 한쪽만 낡을 수 있는 구조였다. 조건은 공통으로 한 번만 적고 bullet 은 "무엇을 병합하는가" 차이만 남겼다. P44"문서와 실제 GitHub 설정 불일치" 완화가 실제로 실패한 사례이므로("API 로 재조회" 는 설정을 읽는 것까지만 다루고 문서를 고치는 주체를 정하지 않았다) 완화 자체를 "설정을 바꾼 사람이 같은 작업에서 정본을 갱신한다" 로 강화하고 발생 사실을 각주로 남겼다. 정본에는 검사 이름이 protection 과 문자열까지 일치해야 하며 개편 시 이 절을 먼저 고친다는 순서 규칙을 박아 두었다. 위 줄은 그 시점의 사실 기록이라 고치지 않고 새 줄을 더했다 — WORKLOG 가 merge=union 이라 기존 줄 수정은 중복 위험이 있다(직전 작업에서 문서화한 주의의 첫 적용) (Jira 작업) CONTRIBUTING, workflow, P44
2026-07-30 app coverage 를 비차단 측정에서 병합 차단 게이트로 전환했다. 티켓 기준선(07-28, 52 tests · line 76.95% · branch 62.5%)이 낡아 착수 시 재측정했다 — 그 사이 -121 이 58 테스트를 더해 b45aa93 시점 실측은 146 tests · line 88.80% · branch 82.08% 였고, 티켓이 지목한 미검증 4곳 중 둘은 이미 -121 이 덮은 뒤였다. 두 지표 모두 이미 80% 를 넘겨 게이트만 켜도 통과했지만 그러지 않았다 — branch 여유가 2 포인트뿐이라 다음 기능 PR 이 분기를 몇 개 추가하는 것만으로 무관한 이유로 실패한다. 남은 공백(load_presets.py 0%, main.py 58%, 재측정으로 드러난 gms_roundtrip.py 76%)을 계약·실패 경로로 메워 181 tests · line 99.74% · branch 98.11% 로 올렸다. --cov-fail-under 를 쓰지 않았다--cov-branch 를 켜면 그 값은 statement 와 branch 를 합산한 하나의 비율이라(기준선에서 합산 88% · branch 82%) statement 759 개가 branch 106 개를 압도해 branch 가 60% 대로 떨어져도 통과한다. 완료 조건이 "각각" 인 이상 합산 게이트는 조건을 검사하지 못하므로 tools/check_coverage_gate.py 가 둘을 나눠 판정한다. 임계값은 스크립트 상수이며 CLI 로 덮을 수 없다(덮을 수 있으면 CI 가 조용히 낮은 값을 넘겨 무력화할 수 있다). --cov-branch 없는 리포트는 branch 키 자체가 없어(실측) 분기 검사가 공짜로 통과하므로 판정 불가로 끊는다 — "측정하지 못했다" 는 통과가 아니다. 통과만 확인하면 아무것도 강제하지 않는 게이트를 놓치므로 RED 4 종을 실측했다: 테스트 제거(line 42.82%·branch 34.91%), 임계값 99.9 상향(되돌림), --cov-branch 누락, 리포트 부재 — 전부 exit 1. 임계값 하향을 막는 테스트도 함께 실패하는 것을 확인했다. 신설한 두 계층은 요청 경로 밖이라 파이프라인·API 테스트로는 한 줄도 실행되지 않던 곳이다 — test_api.py 가 lifespan 을 우회하고 app.state 에 Fake 를 직접 꽂기 때문이다. lifespan 테스트는 진짜 클라이언트를 조립하는지를 단언하므로 Fake 로 바꾸지 않았다(생성자는 IO 를 하지 않아 실호출 금지와 충돌하지 않고, Fake 로 바꾸면 그 단언이 사라진다). if __name__ == "__main__" 아래는 runpy 로 검증하는데, 새 네임스페이스라 캐시된 모듈 패치가 보이지 않아 원본 클라이언트 모듈의 속성을 갈아 끼워야 한다(함정을 tests/README 에 기록). 제외로 수치를 맞추지 않았다pragma·omit 없이 2 line · 2 branch 를 미달로 남겼다. 둘 다 FOR UPDATE 로 잠근 직후의 재검사라 같은 트랜잭션에서 도달 불가지만, 도달 불가가 구조에 딸린 성질이라 저장 로직이 잠금 밖으로 나가면 도달 가능해진다 — pragma 를 붙이면 그때 아무도 모르고, 미달로 두면 리포트가 계속 그 줄을 가리킨다(여유 18 포인트라 비용이 없다). 함께 integration-tests.md §4.2 에 계층 구분을 명문화했다(-121 후속) — §4.2 가 "HTTP 레벨 목이 아니라 인터페이스 레벨 Fake" 라고만 적어 client 단위 테스트의 HTTP mock 이 위반인지가 -121두 번 질문으로 올라왔고, 판정은 충돌이 아니라 층이 다름이었다. 경계를 한 문장으로 고정했다: app/client/ 밖의 코드는 HTTP 를 몰라야 하고 그 코드를 검증하는 테스트도 HTTP 를 몰라야 한다. -121docs/spec/ 을 못 고친 것은 그 작업이 구현을 계약에 맞추는 것이었기 때문이고, 이번은 계약 자체의 공백을 메우는 것이라 허용됐다 (Jira 작업) 게이트 리포트, spec/integration-tests.md, CONTRIBUTING, tests/README
2026-07-30 가공 데모 14건에 실사용자 기록 23건을 더해 37건으로 전체 통합을 검증했다. 검색 1위 일치 10/12(가공 4/4·실데이터 6/8), 피드·Keyword PASS. 시딩 15분 8초·회수 0회로 --pace 25가 AI API 쿼터 안에 들어감을 재확인했다. 실패 2건의 원인이 갈렸다 — 하나는 축약어(「피맥」↔「피자에 맥주」), 하나는 임베딩 입력이 Context 본문뿐이라 장소명이 검색에 안 걸리는 것으로 후자는 설계 관측이다. 응답에 실려 오던 AI API 토큰을 두 클라이언트가 전부 버리고 있어 PINLOG_TOKEN_LOG 게이트 계측을 신설했고, 판정이 임베딩의 17배(839.6 대 37.7)를 쓰며 그 대부분이 후보 Preset 27개를 매번 싣는 prompt 임을 실측했다 (Jira 작업) 실데이터 E2E · T27·T28
2026-07-30 시딩이 느렸던 것은 AI API 가 아니라 우리 --pace 25 였다. 재측정에서 같은 코드가 분당 30건 이상을 통과했고(간격 1s 15/15 · 동시 10건 1.7초), 같은 데이터 37건이 --pace 142초에 완주했다 — 15분 8초에서 21.6배. 결과와 토큰은 같았고 검색 정확도도 2회차 10/12 로 재현됐다. T27 이 07-29 관측을 상수로 적은 것을 시점·경로 의존으로 정정하고 기본값을 1 로 낮췄다. 벤더 비교에서 429 가 Gemini 경로에만 나는 것을 확인해(OpenAI·Anthropic 12/12) 폴백을 -175 로 발주했다. 함께 보고서 두 오류를 고쳤다 — 판정 후보는 27개가 아니라 top-10 이고(프리셋 총수와 혼동), 데이터가 원본·계약 자리채움·시연 구성 세 층인데 그 구분 없이 「실데이터」로 뭉뚱그렸다 (Jira 작업) 실데이터 E2E · T27 정정
2026-07-31 임베딩 502 가 검색 500 이 되던 운영 버그(ai#69)를 고쳤다. 분류도 재시도도 이미 맞았다-121classify_http_status502→TransientError 를 확정했고 test_client_retry.py 가 상태 코드별로 단언한다. 비어 있던 것은 그 예외가 응답이 되는 지점이었고, app/main.py 의 핸들러는 ProfileMismatchError(422) 하나뿐이라 분류된 나머지가 uvicorn 까지 올라가 500 이 됐다. 클라이언트 테스트는 예외가 던져지는 것까지 보고 API 테스트는 Fake 를 꽂아 성공 형식만 봐서, 두 파일 사이에 계층 하나가 통째로 비어 있는데 각자는 자기 범위에서 통과하고 있었다. TransientError→503 · PermanentError→502 로 갈랐고, 502 를 고른 근거는 500 을 비워 두는 것이다 — 분류된 실패까지 500 이면 "AI 가 깨졌나 게이트웨이가 깨졌나" 를 가르려던 목적이 절반만 남는다. 티켓이 열어 둔 "back 이 재시도해도 소용없다는 신호" 논거는 쓰지 않았다: AiSearchClient 는 애초에 검색을 재시도하지 않는다(사용자 요청 경로). back 이 500 과 503 을 구분하지 않는다는 사실을 확인했다translate()422+serverProfile401/403모든 상태를 AiSearchException.unavailable()(→503 SEARCH_UNAVAILABLE)로 묶으므로 사용자 화면은 이미 같고, 이 변경이 바꾸는 것은 화면이 아니라 관측이다. ai#69 두 번째 답변의 "AI 가 503 을 주면 back 이 전달한다" 는 맞지만 500 도 이미 그렇게 전달되고 있다는 사실이 빠져 있었다. 그래서 back 변경은 하지 않고 별건으로 남긴다. 응답 본문은 고정 문구 한 줄이다 — back 이 본문을 읽지 않고(파싱하는 것은 serverProfile 뿐), 업스트림 상태 코드는 -197 계측이 이미 남기며, 예외 메시지를 그대로 실으면 게이트웨이 응답 200자(resp.text[:200])가 응답으로 새기 때문이다. 핸들러 부재는 500 만 낸 게 아니었다 — 대조군 로그에서 예외가 uvicorn 까지 올라가며 트레이스백에 업스트림 본문이 그대로 찍히는 것을 봤고(스텁 401 의 키 힌트가 노출됐다), probe.py·§2.4 원칙 4 위반이라 핸들러가 그 누출도 막는다. 검증은 핸들러를 넣기 전에 RED 14 건을 확인했고(실패 형태가 「500 이 나왔다」가 아니라 예외가 응답 계층으로 샜다 였다 — 운영에서 uvicorn 이 500 을 만드는 바로 그 지점), 넣은 뒤 18 건 GREEN · 전체 309 건 통과다. 테스트만으로는 「핸들러를 넣었다」에 가까워 로컬 AI API 스텁으로 실제 502 를 만들었다: 같은 스텁에 물린 uvicorn 두 대에서 origin/dev(a5e1142)는 502·401 둘 다 HTTP 500, 이 브랜치는 503·502 였고 정상 응답은 200 으로 실데이터 5건을 돌려줬다. 계약 테스트는 Fake client 를 쓰지 않는다raise_exc 주입은 분류 경로를 건너뛰어 classify_http_status 가 바뀌어도 통과하며, 그 모양이 ai#69 를 놓친 구멍이다. 명세도 함께 메웠다(failure-recovery.md §2.5 · personal-search.md §6.2) — §2.1·§2.2 는 Context 처리 경로의 상태 반영만 정하고 API 층 변환을 말하지 않아서, 코드만 고치면 같은 공백이 남는다. DB 실패는 여전히 500 이다 — §2.1 이 DB 연결 실패를 일시 오류로 두지만 SearchService 가 asyncpg 예외를 분류하지 않는다. 범위 밖으로 남기고 후속 후보로 올린다 (Jira 작업) 오류 응답 계약, failure-recovery §2.5, personal-search §6.2, T43~T45, ai#69
2026-07-31 임베딩 개선 축 둘을 교차시켜 넷을 실경로로 쟀다(입력에 장소명 결합 × small/large). 1위 일치는 A 10 · B 9 · C 10 · D 10 / 12 이고 top-3 는 넷 다 12/12 다 — 합계로는 어느 조건도 기준선을 넘지 못한다. 그런데 합계가 같은 A·C·D 가 서로 다른 질의에서 실패한다: 8번(공원)은 large 가 고치고(C·D) 장소명 결합과 무관하며, 7번(축약어 「피맥」)은 둘을 겹쳤을 때만(D) 통과하고, 1번은 D 에서만 깨지고, 6번은 A 에서 +0.0087 차이로 통과해 우연에 가깝다. -174 가 8번의 원인으로 지목한 「장소명이 임베딩에 없다」는 진단이 틀렸다 — 맞았다면 입력에 장소명을 넣은 B 가 고쳤어야 하는데 B 는 못 고쳤고 장소명을 넣지 않은 C 가 고쳤다. 원인은 어휘 부재가 아니라 small 이 「그네팟」·「산책하면서 머물다」를 「공원」과 잇지 못한 것으로 보인다. 장소명 결합은 이득과 손해가 함께 있다. 저장 입력에만 붙고 질의에는 붙지 않는 비대칭이 유사도 전반을 끌어내린다(1번 0.5021→0.3344). 비용 축은 저장 하나뿐이다: 벡터가 정확히 2배(6,148→12,292 B)이고 토큰은 조건 간 차이가 없다(임베딩 토큰은 입력 텍스트가 정하므로 모델 크기와 무관), 시딩 소요도 42.5~44.2초로 AI API 왕복이 지배한다. vector(3072) 이 동작하는 것은 벡터 인덱스가 없기 때문이며(37행 순차 스캔) 색인이 필요해지면 pgvector 의 2000차원 상한이 되살아난다. 채택 판단은 하지 않는다 — 12건 표본의 1건 차이이고 세 조건이 동률이며 기대값을 우리가 작성했다(-174 §3.3). 측정 전제로 두 가지를 고쳤다: seed.pysocial_account.email 을 채우지 않아 back V6__social_account_email_not_null.sql 에 걸려 back 이 기동조차 못 하던 것(시연 도구가 현행 스키마와 어긋나 있었다. {key}@demo-seed.invalid — RFC 2606 예약 TLD 라 실재 주소로 오인되지 않는다)과, --resetdemo-seed 소유만 지워 남은 고아 임베딩 8건이 저장 비용 평균을 오염시키던 것(하네스가 조건 profile 로 한정해 세게 했다). 로컬 DB 는 복구했다alter_dim.py --to 1536 · 프리셋 실배포 profile 재적재 · 데모 37건 재시딩까지 실행해 grid-* 행이 하나도 남지 않은 것을 확인했다. Flyway 마이그레이션은 만들지 않았다(측정과 채택은 별건) (Jira 작업) 4조건 측정, tools/emb_grid/, 실데이터 E2E
2026-07-31 시연 도구 결함 3건을 값이 아니라 「조용함」 쪽에서 고쳤다. 셋 다 -191 이 값은 이미 고쳤고(email 채움·고아 8행 삭제) 여기서 만든 것은 재발 경로다. tools/demo_seed/preflight.py--reset 보다 먼저 돌고 걸리면 아무것도 지우지 않은 채 exit 2 로 끝난다 — 결함 3 과 T28둘 다 reset 이 지운 뒤에 실패해 데이터를 잃었기 때문에 순서가 방어의 절반이다. 결함 1 의 방어 대상을 「NOT NULL 제약」이 아니라 「우리가 값을 주지 않는 컬럼」으로 잡았다emailV4 에서 nullable 로 태어났고 그때는 아무 오류도 나지 않으며 V6 가 제약을 걸 때는 이미 늦다. WRITE_CONTRACT 가 시딩이 직접 INSERT 하는 두 테이블의 모든 컬럼SEED/DB/NULL 로 선언하고 미선언 컬럼이 나타나면 멈춘다. 이 방향이면 back 이 언제 무엇을 걸지 알 필요가 없어 back 이 우리 CI 가 아니라는 현실에서 작동한다. 티켓 후보 중 「기동 스모크」는 넣지 않았다(그 시점엔 DB 가 이미 오염돼 예방이 아니라 관측이다), 「트랜잭션 경계」도 아니다(INSERT 는 정상 커밋됐고 며칠 뒤 다른 프로세스가 죽었다). 결함 2 는 「지우지 않는다」로 판단했다tools/e2e/ 검증 데이터는 의도적으로 core 대응 행이 없어(README 명시) 고아 정의에 전부 걸리므로 자동 삭제는 남의 하네스를 깨뜨린다. 세어서 보여주고 --prune-orphans 로 사람이 정한다. 다만 reset()ai.context_keyword_analysis 를 지우지 않던 것은 논쟁이 아니라 누락이라 그냥 고쳤다(259행 중 222행이 고아였고 FK 가 없어 DB 도 알려주지 않았다). 목록을 ORPHAN_TABLES 하나로 묶어 빠뜨리면 집계가 먼저 고아로 보고하게 했다. 결함 3 은 묶는 것과 드러내는 것을 둘 다shared_root()--git-common-dir 로 키 경로를 메인 워킹트리에 고정하고, 인증 프로브가 back 이 그 키를 실제로 받아들이는지 한 번 통과시켜 본다. 경로 고정은 흔한 원인 하나만 없애고 back 에 주입된 키가 다르면 여전히 401 이라 프로브가 본체다. 셋 다 일부러 어긋내 RED 를 봤다(-156 과 같은 이유): 판정 함수에 return [] 뮤턴트를 넣어 5 케이스 RED · 별도 guardprobe DB 에 back 마이그레이션 9 개를 적용하고 ADD COLUMN nickname 으로 nullable 추가 시점에 걸리는 것을 실측 · 키를 어긋내 HTTP 401 BLOCK · :5433 에서 테이블 없음 BLOCK. 실 DB 실행이 오탐 하나를 잡았다GENERATED ALWAYS AS IDENTITYcolumn_default 가 NULL 이라 id 가 "NOT NULL 인데 기본값 없음" 으로 걸렸고 단위 테스트는 그 쿼리를 타지 않아 못 봤다(is_identity·is_generated 포함으로 수정). 조사 중 로컬 pgvector 가 둘이고 .envDATABASE_URL 이 시연 정본(:15432)이 아니라 07-27 잔재(:5433)를 가리키는 것을 발견해 preflight 첫 줄에 접속 대상을 찍게 했다 — 기본값 자체는 -174 절차·타 세션 습관과 얽혀 이 티켓에서 바꾸지 않았다. back 은 §7 절차로 실기동했고 첫 기동은 로컬 jar 이 07-30 빌드라 V6 를 담지 않아 Flyway 가 거부해서 bootJar 리빌드 후 정상 기동했다. 254 passed · ruff check . 통과 (Jira 작업) 구현 리포트, tools/demo_seed/, 임베딩 4조건
2026-07-31 dev 의 세 gate 가 열렸는데 무엇이 실패하는지 볼 수단이 없던 것을 메웠다(-197 감사 §4-c 의 관측 3종). AI API 호출 1회를 감싸 벤더·모델·상태 코드·결과 분류·소요 시간을 남기고, 60초 창 집계를 한 줄로 낸다. _usage.py 와 합치지 않았다 — 그쪽은 PINLOG_TOKEN_LOG 게이트가 있고 200 응답을 파싱한 뒤에 불려 실패한 호출을 한 줄도 남기지 않는다. 실패율의 분자가 애초에 거기 없고 dev 배포에는 그 env 가 없어 분모도 없다. 로그로 합치면 반대로 토큰 JSONL 이 실패 행과 섞여 -174 집계가 깨진다. 재선점은 쿼리를 갈랐다try_start 가 신규 시작과 만료 재선점에 같은 rowcount 1 을 주는데, 호출부가 미리 읽은 값으로 가르면 keyword 단계는 근거가 아예 없고(try_start 전에 상태를 안 읽는다) embedding 단계는 경합 창 때문에 거짓 양성이 섞인다. 재선점은 드물지만 「앞선 처리가 600s 안에 못 끝났다」는 신호라 거짓 양성이 섞이면 신호를 못 믿는다. CTE 로 UPDATE 직전 상태를 같은 문장에서 읽어 원자성을 지켰다(추가 조회가 아니므로 state-machine §3.2 와 충돌하지 않는다 — 그 규칙은 0 의 이유를 묻지 말라는 것이고 이쪽은 1 이 무엇인지를 묻는다). 레벨은 성공 DEBUG · 실패 WARNING · 창 집계 INFO · 재선점 WARNING 이다 — 성공까지 INFO 면 정상 트래픽이 실패 행을 덮고, 성공을 안 세면 분모가 없다. 집계는 타이머가 아니라 다음 호출이 밀어내므로 유휴 시 조용하고, 마지막 창은 lifespan finallyflush() 가 낸다. 계측을 붙이다 httpx 가 요청마다 INFO 로 전체 URL 을 남기는 것을 발견했다probe.py 의 endpoint 미노출 기준을 어기면서 성공 호출까지 INFO 라 새 설계를 무의미하게 만들고 있었다. caplog 를 로거 이름으로 걸러 읽는 테스트가 이것을 통과시켰고, 실제 실행 출력을 눈으로 보고서야 드러났다(자기 로거만 보면 옆에서 새는 것을 못 본다). configure_logging 이 httpx 를 WARNING 으로 올리고, 로거를 가리지 않고 전부 읽는 테스트를 따로 뒀다. 값 노출 기준은 벤더·모델은 싣고(공개 설정 P45, 어느 경로가 막혔는가가 곧 원인) credential·endpoint·요청/응답 본문은 안 싣는다 — 요청 본문에는 사용자 Context 원문이 있다. 전송 실패에서 예외 메시지 대신 타입 이름을 쓰는 것도 httpx 가 메시지에 URL 을 넣기 때문이다. /metrics 는 만들지 않았다(infra ai-serving.md 검증 순서 7 이 prod 승격 전 별도 승인으로 이관). 기존 resp.text[:200] 노출은 진단에 필요해 남기고 별도 판단 항목으로 올린다 (Jira 작업) AI API 호출 관측, failure-recovery §2.4, state-machine §3.2
2026-07-31 개인 검색에 두 컷(τ_abs=0.30 절대 하한 · r=0.60 1위 대비 상대 하한)을 넣고 limit 기본값을 공용 계약 08 §6.1 의 size 기본값과 같은 20 으로 맞췄다(back 이 sizeOrDefault() 로 항상 명시해 보내 드러나지 않은 채 어긋나 있었다). personal-search.md §6 의 「컷오프를 적용하지 않는다」를 뒤집는다 — 그 근거는 무관 질의 1건의 실측(관련 top-1 최소 0.5263 vs 무관 top-1 최대 0.3143, 간격 +0.2120)이었는데, 무관 질의를 질의 5종 × 소유자 3명 = 15건으로 늘리자 그 간격이 -0.0176 으로 뒤집혔다(「치과 임플란트 상담 받을 곳」 → 연남칼국수 0.3819 가 기대 정답 최솟값 0.3642 를 넘는다). 즉 어떤 τ_abs 도 무관 질의를 전부 무노출로 만들면서 정답을 전부 살릴 수 없고, 동시에 「그러므로 컷을 걸지 말자」도 성립하지 않는다 — 컷이 없으면 무관 질의에 보유 기록 전량(소유자별 6·11·17건, 15질의 합 170행)이 반환된다. 티켓이 채택한 조합을 측정이 지지했다. 검증 질의 12건만 재면 r 이 압도적으로 보이지만(r=0.80 꼬리 97.4% 제거, τ_abs 는 어떤 값과 조합해도 결과 동일) r 은 정의상 1위를 언제나 남겨 무관 질의를 0.40~0.90 전 구간에서 15건 중 0건도 무노출로 만들지 못한다. τ_abs 는 반대로 같은 안전 마진에서 꼬리 제거가 약하다(상한의 83% 에서 62.3%, r 은 상한의 75% 에서 73.7%). 정답이 있는 질의만 재면 컷의 절반이 안 보인다는 것이 이 측정의 방법론적 결론이다(T40). 채택값에서 정답 누락 0/12 · 빈 결과 0/12 · 꼬리 76.3%(비관)·71.5%(낙관) 제거이고, 무관 질의는 15건 중 11건 무노출(11/15)이다. 안전 상한(τ_abs 0.36 · r 0.80)에 붙이지 않고 17%·25% 마진을 뒀다 — 두 축의 상한을 같은 데이터점 하나(「친구들이랑 피자에 맥주 마신 곳」의 정답이 3위 0.3642·r=0.807, -174·-191 네 조건에서 계속 실패한 축약어 「피맥」 건)가 정하고, 여기에 배치 구성이 바뀌면 같은 텍스트의 유사도가 10⁻⁴ 규모로 흔들리는 것(0.5264→0.5258, T42)이 겹치는데 상한과 정답 최솟값의 거리가 0.0042 였다. 컷은 LIMIT 뒤에서 건다 — 유사도 하위만 자르므로 WHERE 와 결과가 같고(단조), 그렇다면 이미 고정된 §4 Query 를 건드리지 않는 쪽이 낫다. r 의 기준은 컷 전 1위다(살아남은 것의 1위로 재계산하면 자기충족 컷이 된다). 하네스 tools/search_cut/-210 의 구조를 따라 질의를 한 번 임베딩해 굳히고 격자를 오프라인으로 훑는다(AI API 임베딩 배치 1회) — 다만 -210 의 재구성이 근사였던 것(후보가 줄면 LLM 판정이 뒤집힌다)과 달리 검색 경로에는 LLM 이 없어 정확하고, 그래서 대조군 없이 정확 일치를 요구했다(이 브랜치 코드로 띄운 :8002 에 27건 실호출 → 27/27 일치). 검증 스크립트는 컷 규칙을 구현에서 import 하지 않고 다시 적는다(하면 구현이 명세와 달라도 둘이 함께 틀린다). .search/matrix.json커밋했다 — 다시 뜨려면 AI API 를 부르고 tau_grid 의 것과 달리 Context 본문을 담지 않는다(장소명까지). 라벨은 plausible넉넉히 잡아 컷의 이득을 과대평가하지 않는 방향으로 뒀다. limit 을 20 으로 올리는 것 자체는 꼬리를 늘리므로(반환 104→142행) 컷과 분리해 머지하면 안 된다. 후속: 소유자별 편차를 단일 τ_abs 가 모른다(같은 무관 질의가 6 Record 소유자에게 0.2686, 17 Record 소유자에게 0.3819) · 정답/무관 겹침은 컷이 아니라 임베딩 품질의 문제다. 291 passed · line 99.71% · branch 98.12% · ruff check . 통과 (Jira 작업) 컷 측정, spec/personal-search.md §6.1, T40~T42, ai#70
2026-07-31 AI API 오류 응답 본문이 예외 메시지를 타고 로그로 새던 경로를 원천에서 막았다(-205). 티켓은 「분류 밖 예외 → 트레이스백」 하나를 남은 구멍으로 봤지만 실제 경로는 여섯이고 그중 다섯이 분류가 정상 동작하는 평상시 경로다 — retry.py 가 재시도 1회마다, 두 service 가 일시·영구 오류마다 예외 객체를 %s 로 찍는다. failure-recovery.md §2.4 원칙 4 는 응답 본문을 "어느 레벨에서도 남기지 않습니다" 라고 적고 있었으나 코드는 그때도 남기고 있었다(§2.6 으로 갈라 정정). 실제 AI API 로 네 경로에 오류 19건을 넣어 본문을 실측했다 — 자격 증명은 한 건도 에코되지 않고(401 은 게이트웨이 고정 문구, 벤더가 답하기 전에 끊는다) endpoint 는 맨 호스트로 실리며, OpenAI 는 요청 값을 앞뒤 3자만 남기고 잘라 되돌린다(Invalid value: 'PIN...def' — 완전 일치 검사를 빠져나가 「안 샌다」로 오판할 뻔했다, T61). 그래서 규칙의 근거를 「관측된 것을 지운다」가 아니라 **「되돌아올 수 있는 자리를 막는다」**로 잡았다. app/core/redact.py 가 자격 증명(sk-·AIza·JWT·Bearer·key=value)을 endpoint(URL·맨 호스트)보다 먼저 지우고, 마스킹이 절단보다 먼저다(순서를 뒤집으면 200자 경계에 걸친 키의 앞부분이 남는다). 본문 200자는 지우지 않는다 — 실측한 벤더 400 본문에 마스킹 대상이 한 글자도 없었고 전부 진단 문구였다. 막는 지점을 로그 호출부가 아니라 client 로 잡은 것이 설계의 전부다(트레이스백 경로에는 애초에 호출부가 없다). tests/test_log_redaction.py 18건이 AST 로 단일 원천을 지킨다. pytest 404 · line 99.83% / branch 98.99% (-223 병합 후 재검증) implements, failure-recovery §2.6, troubleshooting (T61~T63)
2026-07-31 판정 n회 다수결 — 구현 + 실측 후 n=1 유지 결론 (Jira 작업) 판정을 PINLOG_JUDGE_VOTE_N 회 불러 엄격 다수결(votes*2 > n)로 접는 경로를 넣고, 기본값 1 이 현행과 정확히 같음을 실데이터로 고정했다(회차 30개를 n=1 로 접은 결과 원본과 30/30 일치). 분모를 성공 수가 아니라 n 으로 고정한 것이 설계의 핵심 — 낮추면 n=3 에서 1회만 성공했을 때 그 1회가 곧 다수결이 되어 「다수결을 켠 채 n=1 을 실행」하는 상태가 되고, 정족수 미달을 「선택 0건 정상 완료」로 저장하면 판정 실패가 성공으로 기록된다(T54). 짝수 n 은 기동 차단 — 동점 때문이 아니라 바로 아래 홀수에 지배당하기 때문이다(n=4 는 3표·n=3 은 2표인데 호출은 33% 더 든다). 측정은 새로 부르지 않고 접었다 — n회 다수결 1회분과 독립 회차 n개의 다수결이 같은 확률변수이므로 회차 30개(1,260호출)로 2,940호출어치 조건 셋을 얻었고(-210 이 유사도 행렬로 τ 를 재구성한 것과 같은 수법), 다수결 규칙은 서비스 코드를 그대로 부른다. run_live.pyKeywordService._judge_n 실호출로 대조 — 전 지표 범위 겹침, Context당 호출 정확히 3.0. 다수결은 설계대로 작동했다: 비결정성 24%→17%→12%, 흔들리던 오분류 13종 완전 제거, 라벨없음 0.80→0.00, 정상 판정을 오히려 늘려 교환비가 이 프로젝트 최초로 음수(-210 τ 는 1.22 로 기각됐다). 그런데 오분류 행은 안 준다 — 10.13→9.40→9.17, 범위 겹침, p=0.365/0.210 으로 사전·보강 기준 모두 미달. 이유가 §4 다: 다수결은 소수의견을 지우는 장치라 다수의견은 반대로 굳힌다 — 지운 13종이 각 0.030.37행으로 드물었고(-1.7행) 동시에 7097%이던 5종을 100%로 굳혀(+0.8행) 잔액이 노이즈 크기로 남는다. 100% 생존한 7종(274·289 DRINK·265 WALK·284 WITH_FAMILY·275/290 VIEW_GOOD·266 TRENDY)은 -219 가 프롬프트로 못 움직인 것과 같은 목록이다 — -210(후보 선정)·-219(판정 문구)·-223(판정 분산)이 서로 다른 층에서 같은 곳을 가리켰고, 남은 것은 후보의 정의(프리셋 description, back#136)로 AI 파트 소관이 아니다. fit 0건 Context 8.00 — 세 티켓 연속 불변(다수결이 한 일은 줄인 것이 아니라 표준편차를 0 으로 만든 것). 비용 실측: 호출 35배, Context당 지연 1.57→2.01→2.60s(동시 호출이라 n배 아님), 최악 지연은 n 에 비례하지 않는다(n=1 3.94s > n=3 3.70s — 꼬리는 AI API 쪽이다). 개선 폭당 호출 115175회/오분류 1행. back#136 이 7종을 고치면 남는 오분류가 전부 흔들림이 되므로 그때 다시 재야 한다 — 순서가 중요하다. AI API 2,142호출·실패 0·전량 gpt-4o-mini. 386 passed · line 99.83% · branch 98.98% (origin/dev 병합 후. 이 브랜치 단독은 338) · ruff check . 통과
2026-07-31 문서 색인 정합을 CI 로 옮겼다(티켓 없음). 07-31 하루에 색인 사고가 다섯 번 났고 대응이 다섯 번 다 「문서에 문장 추가」였다 — 네 번째(T53 중복)는 첫 번째의 대응(T48 「PR 직전에 다시 확인하라」)을 그대로 따랐는데도 났다. 따를 수 없는 규칙이어서가 아니라 「내가 세는 35분 사이에 남이 병합했는가」 를 사람이 알 방법이 없기 때문이다. 그중 셀 수 있는 것ai-ci / check 스텝으로 옮겼다 — ①전수 표의 T##·I## 중복 ②파일 표 ↔ 파일 시스템의 고아·누락 ③전수 표에만 있고 파일 표에 없는 문서(사고 5, -205 가 발견). 검사를 먼저 만들어 dev 에 돌리고, 위반을 별도 커밋으로 정리한 뒤에 켰다(순서가 반대였으면 무관한 PR 이 전부 막힌다). 착수 시점 dev 에 고아 3건이 있었다 — -197·-223 은 전수 표에만 링크가 있었고 -219 는 어느 표에도 없었다(셋 다 WORKLOG 에는 있다). 일부러 통과시키는 것을 잡는 것만큼 고정했다: 결번(T9·T10 은 back 레포)과 두 표의 내용 불일치, 그리고 파일 표에만 있고 전수 표가 안 가리키는 문서. 마지막 것은 켤 수 없다 — 트러블슈팅 전수 표는 설계상 문서를 안 가리켜(문서 링크 0종) 정상 문서 15건이, 구현 쪽은 번호 없는 문서 7건이 위반으로 나온다. 연속성을 검사하면 없던 규칙이 생기고, 일치를 검사하면 표 이중화라는 문제를 규칙으로 승격시킨다. 재현은 실제 docs/ 사본에 사고 1·2·3 을 그대로 심어 확인했고, 검사가 내 문서 두 개의 색인 누락도 잡았다(T64·T65·I35 를 정하기 전에 돌렸다). 메시지는 「중복입니다」로 끝내지 않고 다음 빈 번호와 붙여 넣을 표 행을 준다. 셸이 아니라 Python 인 것은 pytest 가 같은 함수를 불러 CI 와 로컬이 같은 코드를 쓰기 위함이고, 새 잡이 아니라 check 안의 스텝인 것은 필수 상태 검사가 두 이름뿐이라 잡을 늘리면 실패해도 병합을 못 막기 때문이다. 표 이중화 조사 결과 줄일 수 없다 — 트러블슈팅 전수 표는 문서 링크가 0종이고(T↔문서 매핑의 유일한 출처가 파일 표다) 구현 전수 표는 파일 표 24개 중 17개만 덮으며 13행은 애초에 문서가 아닌 산출이다. 두 표는 같은 것의 두 판이 아니라 서로 다른 두 축이다. 함께 고친 것 — merge=union 이 표 안에 남긴 빈 줄 6개가 GFM 표를 끊고 있었다(에디터·diff 에서는 표로 보인다, T64). 400 passed · line 99.83% · branch 98.98% · ruff check . 통과 색인 검사 리포트, T64·T65, tools/check_docs_index.py
2026-08-03 읽히지 않는 설정 키 전수조사(Jira 작업). -219(T43)가 이미 PINLOG_JUDGE_MODEL 하나를 짚었지만 이번에는 .env·.env.example·config.py·배포 SealedSecret(8키)을 전부 대조하고, Settings 필드 15개 각각에 sentinel 값을 런타임 주입해 grep이 아니라 실제 반영 여부로 확인했다(-210이 "임계값이 없다"고 적었다가 config.py:114에 있었던 사고와 같은 함정을 경계). 읽히지 않는 키는 정확히 2개(PINLOG_JUDGE_MODEL·PRESET_CACHE_TTL_SEC)였고 둘 다 저장소는 이미 정리가 끝나 있었다 — 전자는 -175PINLOG_JUDGE_CHAIN으로 대체, 후자는 ai#24(2026-07-27)가 이미 config.py·.env.example에서 제거. 남은 문제는 개발자 로컬 .env(gitignored)에만 있던 잔재였고 이번에 주석 처리했다. 계약의 전제도 정정했다 — ".env 13 대 .env.example 12"를 단일 차집합(하나만 다르다)으로 가정했지만 실제로는 .env만의 키 2개(둘 다 읽히지 않음)·.env.example만의 키 1개(PINLOG_JUDGE_CHAIN, 코드가 읽음)인 양방향 구성이었다 — 둘 다 정상이라 .env.example에 추가할 키는 없다. 배포 Secret은 애초에 그 두 키를 담지 않았다(인프라 소관이라 값은 확인만, 고치지 않음). -197(app/client/_calls.py) 계측이 벤더·모델을 이미 개별 호출(DEBUG)·창 요약(INFO, 벤더 단위)로 남기고 있어 보강하지 않았다 — 기본 로그 레벨(INFO)에서는 벤더 단위까지 보이고 벤더→모델은 PINLOG_JUDGE_CHAIN(공개 값)과 1:1이라 특정 가능하다. 폴백 체인 구조(-175-210 리포트는 계약대로 손대지 않았다. 코드·.env.example 변경 없음(이미 올바른 상태를 확인) 죽은 설정 키 리포트, T66·T67
2026-08-03 back#138 에 남기고 못 지킨 약속 — AI API 게이트웨이 멀티모달(이미지) 지원 재고 — 을 이행했다(Jira 작업). 1×1 PNG(68바이트)를 크기 요인 배제를 위해 쓰고(-205 T62 — 큰 본문은 게이트웨이가 「모델 없음」으로 오진한다), 세 경로(OpenAI·Gemini·Anthropic)에 벤더 스펙 그대로 이미지 파트를 실어 각 1회씩 총 3호출로 끝냈다(공용 게이트웨이 최소 침습 원칙). 결론: 지원한다 — 셋 다 200 이고, 응답이 빈 문자열·거부가 아니라 이미지 내용에 대한 실제 답(색상 등 한 단어)이었다 — 게이트웨이가 이미지 파트를 버렸다면 나올 수 없는 응답이다. back#138 이 이미지 분석 흐름(Front → Spring → FastAPI)의 유일한 선행 조건으로 남긴 것이 이걸로 해소된다. 오류 응답을 만들지는 않았다 — 지원 여부 확인이 목적이라 정상 200 으로 끝났고, 오류 본문 형태는 -205 가 텍스트 경로에서 이미 실측해 뒀다(같은 게이트웨이 구현이므로 이미지 경로도 같은 형태를 따를 것으로 본다, 별도 확인은 안 함). 스크립트는 세션 스크래치패드에만 두고 커밋하지 않았고, .env 값은 메모리에서만 읽어 모든 출력에 키 문자열 치환 + 20자 이상 토큰 마스킹을 거쳤다(안전망 — 실제로는 한 건도 안 걸림, -205 결론과 일치). 남은 것 — OpenAI 경로의 prompt_tokens 가 8,524 로 텍스트 프롬프트 길이 대비 크게 뜬 것은 원인 조사 없이 관측만 남긴다(이미지 분석 기능 설계 시 토큰 예산에 반영 필요). 429·5xx 본문, 실사용 크기(최대 10 MiB) 이미지, 다중 이미지는 재지 않았다 — 범위 밖 비전 재고, back#138
2026-08-03 -226 이 명시적으로 한 방향만 켠 문서 색인 검사(④)의 반대 방향(파일 표에 번호가 있는데 전수 표에 없다)을 마저 닫았다(Jira 작업). ④는 전수 표 링크가 매핑의 유일한 출처였기 때문에 반대 방향을 검사할 수 없었다 — 이번에 docs/implements 파일 표에 번호 컬럼을 추가해 매핑의 출처를 파일 표 자신으로 옮기고, 그 컬럼으로 새 검사(⑤)를 켰다. docs/troubleshooting 은 그대로 뒀다 — 전수 표가 설계상 문서를 안 가리켜 컬럼을 넣어도 대조 출처가 못 된다(-226 실측 유지). 번호 없던 7건을 다시 세어(계약이 재확인을 요구했다) 하나씩 판정했다 — 5건은 문서에 실코드·실측 산출이 있어 신규 번호(I37I41), candidate-threshold 는 새 번호가 아니라 이미 있던 I30 에 빠진 반영처 링크였을 뿐이라 그것만 채웠고, ticket-audit-96-77 은 문서 자신이 "만든 것이 아니라 확인한 결과"라고 규정해 「없음」으로 명시했다(빈 칸이 아니라 명시적 상태 — ⑤ 는 빈 칸을 형식 위반으로 잡는다). RED 먼저 확인했다 — 검사·테스트를 작성한 뒤 실제 README에 아직 번호 컬럼이 없는 상태로 돌려 26건 형식 위반을 관측하고, 컬럼을 채워 GREEN 전환했다. 계약이 경고한 T56 함정이 실제로 났다origin/dev 병합 중 merge=union 이 번호 컬럼 없는 낡은 파일 표 전체를 되살렸고, 동시에 병합된 병렬 PR(#81, Jira 작업)이 같은 I36 을 잡아 충돌했다. 낡은 블록은 지우고, 이번에 들어가는 쪽(내 retry-and-error-classification)만 다음 빈 번호(I41)로 재번호해 해소했다 — #81 의 I36 은 이미 dev 에 병합된 채였으므로 그대로 뒀다. -226 리포트 §4.1 이 "검사가 있어도 충돌 자체는 안 없어진다"고 적은 것과 정확히 같은 모양으로 재현됐다. ruff check . 통과 · 전량 pytest 통과 · line 99.83% · branch 98.99% · python tools/check_docs_index.py 통과(I1I42) 단방향 검사 리포트, tools/check_docs_index.py
2026-08-03 -210(τ)·-219(프롬프트)·-223(다수결)이 판정 경로의 세 층을 각각 재고 전부 「바꾸지 않는다」로 끝난 뒤, 셋이 공통으로 지목한 마지막 축 — 프리셋의 정의 — 을 쟀다(Jira 작업). examples 만 채택하고 description 은 기각한다. 두 필드가 들어가는 곳이 달라(examples 는 임베딩 입력만, description 은 임베딩+판정 프롬프트 양쪽) 조건을 D·E·DE 로 갈랐고, 갈라 재지 않았으면 D 의 해로움이 DE 안에 묻힌 채 배포됐다Dfit 0건 Context 를 8.20 → 10.00 으로 악화시키고 범위가 분리된다(세 티켓 동안 8.00 ± 0.00 으로 꿈쩍 않던 지표가 처음 움직였는데 나쁜 쪽이다). E 는 오분류 11.00 → 6.90행(p=0.0001) · 정상 손실 -0.80(p=0.16) · 교환비 0.20 이고 순이득 부호가 라벨 처리 양극단에서 다 양수다(네 티켓 중 유일). 셋이 못 지운 100% 고정 오분류 7행 중 3행을 지웠고 그중 2행은 판정 이전에 후보에서 빠진다. 사전 기준(범위 비중첩)은 못 넘었고 무르지 않고 그대로 적었다 — 대신 -219·-223 에 없던 독립 증거 넷(후보 선정 층·라벨 42건 밖 홀드아웃 30건·순이득 부호·회차 확장에서 효과가 커진 것)을 근거로 판단했다. 자기충족 방어 셋을 뒀다: 대상은 라벨에서 고르되 수정 내용은 프리셋 텍스트 안에서만 진단(다른 프리셋 소관 어휘를 걷는다) · 안 고친 22종을 대조군으로 분리 집계 · 42건 밖 본문 30건을 새로 만들어 반대 방향 예측 검증. DE 는 라벨 밖 행이 5.60행으로 뛰어 부호가 안 정해지고, 그 라벨을 개정안 설계자가 붙이면 자기 확인이 되므로 모른 채로 넘긴다. 측정 중 T68(임베딩 API 가 같은 배치 구성으로도 비결정적 — T42 의 전제를 정정)·T69(tau_grid/score.py 의 τ 격자가 rank>K 로 밀린 행을 안 셈, 임베딩이 바뀐 행렬에서만 발현) 발견. DB 재적재와 version 처리는 이 PR 밖이며 리포트 §8.2 가 근거와 함께 남긴다 — 특히 시드 머리말이 version 을 자기 소관이 아니라고 규정하는데 코드는 거기서 읽어, 올릴 경로가 지금 없다 프리셋 개정 리포트, T68·T69, tools/preset_desc/
2026-08-03 ai#97(표지 생성)·ai#98(장소 제안)이 함께 기다리던 AI API 이미지 실사용 조건을 두 축으로 쟀다(Jira 작업, 2026-08-03 17:07~17:26 KST · root https://api.example.com · 호출 50회). 축 A(생성)는 지원한다gpt-image-1·gemini-2.5-flash-image 둘 다 1024×1024 PNG 를 냈고, 게이트웨이에 모델 allowlist 가 있어 dall-e-3·imagen-3 은 400 이다. 축 B 는 가설 C — 토큰은 치수에 붙고 바이트에는 안 붙는다(512×512 고정에서 바이트 36배에 prompt_tokens 8,523 불변). -227 이 남긴 8,524 는 게이트웨이 가산이 아니라 gpt-4o-mini 의 타일 요금이었다(잔차 0). 설계의 핵심은 치수와 바이트를 따로 움직인 합성 이미지다 — 실제 사진으로는 세 가설이 같은 곡선을 내 갈리지 않는다. 그리고 티켓이 묻지 않은 것이 더 급했다 — 게이트웨이 요청 본문 상한(96,024 B 통과 · 129,564 B 거부)이 있고 넘으면 「Model not found in request」로 온다(-205 T62·-225 와 같은 원인). 이미지 원본 약 70 KB 가 확인된 통과 대역이라 실사용 대화 캡처는 토큰을 쓰기 전에 막힌다ai#98 의 전제가 바뀐다. 정책 거절 층도 갈렸다(OpenAI 400 / Gemini 200 + finishReason=PROHIBITED_CONTENT) implements, tools/gms_image/, 비전 재고
2026-08-03 하루에 유형마다 서너 번씩 난 반복 사고 세 유형을 트러블슈팅으로 남겼다(Jira 작업, T70~T72). 개별 사고의 해결이 아니라 반복 유형을 적는 문서다 — 각 건의 처리는 자기 티켓·PR·이슈에 있고 여기에는 「무엇이 반복됐는가」만 남는다. 유형마다 마지막 사고는 앞선 사고의 대응이 이미 있는 상태에서 났다 — 07-31 색인 사고 다섯 건과 같은 모양이고, 차이는 성실성이 아니라 강제 장치다. ① 사본에서 꺼낸 값(T70) — 철회된 단정이 그 문서를 근거로 인용하며 되살아나고, 코드 한 행을 인용하며 바로 다음 행을 놓친 오독이 이미 병합된 작업의 설계 근거가 되고, 원장에 적어 둔 정정이 계약에서 원래 문구로 되돌아오고, 계약에 실은 수치가 여러 번 틀렸다. 넷 다 사본이 아니라 원문을 연 다른 사람이 잡았다 — 「쓰기 직전 재조회」 규칙은 이미 있었는데도 났다. 사본은 자기가 낡았다고 말하지 않으므로 대응을 읽는 쪽이 아니라 쓰는 쪽에 뒀다(R-18 「값 대신 출처」). 철회하는 쪽도 대상을 좁힌다 — 폐기된 것은 값이 아니라 값의 항상성이다.반만 덮는 자동화(T71) — 「성공했다」와 「실제로 반영됐다」가 다르고 실패가 조용하다. 성공 판정을 네 층으로 가른다(프로세스 생존 ≠ 실행 성공 ≠ 실제 변경 발생 ≠ 최종 전달 성공). 감시 고아는 heartbeat 로 고쳤고(판정은 사람이 한다), 배포 자동화 둘은 인프라 소관이라 이슈로 기록만 했다. 「다음 스케줄이 복구한다」는 전제 자체가 약하다.공개 저장소 실사용자 기록(T72) — 훅이 바깥으로 나가는 발화만 검사해 스크립트가 만들고 사람이 git add 한 산출물은 검사 지점을 한 번도 안 거쳤고, .gitignore 예외가 「커밋하라」고 적극적으로 지시하고 있었다. 대응 미정 — 선택지 넷 중 마스킹 문구의 경로 안내 제거만 즉시 가능하다. 위치·규모는 적지 않는다(위치 비공개 선례). 이 문서 자신이 T70 의 첫 시험이라 수치·코드 행 번호를 옮기지 않고 좌표만 가리킨다 T70~T72
2026-08-03 표시명 개정(Jira 작업)이 코드에서 운영 화면까지 간 배포 체인을 기록했다(I50). 체인 여섯 단계의 소관을 갈랐다 — 우리(ai#102 dev · ai#104 릴리스) · 자동화(image-publish 는 별도 워크플로가 아니라 CI 안의 job 이고 main push 조건이라 dev 병합만으로는 이미지가 안 만들어진다) · 인프라 소관(infra#185) · 클러스터 sync · 화면. 핵심은 미결 판정 하나를 실측으로 닫은 것이다 — 프리셋 봉인 값이 개정 전 값 그대로인 채 배포됐는데 새 표시명이 화면에 나왔다. 따라서 그 값은 판 표기이지 부트스트랩 재실행 조건이 아니다(BeforeHookCreation 이 같은 이름 Job 을 지우고 다시 만들고, 이미지 태그가 바뀌면 sync 가 돌며, 적재가 멱등이다). 그 전에는 중앙이 「값이 같으면 재실행되지 않는다」로 판정했고 독립 검증이 논거를 반증했으나 클러스터 관측 경로가 없어 「확인 불가」로 끝나 있었다 — 배포가 답을 냈다. 그래도 표기가 낡은 것은 기록 정합성 문제로 남는다(배포를 막지 않을 뿐이고 틀려도 실패하는 검사가 없다). 릴리스가 두 번 막혔고 둘 다 다음에 그대로 다시 만난다 — base 검사는 main base 를 release/*·hotfix/* 로 한정하므로 dev 를 그대로 열면 CI 가 막고(ai#103 이 그렇게 닫혔다), strict 상태 검사는 BEHIND 를 거부하므로 main 을 먼저 병합해 해소한다(병합 커밋 032e391f). 갱신 자동화의 예정 회차가 릴리스 병합 직전에 돌아 변경 없이 끝났고, 갱신을 만든 것은 수동 실행이라 success 두 번의 뜻이 다르다. 확인 수단이 화면뿐이라 「아직인가 실패인가」가 갈리지 않아 한 번 오판했다가 재확인에서 뒤집혔다. 값은 옮기지 않고 좌표만 적었다(T70 적용) 배포 체인 리포트, I48, T70~T72
2026-08-03 외부 리뷰 문서를 P47 제안 문서로 다시 썼다 — 프리셋의 표시 라벨·축 정의·스키마 개정안(Jira 작업·-269·-270·-271·-292, ai#90 후속). proposals 최초의 Proposed 다 — §7 이후가 미실행이고 §11 실측이 없다. 다만 §6(표시 라벨)은 문서를 쓰는 사이에 반영이 끝났다-292display_name 을 명사·명사구로 통일해 ai#102(devai#104(main)로 나갔고, 그래서 그 절만 제안이 아니라 기록으로 다시 썼다(제목에 「반영 완료」 · 표의 「권장 표시명」을 「반영값」으로 · 정본은 문서가 아니라 data/keyword_preset.yaml 임을 명시). 초안과 실제가 갈린 셋을 확정으로 남겼다ALONE 은 초안의 1인 을 기각하고 혼자 를 유지했고(축의 나머지가 관계어인데 수량 표현만 이질적이다), TRENDYexamples 가 유행이 아니라 미감을 가리키므로 감성 으로 확정했으며, RETRO초안에 없던 결정으로 복고 가 됐다(같은 축의 어휘 층위 통일 · description 과 정합). 선결 조건이 통째로 바뀌었다 — 「프리셋을 고치면 기존 판정을 전부 재처리한다」로 정해지면서 「판을 구분할 경로」(행 version 을 올릴 경로가 없다는 사실, -269)가 선결에서 §3-F 로 내려갔고, 그 자리에 **「재처리할 수단이 없다」**가 들어갔다: 완료 State 를 되돌리는 코드가 없고 · 재스캔이 잡는 상태에 완료가 없으며 · 한 번에 밀면 게이트웨이 한도에 걸려 재시도 예산이 소진되면 실패로 굳는다(부재의 범위는 ai#100). 「정할 것」이 아니라 「만들 것」이라 §13 0단계가 구현 항목이 됐고, 같은 이유로 §10.3 세트 release 는 운영 용도가 빠지고 평가 재현만 남아 우선순위가 내려갔다. 원본의 다섯 곳을 정정했다: ① Feed 표시 정렬을 절째로 빼고 back P46 을 가리켰다(원본 제안이 그 결정이 명시적으로 기각한 방향이었다), ② 「CI 에서 검증되지 않는 것」 목록을 없애고 방향과 「테스트 코드가 정본」만 남겼다(이미 있는 테스트를 없다고 적고 있었다), ③ 배포 서술을 hook 메커니즘 존재라는 구조 사실로 줄였다(나머지는 infra 소관), ④ version 을 「세트 release 와 분리」가 아니라 판 식별 문제로 재배치, ⑤ 표시명 오염을 「임베딩 입력」이 아니라 임베딩·판정 프롬프트 양쪽으로 고쳤다 — 그래서 이 변경은 임베딩 실험이 아니라 프롬프트 실험 대상이다. 구조 사실 일곱을 보강했다(개정에 기존 판정 재처리가 따라붙는다 · 후보 슬롯 경쟁으로 프리셋 단위 개선이 전역에서 상쇄된다 · 저빈도 프리셋 처분이 라벨 개정보다 앞선다 · 표시명 개정이 백엔드 조회의 정렬과 중복 흡수에 파급된다 · is_active 를 끌 경로가 없어 tombstone 은 구멍 메우기다 · 공용 계약 개정 단계 · 임베딩 비결정성 때문에 1회 측정으로 채택을 가르지 않는다). 값을 쓰지 않았다 — 측정 수치·현행 상수·행 번호·배포 상태를 전부 출처 참조로 바꿨고 남은 숫자는 §6.2 의 반영값뿐인데 그것도 YAML 이 정본임을 표 앞에 못박았다. 문서가 값을 안고 있으면 값이 바뀔 때 문서만 낡는다. §6 이 실측 없이 먼저 나간 것은 §14 에 감수 항목으로 남겼다 — 표시 계층만 손댄 범위였기 때문이고, 대가로 라벨 개정이 판정에 얼마나 파급됐는지는 재지 않은 채 §11 에 남는다 P47, 근거 리포트, T68·T69
2026-08-07 검색 응답에 keywordMatched 필드를 추가했다(Jira 작업). 키워드 재정렬(_rerank_by_keyword, P49 §4)이 이미 계산하지만 정렬에만 쓰고 버리던 match 여부를 응답까지 살렸다 — 재정렬을 tuple[list, set[int]]로 바꿔 match 한 Record id 를 함께 반환하고, 재정렬이 생략되는 모든 경로에서는 빈 집합이라 자연히 False 다. similarity 는 원래 코사인 그대로 두어 정렬 점수가 새어 나가지 않는다는 기존 계약을 지켰다. 결합 신뢰도 게이트(Jira 작업)가 쓸 S3 신호를 준비하는 작업이다 리포트
2026-08-07 재정렬 후보 불변 계약(test_search_rerank.py ②)과 결합 신뢰도 게이트(back 레포 Jira 작업, BD-52)의 계약을 분리 명시했다(중앙 조정 세션 인계 문서 §4.5가 요구한 검증). 재정렬은 후보를 안 지우지만 게이트는 정반대로 의도적으로 지운다 — 같은 파일에 다섯 계약만 있으면 게이트도 후보 불변을 지켜야 한다는 오독이 생긴다. docstring에 "이 파일이 고정하지 않는 것" 절을 추가하고, 게이트라면 제외했을 모양의 후보(기존 컷은 통과·게이트 임계값 미만·신호 없음)도 재정렬은 순서만 밀 뿐 지우지 않는다는 것을 새 테스트로 고정했다. 이 브랜치는 -399(I59) 병합 전 갈라져 같은 색인 누락을 독립적으로 I58 로 잡아 두고 있었다 — dev 병합에서 두 번호가 부딪혀(-399 가 먼저 I59 로 확정 병합) 이 리포트는 I60 으로 재확정했다(T60 과 같은 패턴, check_docs_index.py 의 번호는 병합 시점 이후에 확정한다는 원칙대로) 경계 리포트
2026-08-07 결합 신뢰도 게이트(중앙 조정 세션 인계 문서 OFFTOPIC-CONFIDENCE-GATE-HANDOFF-DRAFT.md §4) 의 threshold 후보 0.35 를 오프라인으로 재측정했다(Jira 작업). 0.35 는 원래 SEARCH_KEYWORD_RERANK_FLOOR(질의-Preset 코사인 판정값)에서 가져온 값이라 이 용도(S1 단독·저유사도 결과를 숨기는 게이트)로는 검증된 적이 없었다. gate_sweep.py 로 문장형 정답 12·단어형 정답 66·진단프로브 22·무관 60·타인소유 96건에 게이트 규칙을 적용해 보니 threshold 0.35 에서 정답 손실 0 을 유지하면서 무관 노출이 크게 줄었고(단어무관 23/45·타인소유 55/96 신규 침묵), 손실 0 구간의 최적값(0.36)과도 사실상 같았다. 최초 실행에서는 프로브 6 건이 손실로 나왔는데, 원인은 recall_probe.json 이 질의별이 아니라 최상위에 user_id 를 두는 형식을 놓친 것이었다 — 고치자 손실이 threshold 0.38 까지 0 건으로 나왔다. 0.35 채택을 권고한다 리포트
2026-08-07 401 리포트 작성 뒤 test_docs_index.pydocs/implements/README.md의 색인 두 표(개별 리포트·구현·산출 전수) 모두에서 I58(2026-08-07-gate-threshold-remeasure.md) 행이 빠진 것을 잡았다 — 리포트를 커밋하면서 색인 갱신을 놓친 것. 두 표 모두에 행을 추가해 정정했다 gate_sweep 리포트