테스트가 아닌 검사 — 부재 가드 · 완료 감지기 · 문서 전체를 훑는 단언 — 은 RED 를 먼저 보는 습관이 없어서, **구조상 한쪽 판정만 낼 수 있는 채로** 배포된다. 초록이면 "결함 없음"이 아니라 "빨개질 수 없음"이고, 타임아웃이면 "전파 안 됨"이 아니라 "성공을 감지할 수 없음"이다. 검사마다 **반대 판정을 한 번 강제로 보라.** 트리거 - `until`/폴링 대기 루프, 삭제 PR 의 부재 가드, dict/응답을 문자열로 합쳐 매칭하는…
LLM/AI 검토자로 변경 전후를 재라벨해 "correct가 +4 늘었다"를 주장할 때, 출력이 바뀌지 않은 항목에서도 라벨이 같은 크기로 오르내린다 — 그 변동이 측정의 잡음 바닥이다. 전체 집계가 아니라 "출력이 실제로 바뀐 항목"과 "그대로인 항목"을 나눠 보고, 그대로인 쪽의 개선/악화 수를 신호의 기준으로 써라. 트리거 - AI 검토자 블라인드 재라벨, 전후 비교 표, "정확도 향상" 주장, 사람 라벨 없는 평가, 같은 평가셋을 여러 차례 채점.
기존 필드(예: "판단 시점 분류 provisional") 옆에 더 엄격한 정의의 쌍둥이 필드(예: "판단 시점 클릭만 쓰는 initial")를 추가하면, 둘의 차이가 검토자에게 "누출"로 보고된다 — 새 필드가 만든 문제가 아니라 **기존 필드가 원래 경계를 넘고 있었다**는 발견이다. 추가 전에 기존 필드의 입력 시간 범위를 실측하고, 차이를 플래그로 표면화하라. 트리거 - "판단 시점/as-of" 필드 추가, 사후 해석과 사전 해석 분리, 시간 경계…
결과를 숨긴 "블라인드" LLM 평가 패킷(사후 판단 편향 통제)은 두 방향으로 동시에 실패한다. 행동 요약의 필터가 증거를 지워서 평가자가 행동을 반대로 읽거나, 메타데이터 필드(샘플 유형·이벤트 이름·소스 kind)가 숨긴 결과를 흘린다. 블라인드 패킷을 설계할 때, 블라인드 평가가 기대와 다르게 나왔을 때, 평가 대상 패킷을 고친 직후, "증거 없음"을 "불가"로 판정하는 레지스트리를 쓸 때 읽어라.
LLM 평가자(코치·판정자)가 근거 없는 부정 판정을 낼 때, 그 판정이 기대고 있던 선택지 하나만 막으면 평가자는 다음 선택지로 옮겨 같은 결론을 낸다. 고쳐야 하는 것은 선택지가 아니라 "부정 판정을 내려도 되는 조건"이다. LLM 판정·코칭에서 특정 오답을 막는 수정을 할 때, 수정 후에도 같은 사례에서 같은 결론이 다른 근거로 나올 때 읽어라.
규칙·휴리스틱을 개발용 라벨로 튜닝할 때, 집계 지표(정밀도 +3%p)가 좋아진 변경이 **사람/검토자가 높은 확신으로 확인한 대표 사례 하나를 조용히 잃을 수 있다** — 집계에서는 FP 2개 감소 vs TP 1개 손실로 "개선"처럼 보인다. 대표 회귀 사례를 정확한 기대값(시각·종류)으로 고정한 테스트가 유일한 센서다. 또 새 분류 체계로 기준선(v3)을 재채점할 때 옛 이름을 새 종류 **집합**으로 매핑하면 기준선에 유리해져 결론이 뒤집힐 수 있다.…
zsh 는 `$var` 를 단어 분할하지 않는다 — `FILES=$(find ...)` 뒤 `sed ... $FILES` 는 목록 전체를 **파일명 하나**로 넘겨 15개 치환이 전부 실패한다. 진짜 위험은 실패 자체가 아니라, 바로 뒤 검증 `grep ... $FILES` 가 **같은 관용구를 공유해 함께 죽는 것**이다: 편집도 안 되고 검증도 "잔여 없음"처럼 보인다. 배열 `FILES=(...)` + `"${FILES[@]}"` 를 쓰고, 검증은 편집과…
자동 탐지 결과를 사람/에이전트 라벨과 대조할 때, 채점기의 짝짓기 규칙(시간 허용 오차·반올림·라벨 이름 대응표)이 정답을 옆 구간에 붙여 FP·종류 불일치를 만들어 낸다. 그 숫자를 보고 규칙을 튜닝하면 존재하지 않는 오류를 고치게 된다. 탐지기 정밀도·재현율을 라벨로 잴 때, 구간 라벨과 시점 탐지를 짝지을 때, 새 규칙 버전의 출력 이름이 바뀌었을 때, FP 사례를 보기 전에 임계값을 바꾸려 할 때 읽어라.
클라이언트가 유효성 검사를 통과한 입력만 기록하는 로그(게임 리플레이 명령, UI 이벤트, API 요청 로그)에서는 같은 동작의 연속 입력 간격 분포에 생긴 "빈 구간"이 숨은 상태(쿨다운·레이트 리밋·잠금)를 드러낸다. 그 상태를 결과에 따라 바꾸는 규칙(적중 실패 시 쿨다운 환급 등)이 있으면 입력 타이밍만으로 기록되지 않은 결과를 도출할 수 있다. 전제는 반드시 상태가 바뀌는 조건(특성·설정)에서 경계가 같이 움직이는지로 검증하라. 트리거 - "실행/성공…
진실값(관측)이 어려운 상황에 몰려 있는 데이터에서 "평가 대상 한 점만 빼는" 보류 검증을 하면, 자연적으로 긴 공백은 쉬운 구간(관측이 드문 = 일이 없는 구간)에서만 생기므로 정확도가 과대평가된다. 평가 대상 주변 ±H 창의 관측을 모두 숨기는 강제 공백 평가를 따로 돌리고, 둘을 섞지 말라. 또 미래 관측을 쓰는 오프라인 재구성은 평가 대상과 숨김 창을 입력에서 빼야 보류 검증이라 부를 수 있다. 트리거 - 보간·궤적 재구성·결측 대치 정확도 평가,…
헤드리스 브라우저에서 재생·애니메이션 fps 를 재면 페이지가 아니라 **하네스의 프레임 제한**을 잴 수 있다 — 유휴 rAF 간격(아무것도 안 그릴 때)을 먼저 재서 기준선을 확인하고, fps 판정은 headed 로 하라. 트리거 - requestAnimationFrame 재생 루프, Playwright/Puppeteer 로 fps·jank 측정, "재생이 9fps 로 끊긴다", 성능 수치를 검증 보고서에 적을 때.
fakeredis(aioredis) 의 XREADGROUP 은 `block=` 을 무시하고 즉시 빈 결과를 돌려준다 — 실제로 suspend 하지도 않아서 `while not stop: await xreadgroup(..., block=1000)` 소비 루프가 이벤트 루프를 굶겨 테스트가 영원히 멈춘다. 그리고 컨슈머 그룹을 `id="$"` 로 만들면 그룹 생성 전에 XADD 된 메시지를 건너뛴다. 트리거: fakeredis 로 스트림 소비자 테스트가…
import-linter(및 대부분의 정적 아키텍처 게이트)는 AST 를 읽으므로 **함수 안 지연 import 도 계약 위반으로 잡는다**. 순환 import 를 피하려고 import 를 함수 안으로 옮기는 흔한 처방이 아키텍처 계약에는 통하지 않는다. 처방은 계약이 허용하는 레이어에 헬퍼를 두는 것(때로는 의도적 중복). 트리거: `lint-imports` 실패인데 파일 상단엔 그 import 가 없음, "circular import" 를 lazy…
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or…
가드를 실증하려고 코드를 되돌렸는데 그 치환이 문법 오류를 만들면, 빨강은 나오지만 아무것도 증명되지 않는다 — 그리고 빨강이 나왔으므로 성공처럼 읽힌다. 이걸 잡는 신호는 초록/빨강이 아니라 **돌아간 테스트 개수**다. 트리거 - 전선 절단/mutation 실증, sed·python 로 소스 되돌리기, "가드가 진짜 잡는지 확인", 절단 후 실패 수가 예상과 다를 때, 절단했는데 초록이라 "가드가 약하다"는 설명을 쓰려 할 때.
라이브 서비스의 취약점을 고칠 때, 레포가 PUBLIC 이면 커밋 메시지 · PR 본문 · 이슈 코멘트가 **배포 전**에 취약점을 공개한다(무엇을, 어디로, 어떻게). 커밋/PR 쓰기 전에 `gh repo view --json visibility` 를 확인하고, 공개면 중립적인 제목으로 쓰고 상세는 비공개 채널에. 트리거 - SSRF · 인증 우회 · 권한 · 격리 결함 수정, "보안 수정 PR 올려", 공개 레포, 자동 배포까지 CI 대기가 있는 흐름.
사용자가 입력하는 필드(api_base · url · host · path)를 검증하기로 했을 때, 같은 레코드에 그 호출의 kwargs 로 **나중에 merge 되는 자유형 dict**(extra_params · options · headers · config)가 있으면, 그 dict 가 같은 키나 동의어(base_url)를 실어 검증된 필드를 덮는다. 검증은 필드가 아니라 **최종 호출 인자**에 걸어야 하고, 그 지점은 모든 호출이 지나는 한 곳이어야…
큐 claim 이 SQL 에서 "오래된 순 N 개"를 가져온 뒤 Python 에서 영구히 처리되지 않을 행(거절·보류 표시)을 건너뛰면, 그 행들이 창을 영원히 차지한다 — N 개가 쌓이는 순간 큐는 새 행을 다시는 보지 못한다. 조용하다: 에러도 로그도 없이 그냥 아무것도 안 들어온다. 트리거 - SELECT … ORDER BY … LIMIT 뒤에 in-process `continue`, "JSON 키 판정이 DB 마다 달라서 Python 에서 거른다"…
넓은 범위로 돌릴 때만 실패하고 단독으로는 통과하는 테스트는, 반대가 증명될 때까지 "테스트 간 오염"이다 — 내 변경 탓도, flaky 도 아니다. 파일 쌍으로 이분해 오염원을 찾고, 깨끗한 origin/main 워크트리에서 같은 순서로 재현해 기존 결함인지 가른다. 흔한 원인 두 가지 - 헬퍼의 raw `mod.X = fake`(복원 없음), "이번에 추가된 수"를 세는 멱등성 테스트. 트리거 - 단독 통과·묶음 실패, CI 는 초록인데 로컬 넓은 실행만…
데이터 diff 로 "무엇이 바뀌었나"를 자동 생성하면(게임 빌드·설정·스키마·가격표) 유닛 테스트는 파서만 증명하고 **귀속**(어느 이름에 붙였나)은 증명하지 못한다 — 사람이 쓴 변경 기록이 있는 구간 하나를 골라 줄 단위로 대조하라. 그리고 같은 (필드, 이전→이후)가 주인 없는 엔트리에도 나오면 그건 공통 변경이다. 트리거 - 빌드/버전 diff 로 변경 내역 생성, 내부 ID→사람이 읽는 이름 매핑, "공지 없는 변경" 감지, diff 결과를 사용자…
부작용을 막으려고 게이트(필터·플래그·킬스위치)를 잠깐 내렸다 올리는 절차는, 그 게이트를 **누가 언제 읽는지**가 정한다 — 웹훅 수신기가 아니라 뒤에서 큐를 비우는 소비자(intake·worker·cron)가 읽는다면, 복원은 그 소비자가 해당 이벤트를 처리한 **뒤**여야 한다. 동작 직후 "결과가 없다"를 확인하는 것은 아무것도 증명하지 않는다 — 결과는 아직 만들어지지 않았다. 센서는 결과의 부재가 아니라 **소비자 자신의 판정…
무작위·no-skill 기준선(또는 대조군)을 후보와 **다른 데이터 패널**(다른 prefetch 옵션, 빠진 맵, 다른 scorer_types)로 돌리면, 그 기준선은 신호 없음을 재는 게 아니라 **패널 차이가 엔진과 맞물린 효과**를 잴 수 있다 — 그리고 그 효과로 "그럴듯한 메커니즘"(예: 회전율이 DD 를 낮춘다)이 만들어져 결정의 근거가 된다. 센서는 **같은 기준선을 다른 경로로 재현했을 때 안 맞는 것**이다. 또한 "보수적으로" 바꾼…
문서 간 TF/bag-of-words 유사도(공시 텍스트 diff, 버전 비교, 중복 탐지)에서 **문서가 자기 이름을 가장 많이 말한다** — 회사명·제품명·작성자명이 최빈 토큰이라 그 표기가 바뀌면(리브랜드, 붙여쓰기, 합병명) 내용이 그대로여도 유사도가 무너지고, 반대로 이름 반복이 유사도를 1 쪽으로 끌어올린다. 이상값을 보면 추출부터 의심하지 말고 **두 문서의 최빈 토큰을 나란히 찍어라.** 이름을 빼는 "교정"은 신호 자체를 바꿀 수 있으니…
로컬에선 매번 통과하고 CI 에서만 가끔 실패하는 E2E 가 "결국 맞는 위치/상태가 되는가"를 기본 타임아웃(5 s) 안에 폴링한다면, 그 "결국"을 누가 만드는지부터 재라 — 앱 코드가 아니라 **브라우저·프레임워크의 늦은 자동 보정**(앵커 재스크롤, 레이아웃 안정 후 재배치)에 기대고 있으면 느린 러너에서 창을 넘긴다. 재현이 안 되면 가설을 하나씩 **측정으로 기각**하고, 제품 쪽은 그 보정을 **직접·결정적으로** 하게 바꾸고, 테스트 대기 시간은…
파라미터 스윕에서 어떤 arm 이 게이트를 "통과"하면, 숫자를 믿기 전에 **그 knob 이 이름대로 작동해서 통과했는지** 메커니즘으로 설명하라. 센서는 **같은 knob 의 서로 다른 값이 바이트 단위로 같은 행을 내는 것**이다 — 그건 knob 이 측정 대상이 아니라는 뜻이다. 그리고 한 arm 에서 엔진이 죽으면 그건 데이터가 아니라 하네스 결함이다: TDD 로 고친 뒤 **이미 잰 arm 이 필드 단위로 재현되는지**와 **새 경로가 실제로…
집계 API 를 "차원 A × 차원 B 칸마다 1회"로 예산 잡기 전에, 각 차원이 **분할(partition — 겹침 없이 전체를 나눔)**인지 **겹침(overlap)**인지부터 실측하라. 분할 차원은 하위 칸만 받아 **전체를 합산으로 만들 수 있어** 전체 칸 호출이 0 이 되고, 겹치는 차원만 칸마다 따로 받아야 한다. 검증은 이미 가진 데이터로 "부분 합 == 전체"를 행 단위로 대조하면 공짜다. 트리거 - 주간/일간 쿼터가 있는 통계 API,…
래핑한 CLI/프로세스에 한도(--max-turns · 토큰 예산 · 시간 상한)를 새로 붙일 때, 그 한도에 닿으면 도구가 **non-zero exit / is_error** 로 끝나는 경우가 많다 — 그리고 래퍼가 "실패 = 재시도"로 배선돼 있으면, 한도가 막은 일을 **새 세션이 처음부터 반복**한다. 한도가 자기를 무력화한다. 붙이기 전에 진짜 CLI 를 한 번 한도에 닿게 돌려 종료 모양을 재고, 한도 도달은 실패가 아니라 "정상 종료 + 표식"으로…
파생값(추출 텍스트·파싱 결과·임베딩·썸네일)을 **원본 식별자만으로** 캐시하면, 원본이 진짜 불변이라 `if cache.exists(): read` 가 완벽히 옳아 보인다 — 그런데 **파생 코드를 고치면 그 수정은 디스크에 이미 있는 행에 영원히 도달하지 않는다.** 머지·배포·CI 전부 초록이고, 테스트는 자기 픽스처를 만들어 쓰니 통과하고, 프로덕션은 옛 출력을 계속 서빙한다. 더 나쁜 건 신규 항목만 새 버전으로 들어와 **캐시가 두 파서 버전으로…
데이터 소스/포맷을 옮긴 뒤(v4→v5, API v1→v2, 스키마 개편) 예전엔 있던 항목이 새 출력에서 사라지면, "새 소스에 없다"고 결론 내고 옛 값을 이어붙이기(carry-over) 전에 **그 id 가 새 원본 어디에든 나오는지** 전체 검색하라. 대개 새 포맷이 그걸 다른 경로(하위 유닛·중첩 섹션)로 옮겼고, 빠진 건 내 어댑터다. carry-over 는 원인을 덮고 "실제로 사라진 것"과 "내가 못 읽은 것"을 구분 불가능하게 만든다. 트리거…
같은 데이터 소스를 쓰는 다른 서비스가 나와 다른 결과(더 새 버전·더 많은 표본·다른 순위)를 보여주면, 임계값부터 의심하지 말고 **그쪽이 보낸 쿼리의 집계 단위**(버전 묶음·기간·필터)를 그들 페이지 payload 에서 뽑아 내 쿼리와 diff 하라. "수집 누락이냐 진짜 부족이냐"를 내 정의 안에서만 답하면 정의 자체가 원인인 경우를 놓친다. 유료 API 를 탐침할 땐 **무료인 에러 응답부터** 보내 허용값과 남은 쿼터를 읽어라. 트리거 - "저…
문서·약관 페이지가 Cloudflare 봇 체크로 curl/WebFetch/헤드리스에 403 이면, API 경로를 추측해서 두드리지 말고 **시스템 Chrome 을 headed 로, 자동화 플래그를 빼고** playwright-core 로 띄워 `innerText` 를 떠라. 챌린지 대기 루프는 **현지화된 제목**("잠시만 기다리십시오…")도 매칭해야 한다 — 영어 "Just a moment" 만 보면 챌린지 페이지를 본문으로 착각한다. 트리거 -…
캐시 앞단의 "업스트림이 실패하면 stale 캐시를 보여준다" 폴백은 **모르는 4xx 를 전부 장애로** 접는다 — 그런데 업스트림이 일부러 거절하는 경우(비공개 전환·삭제·차단·권한 회수·법적 삭제)엔, 그 폴백이 **거절당한 바로 그 데이터를** "잠시 오래된 정보" 딱지를 붙여 계속 서빙한다. 테스트·로그는 전부 "우아한 degrade" 로 읽힌다. 거절 코드를 **실측해서** 장애와 분리하고, 거절이면 캐시를 지우고 다시 묻지도 마라. 트리거 -…
Living 문서(STATUS·ROADMAP·인수인계)는 같은 사실을 산문·취소선·표에 여러 번 적는데, 갱신은 **산문만** 된다 — 표의 행이 가장 늦게 썩고, 그 행이 백로그를 이슈로 옮길 때 근거가 된다. 그리고 문서 이주·의존성 제거 과제에서는 **옮기기 전에 그 참조가 아직 살아 있는지부터 재라** — 실측하니 코드의 문서 인용 13개 중 8개가 이미 죽은 링크였다. 그리고 **이주는 다른 세션에 전파되지 않는다** — 옛 경로에 쓰는 것은 실패하지…
반구조 텍스트에서 구간을 뽑을 때 시작 패턴이 여러 번 걸리면 "가장 긴 구간"으로 모호성을 푸는 게 자연스러워 보인다 — 그런데 **엉뚱한 시작점일수록 종결자가 멀어서 구간이 길어진다.** 즉 극값은 병리적 후보를 피하는 게 아니라 **정확히 그것을 고른다.** 결과가 길고 그럴듯한 산문이라 에러도 빈 값도 안 나오고, 다운스트림은 그대로 삼킨다. 유일한 센서는 **올바른 출력이 반드시 가져야 할 성질**(헤딩 경계에서 시작하는가, 밖에 있어야 할 절을 품고…
HTML 을 `.text()`/`get_text()`/innerText 로 긁으면 **의미를 뒤집는 마크업**(<s>/<del>/<strike> 취소선, <ins>, hidden, 인라인 옛 값)이 평문에 섞여 들어가 철회된 문장이 살아 있는 문장으로, "35 40" 같은 뭉친 숫자로 나온다. 스크래핑 파서를 쓰기 전에 원문에서 `<s>` 등을 grep 하라. 트리거 - 패치 노트·공지·약관·changelog 스크래핑,…
DB 안에 심은 측정 프로브(정책 함수·트리거·감사 함수)가 기록을 **표에 INSERT** 하면, 그 INSERT 는 관측 대상과 같은 트랜잭션에 묶여 대상이 롤백될 때 같이 사라진다. 그리고 ORM 의 읽기 전용 세션은 닫힐 때 기본으로 rollback 한다 — 그래서 「읽기」가 통째로 측정에서 빠지고, 결과는 「0」이라는 좋은 소식처럼 보인다. 기록은 트랜잭션 밖 채널(RAISE LOG · NOTICE · dblink)로, 대조군은 롤백되는 세션에 걸어라.
Playwright MCP 브라우저는 메인 세션과 병렬 서브에이전트가 **같은 브라우저·같은 탭**을 쓴다. 에이전트가 돌고 있는 동안 MCP 로 라이브 검증을 하면 남의 navigate/입력이 끼어들어 "URL 과 결과가 안 맞는다", "안 부른 데이터가 뜬다" 같은 유령 버그가 보인다. 트리거 - 백그라운드 에이전트가 떠 있는 상태의 browser_navigate/evaluate 검증, 결과와 URL 불일치, 네트워크 기록에 요청이 없는데 화면엔 데이터,…
429 를 전부 "잠깐 기다렸다 재시도"로 처리하면, 주간/월간 쿼터 소진 429 의 Retry-After(= 창이 풀릴 때까지, 수십만 초)를 그대로 sleep 해서 배치 잡이 타임아웃까지 멈춘다. 레이트리밋 429 와 쿼터 429 를 **본문 코드/헤더로 구분**하라. 트리거 - HTTP 클라이언트의 429 재시도 로직, Retry-After 존중, 유료 API 주간 한도, cron/Actions 수집 잡, "잡이 왜 몇 시간째 안 끝나지".
공유 함수에 테스트를 붙이면 그 함수의 *계산*은 지켜지지만, 그걸 **부르는 쪽의 배선**은 무방비로 남는다. 인자를 틀리게 넘기거나 아예 안 넘겨도 스위트가 전부 초록이다. 감지기는 초록/빨강이 아니라 **호출 지점에서 전선을 끊었을 때도 빨개지는가**다. 호출자가 N 개면 테스트도 N 개 — 한쪽에 붙인 가드는 다른 쪽에 대해 아무 말도 안 한다. 트리거 - 공유 함수에 새 파라미터/콜백 추가, 두 곳 이상에서 같은 메커니즘을 배선, "계산은…