{{ message }}
Repository navigation
mcp: 미스 지연·라우팅·별표·인용 검증 통합 배치 — 이슈 62건 (#88~#149) - #150
Merged
Merged
Conversation
#102 — TOOL_ALIASES의 키 4개(인용검증·문서검토·처분기준·절차매뉴얼)가 TOOL_CATEGORIES에 없어, resolveAliasToCategory가 반환한 키로 카테고리를 조회하지 못하고 결과가 통째로 사라졌다. 한국어 별칭 10개(계약서 검토· 과태료 기준·환각 검증·수수료 등)가 discover_tools에서 전부 무매칭이었다. 별칭이 가리키던 실재 도구를 카테고리 체계에 연결한다: - 문서검토 → 기존 카테고리 문서분석으로 이관(analyze_document의 description 태그 [문서분석]과 일치). chain_document_review도 편입. - 인용검증/처분기준/절차매뉴얼 → 킬러피처 단일도구 카테고리 관례 (판례생사→cite_check, 행위시법→applicable_law)를 따라 신설. 네 도구 모두 allTools에 존재하므로 execute_tool로 실행 가능하다. #106 — resolveAliasToCategory의 `q.includes(alias) || alias.includes(q)` 양방향 부분일치가 짧은 질의를 긴 별칭에 흡수시켰다(판례→조세심판례). 정방향도 낱말 경계가 없어 행정규칙이 자치법규 별칭 `규칙`에 걸렸다. matchesAlias로 단방향+시작 경계 매칭을 두 리졸버가 공유한다. 한국어 조사(조세심판원에)를 위해 끝 경계는 요구하지 않는다. meta-tools.test.ts가 두 표의 정합(고아 키 0·유령 도구 0)과 별칭 경계를 회귀로 고정한다.
법제처 DRF는 미스도 HTTP 200으로 답하고, 미스 본문 형태는 target×type 조합이
결정한다. `lawService.do` 단건 조회 중 prec/thdCmp/lsStmd + JSON은 **0바이트**를,
lsStmd + XML은 **권한 안내 HTML**을 돌려준다. 이 본문이 "점검·과부하" 신호로
오인돼 재시도 사다리(1+2+4초)를 완주했다 — 미스 1건에 10~13초, 업스트림 4~5회.
- `singleRecordLookup` 옵션 신설: 미스가 빈 본문/안내 페이지로 오는 경로에서
본문 이상은 짧은 확인 재시도 1회(200ms) 후 `UpstreamRecordMissingError`로
표면화한다. 빈 결과를 정상 응답으로 흘려보내지 않는다.
- 배선은 호출부에서 — `lawService.do` 단건 조회에만 붙이고 검색(`lawSearch.do`)
계열은 그대로 둔다. 검색 미스는 18/18이 정상 봉투라 거기 빈 본문은 진짜 장애다.
- `get_law_system_tree`의 XML→JSON 2연속 호출을 함께 배선 — XML만 고치면 JSON
미스가 두 번째 사다리를 여는 함정을 막는다.
- 백오프 base 1,000ms → 300ms. 업스트림 왕복(검색 ~0.45초 / 단건 ~0.9초) 대비
6~8배이던 대기를 왕복 수준으로 되돌린다. 재시도 횟수·`retryOn`(404 포함)·
버스트 404 워크아라운드는 그대로다.
- `formatToolError`가 이 오류를 `LAW_NOT_FOUND`로 분류. 메시지는 "존재하지
않는다"가 아니라 "반환하지 않았다"로 적고, 점검·과부하 가능성을 함께 알린다.
실측(로컬, LAW_OC=openapi, undici 트레이스 동반):
| 케이스 | 전 | 후 | 업스트림 |
|---|---|---|---|
| `get_decision_text{prec,000000}` | 12,523/12,621ms | 2,707/2,923/3,073ms | 5회 → 3회 |
| `get_three_tier{99999999}` | 12,888/11,774ms | 2,054/2,489/2,205ms | 4회 → 2회 |
| `get_law_system_tree{99999999}` | 10,762/10,520ms | 1,275/1,267/1,527ms | 4회 → 2회 |
히트 경로 무회귀: prec 204201 = 9,625 B 동일, thdCmp 288689 = 6,850 B 동일,
lsStmd 284415 = 2,750 B 동일, search_law 미스 업스트림 5회 동일.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
impact_map (#90): 법제처 키워드 검색이 조번호를 부분 일치로 물어와 「민법 제1032조 위헌소원」이 민법 제103조 질의 결과에 최상위로 섞였다. 인용 그래프는 사실 주장을 하는 기능이라 오탐이 그대로 근거가 된다. 검색 결과를 항목 블록으로 쪼갠 뒤 조 단위(의X 포함)로 재판정해 불일치 항목을 제외한다. 5개 경로(판례·헌재·해석례·행정심판·자치법규) 전부에 적용. 조문 표기가 없는 항목은 판정 보류로 살린다 — 앵커를 이유로 정상 결과를 죽이면 오탐을 오탐으로 갚는 셈이다. 실측: 헌재 6건 중 3건(제1032조·제147조·제397조) 제외, 제103조 3건 유지. 역방향(제1032조 질의)에도 제103조 자료 미혼입 확인. impact_map (#98): jo에 6자리 JO 코드를 주면 '제010300'으로 오조립돼 isError 없이 전건 0건이 나왔다. get_law_text와 같은 계약(자연어 표기·JO 코드 모두 수용)으로 정규화하고, 해석 불가 입력은 [INVALID_ARGUMENT]로 명시 반환한다. verify_citations (#93): 사건번호 인용을 추출조차 하지 않아 판례가 무검증 통과하는데 [HALLUCINATION_DETECTED] 배너가 붙어 전체 검증으로 오독됐다. 사건번호 추출 + nb= 존재 확인을 추가하고, 헤더에 검증 범위(법령 N건 / 판례 M건)를 분리 표기한다. 부존재 단정은 구조적으로 불가능한 경우(미래 연도)로 한정하고, 업스트림 미검색은 '미확인'으로 남긴다 — 법제처 수록 판례는 대법원 중심이라 검색 한계와 부존재를 구별할 수 없다. cite_check (#95): 사건명만으로는 판결의 정체성을 알 수 없어(손해배상(기)) 생사만 답하면 오소개로 이어졌다. 대상 판례의 판시사항을 240자 이내로 발췌해 포함한다(전문은 붙이지 않음). 파일 200줄 규칙을 지키려 공용 로직을 lib으로 분리: article-anchor(경계 판정·jo 정규화), impact-buckets(항목 블록 파싱·집계), case-citation(사건번호 추출·확인), precedent-body(판시사항·treatment 스캔). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
2026-08-16 라우팅 검증 라운드(_workspace/11_qa_route_sweep.md) 실측 기반. 릭 재실행: R-케이스 FAIL 20→5, 경계 프로브 FAIL 11→4, detectScenario divergence 10→0. 후퇴 0건. #101 시나리오 감지 규칙 단일 원본화 (lib/scenario-rules.ts 신설) - CLI(query-router)와 MCP(scenarios/index.ts detectScenario)가 어휘·우선순위를 두 벌로 갖고 있어 같은 쿼리가 표면에 따라 다른 시나리오로 갔다. 판정 어휘·우선순위를 한 파일로 모으고 양쪽이 같은 함수를 호출하게 했다. 라우터는 더 이상 scenario 를 하드코딩하지 않고 도구 선택 후 같은 판정기로 라벨을 붙인다 — 구조적으로 갈릴 수 없다. - 우선순위 기준은 라우터 쪽(구체 상황 우선)을 채택했다. detectScenario 는 함수 안에서 chain_full_research 분기가 두 번 나뉘어 customs 가 먼저 return 하는 우연한 순서였고, 라우터 쪽은 action_plan(7) < customs(13) 로 명시 선언된 순서였다. 명시 선언이 우연한 제어흐름보다 근거가 강하고, "관세 환급을 못 받았어" 같은 시민 질의는 구획 주제보다 상황 서술이 앞서는 게 맞다. 단 상황 서술이 없는 "어떻게 해야"류(GUIDANCE)는 13.5로 내려 관세 실무 질의(FTA 원산지 발급 절차 등)가 시민 가이드로 끌려가지 않게 했다. - 어휘 보강: 못 받/돌려받지 못/안 줘 계열, 경어체(받았습니다)를 어간(받았·걸렸·당했)으로 흡수, penalty 에 '처벌' 추가(프로젝트 대표 예시 "음주운전 처벌 기준"이 미도달이었다). - '되나' 단독 오탐 제거 — 허용 요청 어미(-해도/-아도 되나)와 "어떻게 되나"만 시민 상황으로 본다. #99 specific_article(pri 1) 선점 — 양보 가드에 검증·비교·판례·이력 어휘 추가. 조문번호가 든 인용검증 요청이 verify_citations 에 도달할 방법이 없던 문제. 받는 쪽도 함께 넓혔다(인용/사건번호 + 검증 어휘가 한 문장에 있는 형태). extractLawName 이 검색어에서 '검증' 계열을 남기던 것도 정리. #100 날짜 파싱이 패턴 매칭보다 먼저 돌아 시점 트리거를 지우던 문제 — 매칭을 원문 기준으로 옮기고 날짜 제거는 검색어에만 적용. 제거 자리에 남는 조사·부사(이내·에·이랑)를 함께 걷어낸다. explicit_law(pri 3)가 '개정된 XX법'을 선점하던 것도 양보 목록에 추가. cli-executor: 기간 필터를 받지 않는 도구로 갈 때 무음 소멸 대신 경고를 띄운다(스키마는 불변). #103 추출 소실 — 가운뎃점 다중 조문(조문마다 파이프라인 단계), 별표번호(annexNo 전달), '제' 생략 표기(44조·3조의3·1조 2항), 판결 전문 요구(full=true). #104 '조례' 명시 없는 자치법규 질의 — 지역명+행정주제 조합으로 진입 허용. '연구·요구'처럼 행정구역 접미사로 끝나는 일반 명사는 블록리스트로 배제. #107 도로교통법 약칭(도교법) 추가. search-normalizer.ts 는 LexDiff 이식 코드라 CLAUDE.md Critical Rule 1 에 따라 원본을 대조했다 — 공개 원본(chrisryugj/lexdiff lib/search-normalizer.ts) 에 도로교통법 항목이 없음을 확인하고 이 레포 전용 추가로 표시했다(주석에 대조 근거 기재). 구조: query-router.ts 971줄 → 177줄(엔진). 패턴 테이블은 route-patterns.ts, 추출기는 query-extract.ts, 시나리오 어휘는 scenario-rules.ts 로 분리.
법제처 JSON은 같은 필드를 문자열·배열·`{"#text":…}` 중 무엇으로도 내려준다
(CLAUDE.md 규칙 6). String()으로 뭉개면 판시사항이 "[object Object]"로 출력돼
판결 정체성을 알리려던 #95 수정이 정반대로 작동한다. 자기리뷰에서 발견.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
gestell 유지보수 리뷰에서 나온 네 가지를 고친다. 1. impact-buckets의 ITEM_DETAIL_RE에 `안건번호|청구번호`가 있었으나 5개 렌더러 중 누구도 그 키를 내보내지 않는다(interpretations는 `해석례번호:`). 쓰이는 날이 오지 않는 분기라 실제 키만 남긴다. 2. 표본이 검색 건수를 못 덮은 버킷이 있으면 `총 영향 건수`가 경계 확인분과 업스트림 추정치를 말없이 합산했다. 섞였다는 사실을 합계 줄에 표기한다. 3. 사건부호 블랙리스트가 회(회생: 회합·회단)·호(가정보호)를 막아 실존 사건번호를 조용히 무검증 통과시켰다. 두 음절을 빼고, 화이트리스트 대신 블랙리스트를 쓰는 이유(누락 = 무검증 통과, 과추출 = '미확인' 한 줄)를 근거와 함께 적는다. 4. ArticleAnchor.jo가 정수인데 레포 전반의 `jo`는 문자열(JO 코드·자연어 표기)이라 같은 단어가 두 타입을 가리켰다. articleNo/branchNo로 바꾼다. 실호출 재확인: 민법 제103조 impact_map 출력 바이트 동일(2038B). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
안내문이 슬라이스 뒤에 붙어 5만 자 요청에 50,030자가 반환되던 문제를 안내문 길이를 미리 뺀 예산으로 자르도록 고친다. truncateSections의 전체 한도 재확인 경로도 같은 결함이 있어 함께 수정. 무조건 슬라이스라 컷이 문장 한가운데에 걸리던 문제는 컷 지점을 마지막 완결 경계(줄 > 문장 종결)로 후퇴시켜 해소한다. 후퇴는 400자 상한을 두어, 한 문장이 극단적으로 긴 판결문에서 경계를 찾느라 그보다 큰 분량을 버리는 역전이 없도록 한다 — 상한 안에 경계가 없으면 하드컷을 유지한다. MAX_RESPONSE_SIZE 주석의 "50KB"는 실제 동작(UTF-16 자 수)과 어긋나 "5만 자"로 정정한다(law-text.ts·precedent-evidence.ts 주석 동일). schemas.ts가 200줄을 넘지 않도록 경계 절단·요약 추출 전략은 lib/truncate-text.ts로 분리.
display를 업스트림에 그대로 넘기면 법제처의 LIKE + 가나다순 응답이 정확/부분 분리 전에 잘려, "민법" display:3 이 정확매칭 0건 + 난민법 3건을 1순위로 돌려줬다. 토큰을 아끼려 display를 낮추는 것이 LLM 클라이언트의 자연스러운 행동인데 그때 조용히 다른 법령을 받는다. 업스트림에는 하한(50) 이상으로 요청하고, 사용자가 지정한 display는 정확/부분 분리를 마친 결과에 적용한다. 업스트림 호출 횟수는 늘지 않는다. 잘려서 안 보이는 항목이 있으면 헤더에 display 적용 사실을 밝힌다. search.ts가 200줄을 넘지 않도록 0건 폴백 사다리는 tools/search-fallbacks.ts로, 검색 결과 항목 파싱·포맷은 tools/search-hits.ts로 분리.
#91 별표 파일에 내용이 인라인되지 않고 다운로드 안내만 들어 있는 경우 (관세법 관세율표 등) 파서는 success를 주므로 그대로 성공 응답이 나갔다. 호출자는 419B 응답을 "내용이 짧은 별표"와 구별할 수 없어 빈 근거로 답을 구성한다. isDownloadNoticeOnly로 가려내 [본문 미추출] 마커 + isError로 반환한다. 판정은 법제처 안내 문구와 "안내문·링크를 걷어낸 나머지가 사실상 제목뿐"을 모두 요구해, 안내문 없는 짧은 별표(여권법 수수료표)를 오탐하지 않는다. #94 query는 스키마에 아예 없어 zod가 조용히 버렸고, 그래서 서로 다른 query가 같은 목록(3,085B)을 돌려줬다. query를 스키마에 추가하고 (a) "별표28" 같은 번호 힌트는 선택값으로, (b) 나머지 키워드는 별표명 AND 매칭으로 목록을 좁힌다. 1건으로 확정되면 본문까지 바로 추출해 재조회를 없앤다. 일치 0건이면 전체를 주되 그 사실을 밝힌다 — 조용한 전체 반환이 #94가 보고한 증상 자체다. annex.ts 605줄을 줄이려 선택·매칭 순수 함수는 tools/annex-select.ts로 분리.
렌더러 안내문에 질의 조번호가 들어 있어("검색 보정: …민법 제103조") 마지막
항목에 붙으면 무관 조문 항목이 되살아난다. 빈 줄 경계 규칙을 회귀로 묶는다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
gestell 유지보수 리뷰 결과 반영. 라우팅 결과는 전 케이스 동일(behavior-preserving, 릭 76행 도달도구·파라미터 완전 일치 확인). - scenario-rules: delegation·impact·customs·penalty 4개 규칙이 patterns 와 routeTriggers 에 어휘를 두 번 적고 있었다. 파일 간 중복을 없애면서 파일 안 중복을 새로 만든 셈 — impact 는 만들어진 커밋에서 이미 갈라져 있었다(파급 vs 파급 효과). 원본 문자열에서 조립해 routeTriggers 가 patterns 의 '좁힘'임을 구조로 표현했다. - action-plan: 감지기를 어간(받았·못 받)까지 넓혔는데 짝이 되는 키워드 추출기는 옛 완전형(받았어요?)만 알고 있어 "관세 환급을 못 받았어" → "관세 환급을 못" 잔재가 남았다. 두 규칙을 같은 순서로 맞추고 홀로 남는 부정 조사를 정리. 회귀 테스트 추가. - extractTimeTravel 을 query-extract.ts 로 이동 — route-patterns 헤더가 스스로 "추출기는 query-extract.ts"라고 적어 두고 24줄짜리 추출기를 품고 있었다. - Pattern.name 이 계산만 되고 아무도 읽지 않던 것을 RouteResult.matchedPattern 으로 노출해 explainRoute 가 "어느 규칙이 이겼는지" 보여주게 했다. - SCENARIO_RULES 는 모듈 private 로. 대신 SCENARIO_HOST_CHAINS 를 노출하고 실재 도구인지 확인하는 테스트를 추가했다(hostChain 은 string 이라 오타가 나도 컴파일된다). - pri 3 동순위 순서가 분리 전과 달라진 사실(nts_interpretation ↔ time_travel)을 주석에 정확히 기록. 기존 주석은 "분리 전과 같다"고 잘못 말하고 있었다.
#96 diff 항목에 근거 메타가 없어 소비 LLM이 도구의 정답을 자기 사전지식으로 기각한 실측 사례가 있다 — 관세법 "기획재정부장관→재정경제부장관"은 정부조직 개편의 정상 반영인데 "폐지된 부처이므로 도구 오류"라고 사용자에게 안내했다. 두 시점의 공포번호·공포일자·제개정구분을 헤더에 병기하고, 구간에 낀 개정 연혁(공포번호·공포일자)을 따로 실어 각 변경의 근거를 특정할 수 있게 한다. 조문단위의 조문시행일자도 항목별로 병기한다. #97 변경 전후를 앞에서부터 120자씩 자르면, 개정이 조문 중·후반에 있을 때 [전]과 [후]가 똑같은 앞머리만 보여준다 — 무엇이 바뀌었는지 알 수 없어 조문을 다시 조회하게 만드는 형태다(실측 궤적에서 get_law_text 6회 추가 호출). 공통 앞머리·꼬리를 걷어내고 실제 변경 구간을 중심으로 발췌한다. 잘리는 것은 언제나 바뀌지 않은 주변이고 변경 구간 자체는 예산 안에서 통째로 싣는다 — 단서 조항이 중간에서 잘려 다른 규범으로 읽히는 일을 막기 위해서다. 예산을 넘겨 변경 구간까지 줄여야 하면 그 사실과 전문 조회 경로를 함께 표기한다. 발췌 예산은 변경 조문 수로 배분(조문당 120~260자, 총 12,000자 상한)해 응답이 절단 한도로 밀리지 않게 한다. time-travel.ts가 200줄에 가깝도록 스냅샷·비교·발췌는 scenarios/time-travel-diff.ts로 분리.
문장 한가운데서 시작하는 발췌는 단서와 본문의 구분이 흐려져 다른 규범으로 읽힐 수 있다. 발췌 시작점을 60자 안의 가까운 항(①)·호(1.)·목(가.) 머리로 당긴다. 앞으로만 당기므로 내용은 늘기만 하고, 탐색 폭을 제한해 발췌가 통째로 커지지 않게 한다.
diffArticles는 본문이 같고 조문제목만 바뀐 경우도 변경으로 잡는다. 이때 변경 지점이 없어 발췌가 바뀌지 않은 꼬리를 보여주고, 그것이 "변경 내용"인 양 읽힌다. 본문 동일을 감지해 제목 신구만 표기한다.
dispatch한 적대적 correctness 리뷰가 실행 근거와 함께 낸 결함을 고친다.
1. [치명] nb=는 업스트림 **전방 일치**다("2013다6138" → "2013다61381" 반환).
`.includes()`로 받아주는 바람에 한 자리 틀린 환각 인용이 실존 판결의 법원·
선고일을 달고 ✓로 통과했다 — 환각 탐지기가 환각을 인증했다. 정확 일치만 인정.
2. [치명] 연도·부호·번호 사이 공백을 허용해 평범한 산문이 사건번호가 됐고
("2027 예산 500억원" → 2027예산500), 미래연도 판정에 들어가 인용이 하나도 없는
문서에 [HALLUCINATION_DETECTED]가 붙었다. 공백을 막고, 부존재 단정(✗)은
실재 사건부호일 때로 한정한다. 앞선 커밋의 "과추출은 미확인 한 줄일 뿐"이라는
판단은 틀렸다 — 과추출물이 ✗ 분기로 흘러들어갔다.
3. 표본이 검색 건수를 못 덮으면 버킷 줄·mermaid·합계가 여전히 걸러지지 않은
업스트림 총건수를 주장했다(형법 제1조: 표본 10건 중 9건 불일치인데 "506건").
주장하는 수는 항상 경계 통과분으로 바꾸고, 총건수는 검색 건수로 병기한다.
4. 경계 판정은 조번호만 본다 — "형법 제1조" 질의에 「군형법 제1조…」가 남는다.
법령명 대조는 오탐·오제외 위험이 커 이번 범위 밖이므로, 남은 항목을 검증된
것으로 읽지 않도록 응답에 명시한다.
5. [#98 잔여] "10300"·"0010300"은 buildJO가 7자리 코드를 만들어 헤더·검색어·JO가
깨진 채 0건이 사실로 보고됐다. 6자리가 아니면 거부한다.
6. 사건부호 3음절(재고합)과 개인회생(개회·개확)이 블랙리스트·2음절 상한에 걸려
구 정규식이 뽑던 것을 놓쳤다. cite_check가 하드 실패하던 경로다.
7. 사건번호 확인 상한 5건이 조용해서, 6번째의 환각 인용이 검증 대상에서 빠진 채
"판례 인용 5건"으로 표시됐다. 미검증 건수와 목록을 명시한다.
8. cite_check의 판례내용·참조판례가 아직 String()이라 `{#text:…}`가
"[object Object]"가 됐다. 비어있지 않아 가드를 통과하므로 변경 문구를 못 찾고
"계속 인용 추정"으로 인증된다 — 폐기된 판례가 살아있다고 답할 수 있었다.
리뷰가 무결로 확인한 항목(재기록): 정규식 lastIndex 재진입·ReDoS 없음,
Promise.all 오류 전파 정상, 인용 없는 입력에서 낭비 호출 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
앞선 두 커밋에서 규칙을 넓히며 일상 표현까지 삼키는 경로가 생겼다. 릭 76행 결과는 불변
(도달 도구·파라미터 완전 일치), 회귀 테스트 5종 추가.
1. 금액 '조(兆)'가 조문번호로 읽힘 — "예산 3조" → get_law_text{jo:제3조}.
`제` 생략 표기를 무조건 허용한 탓. 법령명(…법/령/규칙/규정/조례/법률) 뒤에서만 인정하도록
좁혔다. 겸사겸사 `(.+?)` 접두를 걷어내 $ 앵커와 만나 생기던 제곱 backtrack 도 제거
(캡처를 쓰지도 않았다 — 법령명은 extractLawName 이 따로 뽑는다).
2. `전문` 부분매칭이 전문가·전문의·전문성을 잡아 full=true 를 주입 — 판례 응답의 축약
(토큰 74% 감축)을 통째로 해제하고 있었다. 요구 문맥("판결문 전문", "전문 보여줘")을 요구.
3. 같은 어휘를 판례 검색어 strip 에도 넣어 "전문가"가 "가"로 잘리던 것 — 단어 경계 추가.
4. 자치법규 패턴의 접미사 `도` 제거 — 위험도·만족도·난이도·인지도가 전부 지역명이 됐다.
블록리스트가 죽어 있던 것도 수정: 정규식마다 캡처 위치가 달라 match[1] 이 엉뚱한 값
(패턴2에서는 행정구역 글자 자체)이었다. 이름 있는 그룹으로 바꾸고 접미사 포함 토큰 전체를 본다.
5. 인용검증의 사건번호 패턴이 평범한 날짜를 삼킴 — "2024년 03월"이 `\d{2,4}[가-힣]{1,3}\d{2,7}`에
걸려 사용자 질문이 verify_citations 로 끌려갔다. 년/월/일 부정 예측 추가.
6. `--json` 경로가 pipeline[0] 만 실행 — 다중 조문("민법 제309조·제310조")의 두 번째가
조용히 사라졌다. 기본 CLI 경로는 이미 전 단계를 돌고 있었다.
…egal_research scenario 안내 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
릭 재실행: R 33→34 PASS / FAIL 5→3, b FAIL 2→0, b2 FAIL 2→1, detectScenario divergence 0 유지. 후퇴 0건. #119 검색계 추출 13건을 매칭 캡처 기반으로 통일 (searchQueryFromMatch). match 정규식과 strip 정규식을 따로 쓰던 탓에 공백 처리가 어긋났다 — `유권\s*해석`로 잡고 `유권해석`으로 지워 "유권"이 남고, `용어\s*뜻`로 잡고 `뜻이?|의$`로 지워 "선의"가 "선"으로 잘렸다. 이제 잡은 것(match[0])만 지운다. 트리거가 곧 질의 전부면 원문으로 되돌린다 — "관세 해석"이 빈 검색어로 업스트림에 나가던 문제. 라우터 경계에도 최종 방어를 뒀다. #120 상세도구의 full 수용 여부를 tool-chain-config 에 선언(supportsFull). 12개 중 3개만 받는데 나머지에도 넘겨 Zod 가 조용히 버렸다. 미지원이면 넘기지 않고 RouteResult.unsupportedParams 로 올려 CLI 가 경고한다(dateRange 와 같은 처리). #121 `(.+?)` 접두 2건을 앵커 기반으로 재작성. law_comparison 은 캡처를 쓰지 않으므로 ^ 앵커 + lookahead 2개로, impact_map 은 접두 제거. 실측 9k자 487ms → 1.4ms, 36k자 7.6초 → 3.3ms (제곱 → 선형). 체인 query 스키마에 max(2000) — 평가 세트 최장 정상 질의 145자의 13배 여유. #122 어휘·우선순위 5건. ① amendment 에 바뀌었/바뀐/달라졌/달라진 ② cite_check 어순 역전 패턴 ③ applicable_law 에 '무렵' ⑤ compliance 를 라우팅 트리거로 승격(precedence 4, 조례+저촉/상위법 동반 시에만) — ordinance(5) 선점 해소. ④ 시점 1개("민법 2024")는 RouteResult 확장 없이 처리했다. 새 clarify 필드는 선택 필드이며 라우팅 계약(tool/params/pipeline)은 그대로다 — 개정추적으로 보내되 "그 시점 본문이 필요하면 날짜를 적어 달라"는 확인 문구를 함께 반환한다. 되물음 전용 응답 유형(실행 없이 질문만)은 여전히 미구현 — 계약 변경이 필요하다. #123 양보 가드를 yieldsTo 참조 구조로. 가드에 어휘를 베껴 적지 않고 수신 패턴 (impact_map·applicable_law·verify_citations·law_system·law_comparison·amendment·precedent)의 정규식을 그대로 평가한다. 수신 패턴에 어휘가 늘면 가드도 함께 넓어진다 — "관세법 제38조 위임 조문"이 선점되던 것이 이 파생만으로 해소됐다. yieldsTo 이름은 모듈 로드 시 존재 검증(오타는 가드를 통째로 무력화한다).
라우팅 엔진(query-router)에서 표시 계층을 떼어냈다. 어느 규칙이 왜 이겼는지 보여주는 CLI 전용 출력이라 라우팅 판단과 수명이 다르다. query-router 209→186줄.
#116 — 경계 앵커에 법령명 축을 더한다. 조번호만 보던 탓에 「형법 제1조」 질의에 「군형법 제1조 제3항 제3호 등 위헌소원」(ID 135469)이 유일한 근거로 남았다. LexDiff(search-normalizer)의 정규화·약칭 사전을 **읽기 전용**으로 써서 대조하되, 판정 3값을 둔다: same=확정, different=제외, unknown=**보류(유지)**. different는 확정적일 때만 — ① 하위법령 접미가 다름(민법 vs 민법 시행령), ② 대상보다 길거나 같아 약칭일 수 없음(군형법 ⊅ 형법), ③ 글자 순서가 대상에 들어있지 않아 축약으로 볼 수 없음(민법 ⊄ 도로교통법). 미등록 약칭(도교법)과 법령명을 못 읽는 항목은 전부 보류로 유지한다 — false drop은 이 이슈의 해결이 아니라 새 결함이다. 응답은 "법령명 대조: 확정 N건 / 보류 M건"으로 밝힌다. 실측: 형법 제1조 헌재 1건 확인 → 0건(군형법 제외), 민법 제103조는 확정 4/보류 2로 무회귀. 자기리뷰에서 잡은 false drop 하나를 함께 고쳤다: normalizeAliasKey는 가운뎃점을 `·•`만 지워서, 나머지 3종(ㆍ‧・)이 섞인 같은 법령이 글자 수까지 같은 채로 '확정적으로 다름'이 됐다. 레포가 이미 쓰는 INTERPUNCT_CHARS 5종을 접고 비교한다. #117 — **원인 정정.** 이슈는 "폴백이 조번호를 버리고 재검색"이라고 봤으나, 실측하면 폴백은 발동조차 하지 않는다. target=ordin은 section=ordinNm(자치법규명)만 훑어 1차 질의 "도로교통법 제148조의2"에 곧바로 158건을 돌려준다(2026-08-17 확인). 조번호는 업스트림이 무시한다. 따라서 폴백 고지가 아니라 **조번호 미반영 사실**을 search_ordinance 응답에 붙이고, impact_map의 자치법규 행도 "(법령 단위·조번호 미반영)"으로 이름을 바꿔 수치 옆에서 주장 범위를 좁힌다. #118 — 조 라벨 조립이 의X 조문에 조를 덧붙여 '제401조의2조'가 됐다. 조문 번호 표기는 그대로 재인용되므로 formatJO 정규 표기로 되돌린다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
#108 — resolveAliasToTools의 도구명 판정이 접두사 열거라 cite_check· applicable_law를 도구로 못 봤다. 레지스트리 실재 조회로 바꾼다. 겸사겸사 _allTools 배열을 이름 색인 Map으로 바꿔 선형 탐색 3곳을 없앴다. #109 — 카테고리 미등재 13개 중 11개를 등재한다. ordinance_radar· chain_ordinance_compare→자치법규, chain_law_system→법체계, chain_amendment_track→이력에 편입하고, 결정례통합(search_decisions· get_decision_text)·종합리서치(legal_research·chain_full_research)· 불복준비·정밀분석(legal_analysis)·영향그래프(impact_map)를 신설한다. impact_map은 별칭도 없었으므로 동명 별칭 키를 함께 추가했다. discover_tools·execute_tool은 의도적 제외 — 자기 출력에 자기를 싣는 자기참조이고 둘 다 항상 노출된다(테스트가 이 결정을 고정). #110 — 별칭이 직접 겨냥하는 처분기준·절차매뉴얼·문서분석은 1-hop 진입점 legal_research를 앞세운다. chain_*는 하위호환으로 병기. #111 — intent를 정규화 초입에서 trim한다. 앞뒤 공백이 description 매칭을 막아 `" 판례 "`가 `"판례"`보다 적은 결과를 냈다. 카테고리 직접 매칭의 낱말 경계 통일은 적용하지 않았다: 대표 intent 10개 실측에서 9개가 완전 동일하고 `분석`만 5섹션 9도구→4섹션 5도구로 손실 (비교분석 소멸). 이유와 수치를 코드 주석에 남겼다. 부수 — #109/#110으로 legal_research가 4개 카테고리에 속하게 되면서 600자 description이 한 응답에 최대 4번 실렸다. 렌더링에서 도구 중복을 제거한다 (수수료 4,344B→1,504B, 판례 8,945B→6,118B).
같은 지식이 두 곳에 살면서 한쪽만 낡았다. compact-query-planner의 CASE_CODE_PATTERN은 가사(므·드·르·브·스·너·버·즈·느)·특허(후·허·취)·재심(재)· 개인회생(개회·개확)을 몰라서, 그 사건번호가 **사건번호로 인식되지 않았다** — nb= 정확검색 후보가 만들어지지 않고 키워드 검색으로 흘러간다. 사건부호 목록을 lib/case-citation의 CASE_CODES 하나로 모으고, planner는 COURT_CASE_PATTERN을 가져다 쓴다. 목록은 배열로 두고 길이순 정렬로 패턴을 만든다 — JS 대안은 leftmost-first라 "재"가 "재고합"보다 앞서면 재심 사건번호를 읽지 못하는데, 손으로 순서를 맞추면 부호를 추가할 때마다 같은 함정을 다시 밟는다. 추출(extractCaseNumbers)은 여전히 블랙리스트 방식으로 남긴다 — 화이트리스트로 뽑으면 목록에서 빠진 부호가 무검증 통과하고, 그건 환각 억제 도구에서 가장 나쁜 실패다. 화이트리스트는 부존재를 단정하는 자리(isImpossibleCaseNumber)에서만 쓴다. 무회귀: 전체 스위트 264, 소비 경로(precedent-search-core) 실호출로 키워드 질의· 사건번호 질의 모두 종전 동작 확인. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
#115 — 업스트림 본문이 `MCP_MAX_UPSTREAM_BODY_BYTES`(기본 2 MiB)를 넘으면 도구 호출이 빠른 에러가 아니라 **무한 정지**했다(실측 300초 타임아웃, v4.11.0 신규 경로). 원인은 웹 스트림의 tee 규약이다. `response.clone()`은 본문을 tee하는데, tee의 한쪽 가지만 취소하면 그 취소 프라미스는 **나머지 가지가 취소될 때까지 settle되지 않는다.** 정리 코드가 그 취소를 await하고 있었다. 로컬 mock으로 두 정지 지점을 계측 확인: Content-Length 거부 경로는 `readResponseBytes`의 body cancel에서, chunked 경로는 `cancelReader`에서 멈춘다(각각 15초 관측 후 강제 중단, "AFTER" 마커 미출력). - 정리용 취소는 `abandonReader`/`abandonBody`로 바꿔 **절대 await하지 않는다.** 취소는 정리일 뿐이므로 완료를 기다릴 이유가 없다. - 본문 훔쳐보기가 전체를 복제해 읽던 것을 `readBodyPrefix`(1 KB)로 바꿨다. 빈 본문이냐 HTML이냐는 첫 몇 바이트에서 갈린다. 같은 뿌리의 낭비도 함께 사라진다: 1.8 MB 법령 전문이 예산에 두 번(3,600,064 B) 청구되던 것이 한 번(1,800,032 B)이 된다. 프로브 구간이 전부 공백이고 본문이 더 남았으면 "빈 본문"으로 단정하지 않는다. - `ensureResponseBodySize` 메시지에 실측 크기와 한도, 조절할 환경변수명을 함께 적는다. #114 — `checkEmptyResponse`가 `searchLaw`에만 붙어 있어 다른 검색은 빈 본문을 그대로 파서에 넘겼고, 원인(업스트림 빈 응답)이 증상("missing root element")으로 바뀌었다. 가드를 메서드마다 붙이면 새 메서드에서 잊히므로 `readBody()` 단일 통로에 묶었다. 실측(로컬 mock, 업스트림 호출 0): | 케이스 | 전 | 후 | |---|---|---| | 3.77 MB + Content-Length | 정지(>15,000ms) | ExecutionLimitError 13ms | | 2.1 MB + chunked | 정지(>15,000ms) | ExecutionLimitError 6ms | | 1.8 MB 정상 본문 예산 청구 | 3,600,064 B | 1,800,032 B | | 선행 공백 4 KB + JSON | 정상(1회) | 정상(1회) | 실측(업스트림): `get_annexes{"lawName":"도로교통법 시행규칙"}` 300초 타임아웃(이슈 보고자 3회 재현) → **3,164 ms 정상 완료**. 트레이스상 원인 본문(lawService MST=283481) 요청이 즉시 중단되고 나머지 경로가 그대로 진행된다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
**계약 변경**: 업스트림 미스의 오류 라벨이 `[LAW_NOT_FOUND]` → `[UPSTREAM_NO_DATA]`로 바뀐다. legal-expert 문안 검토(_workspace/13_legal_wording_review.md) 판정에 따른 것이다: 산문은 부존재를 단정하지 않는데(`반환하지 않았습니다`) 기계 판독 라벨이 `NOT_FOUND`라, **둘이 충돌하면 기계 독자에겐 라벨이 이긴다.** 법제처 점검·과부하 시각과 실재 판례 질의가 겹치면 살아 있는 전원합의체 판결을 "없다"고 자문하게 만드는 거짓 부정 경로다. `LAW_NOT_FOUND` 문자열 매칭 지점을 전수 확인했다 — 정의부(errors.ts)와 본 브랜치의 테스트 1건뿐이고 `ErrorCodes`를 참조하는 호출부는 레포에 없다(파급 0). - `ErrorCodes.UPSTREAM_NO_DATA` 신설. "업스트림이 자료를 주지 않았다"는 관측만 말한다. - 메시지: 확인 재시도가 200ms 1회임을 밝혀 유보를 검증 가능한 한정 진술로 만들고, 두 원인(실제 부존재 / 점검·과부하)을 순위 없이 병기하며 "확정하지 마세요"로 닫는다. - 제안 배열: 부존재 단정 금지를 1번, 기존 추측 금지를 2번에 두어 **두 방향을 함께** 막는다. 조치는 "재시도 → 그래도 같으면 ID 확인" 순서로 합쳐 기본 원인을 두지 않는다. #112 — 판례 미스가 확정된 뒤에도 HTML 폴백을 한 번 더 돌았고, 그 폴백의 일반 오류가 미스 분류를 덮었다. - 0바이트 미스(`UpstreamRecordMissingError`)면 폴백을 건너뛴다. 국세법령정보 판례는 이 가지로 오지 않음을 실측 확인했다 — ID 615819는 88바이트 `{"Law": "일치하는 판례가 없습니다…"}` 봉투로 와서 `isMissingPrecedentJson` 분기가 폴백을 태운다. 그 복구 경로는 테스트로 못 박았다. - 폴백 페이지가 그 판례를 갖고 있지 않을 때만 `PrecedentAbsentError` → `[NOT_FOUND]`. 두 원천이 나란히 없다고 답한 경우라 부존재 판단에 근거가 있다. 폴백 기구 자체의 고장(iframe·ntstDcmId 누락)은 계속 외부 API 오류로 남는다. #113 — `get_law_system_tree`가 lsStmd를 XML→JSON 직렬 호출해 히트가 11초를 넘었다. JSON에도 행정규칙이 있지만 형제 법령 아래 흩어져 있을 뿐 최상위 블록이 없어(실측) 한 호출로는 현재 출력을 만들 수 없다. 두 응답은 서로를 쓰지 않으므로 병렬화했다. `allSettled`인 이유는 한쪽 거절이 미처리로 남지 않게 하려는 것이고, 둘 중 하나라도 실패하면 기존과 동일하게 도구 전체를 실패시킨다 — 행정규칙이 조용히 빠진 체계도를 성공처럼 돌려주지 않는다. 출력 형식·바이트는 그대로다(2,750 B). 실측(로컬, LAW_OC=openapi, n≤3, undici 트레이스 동반): | 케이스 | 전 | 후 | 업스트림 | |---|---|---|---| | `get_decision_text{prec,000000}` | 2,707/2,923/3,073ms | 2,169/2,254ms | 3회 → 2회 | | `get_law_system_tree{284415}` 히트 | 10,630/11,209/11,556ms | 6,349/6,922/8,312ms | 2회(직렬→병렬) | | `get_law_system_tree{99999999}` 미스 | 1,267/1,275/1,527ms | 2,146/2,438ms | 2회 → 4회 | 미스가 4회로 는 것은 병렬화의 대가다(JSON 사다리도 함께 발사). 히트가 흔한 경우이고 벽시계는 v4.11.0 기준(10.5초) 대비 여전히 4배 이상 빠르다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
search.ts 에서 잘라 나온 search-fallbacks.ts 의 지역 판정은 행정구역 접미사만 봤다. 같은 배치가 query-extract 에 세운 NON_REGION_TOKENS(#104)를 모른 채 살아서 법령 0건일 때 "재청구 절차"가 자치법규 폴백을 태우고 [FALLBACK] 안내를 달았다. 블랙리스트를 베끼지 않고 isRegionToken 을 import 한다 — 어휘가 늘면 양쪽이 같이 는다. 재현: looksLikeOrdinanceQuery("재청구 절차") === true (isRegionToken 은 false 인데도) looksLikeOrdinanceQuery("선거구 획정 기준") === true 수정: 둘 다 false. "광진구 복무 조례"·"서울시 주차"는 그대로 true 테스트: src/tools/search-fallbacks.test.ts 신규 9케이스 (RED-before 3건 실패 확인) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
route-patterns.ts 806→801, time-travel-diff.ts 212→210. 내용은 그대로. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
체계 확정 (추측 아님 — 2026-08-17 licbyl 실호출, totalCnt=264 의 실번호 목록):
[002800] 별표 운전면허 취소·정지처분 기준(제91조제1항관련) → 별표 28
[012800] 서식 (강사, 기능검정원)자격증 기재사항 변경 신청서 → 별지 제128호
[002003][002004][002005] 어린이통학버스 안전교육 … → 별지 제20호의3·의4·의5
[001708]~[001712] 음주운전 방지장치 … → 별지 제17호의8 ~ 의12
→ AAAABB = 번호 4자리 zero-pad + 가지번호 2자리(없으면 00). JO 코드와 같은 체계이고
parseLawNameAndHint 의 "별표 1의2 → 000102" 인코딩과 동일함을 확인했다.
배정된 가설(`buildSelectorCandidates("28")` 가 체계와 어긋난다)은 **반증됐다** —
`{28, 000028, 002800}` 에 정답 `002800` 이 이미 들어 있다. 실호출 [NOT_FOUND] 의
원인은 후보 생성이 아니라 목록 페이징이다(#148: display=100 고정, totalCnt=264,
002800 은 2페이지). 수정 후 같은 실호출도 여전히 [NOT_FOUND] 이며, 이는 #148 이
남아 있기 때문이다. 지시대로 #148 은 건드리지 않았다.
대신 같은 자리에서 확인을 요구받은 "'1의2' 계열과의 일관성" 검사에서 **실제 결함**이
나왔다. annexNo 경로는 표기 파서를 거치지 않아 가지번호를 통째로 삼킨다:
buildSelectorCandidates("1의2") → {1의2, 1, 000001, 000100} ← 000102 없음
extractSelectorNumbers("1의2") → ["1"] ← "[별표1]" 제목에 매칭
findMatchingAnnex(list, "1의2") → 000100 (별표 1) ← **다른 규범을 정답인 양 반환**
NOT_FOUND 보다 나쁜 무음 오선택이다. 번호부 문법을 annex-notation 단일 원본
(parseAnnexNumber · toAnnexCode)으로 올리고 buildSelectorCandidates·
extractSelectorNumbers 가 그것을 쓰게 한다. 가지번호가 있으면 6자리 코드만 후보로
낸다 — 본번을 함께 내면 가지 별표가 목록에 없을 때 본번이 조용히 잡힌다.
수정 후: "1의2"·"별표 1의2"·"제1호의2"·"별표 제1호의2" → 000102 (lawName 경로와 수렴)
findMatchingAnnex(list, "1의2") → 000102, "1" → 000100
annex-select.ts 267→271, annex-notation.ts 44→61 (둘 다 200줄 미만).
테스트: annex-select.test.ts +8케이스 (RED-before 7건 실패 확인)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
`api-client.getAnnexes` 가 display=100 고정 + page 미지정이라 100건을 넘는 법령에서 뒤쪽 별표가 annexList 에 애초에 담기지 않았다. 라우팅이 별표번호·query 를 보내게 된 뒤(#103·#130 + gestell 지적 2) 이 창이 곧 개선의 천장이었다. 업스트림 실지원 확인 (LAW_OC=openapi, licbyl, 도로교통법 시행규칙 totalCnt=264): display=300 page=1 → numOfRows=100 returned=100 ← display 상향 불가(100에서 잘림) display=100 page=3 → numOfRows=64 returned=64 ← page 지원, 002800 포함 display=100 page=4 → numOfRows=0 returned=0 ← 끝 페이지가 깨끗이 0 → display 는 못 올리고 page 는 된다. page 로 이어 받는다. 봉투 파싱 + 수집을 tools/annex-list.ts 로 분리하고(annex.ts 421→415) totalCnt 를 읽어 필요한 만큼만 돈다. 무한 루프 방어 3겹: for 의 페이지 상한 · 목표 건수 도달 · "새로 담긴 항목 0건이면 중단"(업스트림이 page 를 무시하고 같은 페이지를 돌려주는 경우). 1페이지 요청은 page 파라미터를 붙이지 않아 기존 요청과 바이트 동일하다. 상한 5페이지(500건) 근거 — 요청 예산 MCP_MAX_UPSTREAM_REQUESTS 기본 48회. get_annexes 는 목록 사다리(최대 4단계)·현행 본문 병합·별표 파일 다운로드를 함께 쓰는데, 사다리 2~4단계는 앞 단계가 0건일 때만 도는(=totalCnt 0 → 추가 페이지 없음) 배타 경로라 목록 수집이 실제로 쓰는 것은 한 단계 × 최대 5회다. 예산의 약 1/10. 실측 최대 사례가 264건(3페이지)이라 실질 여유도 충분하다. 상한에 걸리면 침묵하지 않고 "500건까지만 수집"을 목록 맨 앞에 명시한다 — 부분 목록이 전체로 읽히면 "그 별표는 없다"는 오답이 된다. 전후 실호출 (LAW_OC=openapi): {lawName:"도로교통법 시행규칙", annexNo:"28"} 전: [NOT_FOUND] 별표 선택값 "28"에 해당하는 항목을 찾을 수 없습니다 (442자, 2,969ms) 후: ■ 도로교통법 시행규칙 [별표 28] <개정 2025.12.2.> 운전면허 취소·정지처분 기준 (제91조제1항관련) 본문 도달 (20,481자, 5,015ms) {lawName:"도로교통법 시행규칙", query:"운전면허 취소"} 전:⚠️ 일치하는 별표명이 없어 전체 목록을 표시합니다 (총 100건) 후: 별표/서식 목록 (총 7건, query="운전면허 취소" 적용) api-client 재시도 설정(DRF_RETRY 백오프·singleRecordLookup 배선)은 무변경. 4차 admbyl 폴백(fetchApi 직접 호출)은 이번 범위 밖이라 단일 페이지 유지 — 다만 totalCnt 를 읽어 truncated 표기는 붙였다. 테스트: src/tools/annex-list.test.ts 신규 10케이스 (RED-before 4건 실패 확인 — 264건 fixture 에서 총 100건 수집 · annexNo 210 NOT_FOUND · query 미매칭) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
#131 action_basis 완주 25.3/20.1초 → 13.7/11.8초 (실배선 n=2, 동일 질의). 단계별 실측으로 원인을 먼저 갈랐다: 기반법령 2.9s / 3단비교 1.6s / 검색3종 0.6s / 별표 3.0s / penalty 시나리오 13.2s — 전부 순차라 그대로 누적됐다. (a) 기반 법령이 정해진 뒤의 4갈래(3단비교·증거검색+상세·별표·시나리오)는 서로를 기다릴 이유가 없어 동시 실행으로 바꿨다. 출력 순서는 조립 단계에서 그대로 지킨다. (b) penalty 시나리오가 이미 같은 법령의 별표(처분기준표)를 싣는데 체인이 또 받고 있었다 — 같은 파일을 두 번 내려받아 파싱하고 같은 표가 두 번 나왔다. shouldFetchAnnexSeparately 로 한 번만 받는다. 응답도 27,304자 → 26,282자로 줄었다. (c) penalty 내부의 개정이력 조회는 lawId 만 있으면 되는데 Promise.all 뒤에서 따로 기다리고 있었다 — 같은 묶음으로 넣었다. 섹션 구성은 중복 별표 하나만 빠지고 동일. 60초 클라이언트 한도 대비 여유가 목표였다. #132 cli-executor 의 verbose 경로가 explainRoute 를 부르며 라우팅을 통째로 다시 계산했다 (날짜 파싱 포함). 이미 계산한 RouteResult 를 넘긴다 — 설명이 실제 실행된 라우팅과 갈릴 여지도 함께 사라진다. #136 DateParseResult.cleanQuery 고아 필드 제거. 소비자는 분해 커밋에서 matched + stripMatchedDate 로 옮겨 갔는데 필드와 그 계산이 남아 있었다. removeTimeExpression 은 stripMatchedDate 전용이라 하나로 합쳤다. #142 explainRoute 가 unsupportedParams·clarify 를 표시한다. CLI 본 출력은 둘 다 보여 주는데 --verbose 쪽이 더 적게 보여 주고 있었다 — 분리가 만든 표류. #130 후속 배선(#133 준비): 조문 동반 별표 질의가 조문값을 jo 로 함께 싣는다. get_annexes 가 jo 를 열기 전까지는 Zod 가 조용히 버리므로 무해하다. 스키마 의존 단언은 shape 에 jo 가 생기는 순간 스스로 활성화된다 — 수동 토글 없음.
#147) #124 자치법규 조문 관련성 — 이름으로 못 좁히는 것을 본문으로 좁힌다. 상위 N건(기본 10)의 본문을 병렬로 열어 조문 경계 앵커(법령명 축 포함)로 인용 여부를 확인하고, 확인분을 앞으로 올린다. **미확인은 버리지 않는다** — 확인 못 한 것을 무관으로 단정하면 #117에서 고친 과대주장을 반대 방향으로 되풀이한다. 비용이 설계의 전부라 옵트인이다: 실측 건당 1.03s / 3.4KB, 10건 병렬 = 벽시계 1.9s·요청 예산(기본 48회)의 약 1/5. 본문은 lawCache에 태워 재질의는 업스트림을 다시 때리지 않는다(테스트로 고정). #137 사건번호 추출 오탐 — 블랙리스트라 산문의 `숫자+한글+숫자`가 전부 사건번호가 됐다 ("1000억원2024", "9001인증2020"). ✗ 단정까지는 안 갔지만 "판례 인용 N건"을 지어내고 확인 상한 5건을 쓰레기가 소진해 **진짜 인용이 ⊘ 미검증으로 밀렸다** — 모듈 자신의 원칙("누락보다 오도가 문제다")에 걸린다. 놓침과 오탐은 서로 다른 자리의 위험이라 두 판단을 분리한다: 추출은 실재 부호(CASE_CODES)로 한정하고, 부존재 단정은 거기서 한 번 더 좁힌다. #125가 부호 목록을 단일 원본으로 넓혀둔 덕에 가능한 교환이다. 덤으로 근거 없이 '호(가정보호)'라 적었던 주석·테스트를 실재 부호(너·버)로 정정했다. #139 extractCitedLaws 접미 — `조례`가 빠져 조문이 인용한 조례가 그래프에서 사라졌다. 접미 목록을 article-anchor에서 export해 공유한다(따로 적으면 한쪽만 낡는다). 같은 정규식의 인접 결함도 고쳤다: 접두에 2자를 요구해 「민법」·「형법」처럼 가장 많이 인용되는 두 글자 법령명이 **통째로 안 잡히고 있었다**. #140 조문 표기 — 경고 트리거가 `제`를 요구해, #103이 새로 지원한 "44조"·"3조의3" 표기에서만 경고가 사라졌다. 판정을 query-extract.extractArticleNumber로 통일한다. #135 looseMatchLawName 잔여물 — 재수출이 근거로 든 두 소비자(applicable_law·impact_map)가 모두 사라졌고 테스트 하나가 붙잡고 있었다. 재수출·주석을 지우고, 테스트를 살아있는 소비자(verify-citations.verifyOne의 후보 확정)를 명시해 law-search로 재앵커했다. #147 원숫자 ⑮ — 상류 대조 완료(2026-08-17). chrisryugj/lexdiff@f15d400 lib/citation-content-matcher.ts:42는 실제로 ⑮에서 멈추고 로컬 이식본과 동일했다. 다만 상류 자신이 이 파일에서만 ⑮이고 나머지 전 모듈은 ⑳까지 쓴다 — 상류 내부 불일치다. 제16항 이상이 원문자로 남으면 정규화가 조용히 실패해 인용 대조가 어긋나므로, 원본 확인을 마친 근거(규칙 1 충족)로 CIRCLED_DIGITS 단일 원본을 참조해 확장한다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
응답이 대표 intent 기준 최대 10.7KB까지 부풀어 있었다. 원인 셋을 함께 고친다. (1) 노출 도구 설명 재인쇄 중단 — V3_EXPOSED 의 description 은 ListTools 로 이미 클라이언트 컨텍스트에 들어가 있다. 게다가 legal_research(640자)· legal_analysis(480자)는 거의 모든 법률 어휘를 담고 있어 어지간한 intent 의 description 매칭에 다 걸린다. 이름과 호출 방식 한 줄로 대체한다. (2) 티어 랭킹 + 섹션 상한 5 — 별칭 도구(0) > 별칭 카테고리(1) > 카테고리명(2) > description(3) 순으로 정렬하고 상한을 건다. 동률은 안정 정렬로 기존 삽입 순서 유지. 상한값 근거(실측): 반드시 잔존해야 하는 30건(대표 intent 10 + 별칭 20)의 정답 카테고리가 전부 1~2위에 온다 — 5는 2.5배 여유다. 2까지 낮추면 80%↓지만 `법령`에서 법령연계·법체계 같은 유효한 2차 카테고리까지 잘린다. 잘린 수는 반드시 고지한다(무음 절단 금지). (3) resolveAliasToCategory 단일 승자 → 다중 — 첫 매칭 하나만 반환해 나머지 별칭 카테고리가 증발했다. `대법원 판례 유효성`이 판례만 주고 판례생사를 잃던 케이스. 매칭·랭킹은 lib/tool-discovery 로 분리했다. 경계 기준은 줄 수가 아니라 결이다 — 어휘 매칭은 tool-profiles 옆에, MCP 표현·실행은 meta-tools 에. 레지스트리 의존은 ToolLookup 콜백으로 역전시켜 역참조가 0이다(meta-tools 192→117줄). 실측(대표 intent 10개, mcp-call): 법령 19섹션 10,714B→5섹션 2,492B(77%↓), 판례 13섹션 6,633B→5섹션 1,934B(71%↓), 16개 intent 합계 53,726B→19,054B(65%↓). 정답 카테고리 잔존 10/10, 조사 부착 10쌍 무회귀 10/10. 실측으로 반증된 두 가지는 그대로 두고 근거를 주석에 남겼다: description 부분일치와 카테고리 직접 매칭에 낱말 경계를 걸면 안 된다(`심판` 3섹션→0 전멸, `분석` 5섹션 9도구→4섹션 5도구 손실).
무매칭이면 카테고리 40개를 통째로 뱉었다(44줄 1,301B). 정작 소비자는 그중
무엇이 자기 질의에 가까운지 모른다 — 상한·포인터화와 같은 원칙으로 줄인다.
(a) 표면이 가까운 카테고리 상위 3개를 짚는다. 문자 바이그램 Dice 계수를
카테고리명과 별칭에 걸어 최대 점수를 쓴다. 컷오프 1/3 실측 근거:
`세금`→조세심판(0.50), `영문볍령`→영문법령(0.33), `자치볍규`→자치법규(0.33),
`조새심판`→조세심판(0.33)이 구제되고, 진짜 미등록 어휘(하자·손해배상·특허·
근로시간)에는 아무것도 안 걸린다. 0.25로 낮추면 `환경영향평가`→영향그래프
같은 헛제안이 들어온다.
(b) 넓혀 볼 방향 한 줄 — 1-hop 노출 도구를 담은 카테고리부터 제시해,
제안이 곧 가장 싼 경로가 되게 했다. 하드코딩이 아니라 V3_EXPOSED 파생이라
도구 노출이 바뀌면 안내도 따라 바뀐다.
실측: 하자 1,301B → 204B (84%↓), 손해배상/특허/ㅁㄴㅇㄹ 204~210B.
`세금`은 종전 1,301B 벽만 받던 것이 241B로 줄면서 조세심판을 짚어 준다.
기존 무매칭 계약("찾지 못했습니다")은 그대로 유지.
#141 — 같은 질문에 세 곳이 서로 다른 술어로 답하고 있었다. 재시도 계층은 앵커+대소문자 무시(`/^<!doctype html/i`), `api-client`는 비앵커+대소문자 구분(`includes("<!DOCTYPE html")`), `precedents`는 또 다른 substring 판정. 소문자 doctype 으로 시작하는 본문이 한쪽엔 HTML이고 다른 쪽엔 아니었다. `lib/body-shape.ts` 를 세워 셋을 한 곳에서 답한다. 다만 **하나로 합치지는 않았다** — 셋은 서로 다른 질문이다: - `isBlankBody` / `isHtmlPage`: "응답이 통째로 웹 페이지인가" (오류 판정, 앵커) - `containsHtmlMarkup`: "이 문자열이 마크업을 담고 있는가" (내용 판정, 비앵커) 합치면 "내용에 HTML이 섞였다"와 "응답이 점검 페이지다"가 같은 답을 받는다. 부수 효과로 `checkHtmlError` 의 오탐이 사라진다: 법령 본문 CDATA 안의 `<html>` 조각을 점검 페이지로 오인해 정상 검색 결과를 통째로 버리던 경로가 테스트로 고정됐다. 소문자 doctype 안내 페이지는 이제 잡힌다. #146 — `UpstreamRecordMissingError` 는 미스 모양을 3값으로 아는데(빈 본문 / 권한 안내 HTML) 표면은 원인을 둘로 단정했다. lsStmd XML 경로는 실제로 열려 있어서, API 신청이 안 된 OC 키로 체계도를 부르면 "잠시 후 재시도"만 안내받고 영원히 낫지 않는다. `kind` 를 에러에 보관해 `html` 일 때 셋째 원인 "(c) 이 인증키(OC)로 해당 API가 신청·승인되지 않음"과 open.law.go.kr 안내를 덧붙인다. 부존재 단정 금지·추측 금지 문안은 그대로 둔다. #138 — `ErrorCodes.ANNEX_BODY_UNAVAILABLE` 추가. 대괄호 라벨은 기계 독자에 대한 계약이라 상수 밖에서 만들지 않는다(사용처 교체는 별표 커밋에서). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
체인 쪽의 `scenario !== "penalty"` 하드코딩이 이미 새 것을 놓치고 있었다. 별표를 조회하는 시나리오는 penalty 하나가 아니라 셋이다 — customs(세율표), action_plan(별표/서식)도 같은 법령(ctx.law)의 별표를 싣는다. 즉 chain_full_research 에서는 중복이 그대로 살아 있었다. RED 로 재현 후 수정했다. - scenarios/types.ts: ScenarioResource 타입 신설. - 각 시나리오 모듈이 자기가 싣는 것을 선언한다(penalty·customs·action-plan 의 PROVIDES). 선언이 구현 옆에 있어야 같이 움직인다 — 체인 쪽에 목록을 두면 새 시나리오마다 재발한다. - scenarios/index.ts: 선언을 모아 scenarioProvides(type) 로 제공. - chains.ts: shouldFetchAnnexSeparately 가 이름 대신 선언을 읽는다. chain_full_research 의 별표 블록에도 같은 가드를 적용(서식 포함). - 드리프트 가드 테스트: getAnnexes 를 import 하는 시나리오 모듈이 annex 를 선언하지 않으면 실패한다. 선언 누락으로 중복이 되살아나는 경로를 막는다. 잔여 체인 3종은 측정만 하고 손대지 않았다(15초 미만 — 측정 없는 개선 금지): chain_procedure_detail 6.6초 / chain_dispute_prep 2.2초 / chain_law_system 2.2초. action_basis 실배선은 전후 동일(12.7·13.6초, 응답 26,282자 53,123바이트 바이트 동일).
#92가 최상위에서 세운 계약이 아래 단계들에 관철되지 않았다. - `schemas.ts` summary 모드가 초과분을 `slice(0, maxSize)` 로 하드컷했다. 같은 커밋이 하드컷을 없애려고 `cutAtSafeBoundary` 를 만들어 놓고 이 경로만 빠뜨려, `📋 요약 모드` 꼬리가 한가운데서 끊겼다. 경계 절단으로 바꿨다. - `truncateSections` 의 섹션별 안내문(`⚠️ (이 섹션 …축약)`)이 `cutAtSafeBoundary` **뒤에** 붙어 섹션마다 `perSection` 을 안내문 길이만큼 넘었다. #92가 최상위에서 고친 것과 같은 유형이 한 단계 아래 남아 있었다. 예산에서 먼저 빼도록 정렬했다. - `meta-tools.ts` `discover_tools` 반환에 `truncateResponse` 가 없었다. 현재 레지스트리 크기로는 5만 자를 넘을 수 없지만, 도구 출력이 상한을 자기 책임으로 지킨다는 규칙(Critical Rule 5)에서 이 return 만 빠져 있었다. - `legal-research.ts` `withNote` 가 노트를 **별도 블록**으로 얹어, 내부 도구가 이미 상한까지 채운 응답이 노트 길이만큼 상한을 넘었다. 첫 블록에 합쳐 넣고 다시 상한을 적용한다 — 잘려야 할 것은 경고가 아니라 뒤쪽 본문이다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
**N1 — 단일 원본이 자기 호출자보다 좁았다.** `annex-notation` 이 "이 레포의
단일 원본"을 선언하면서 어휘는 둘(`별표`·`서식`)만 알았고, 라우팅 트리거는
넷(`별표`·`서식`·`양식`·`별지`)을 인정했다. `별지 제3호` 는 번호가 떼어지지
않아 `annexNo` 대신 법령명/`query` 로 샜다. 어휘를 `ANNEX_KEYWORDS` 하나로
올리고 꼬리의 `서식`(`별지 제3호서식`)까지 소비한다. `annex-select` 의 네
번째 사본(`annexQueryKeywords`)을 지웠고, **자체 점검에서 다섯 번째 사본을
더 찾아**(`titleMatchesAnnexNumber` 가 `별표|서식` 만 알아 이 파일이 대표
사례로 드는 `[별지 제4호서식]` 제목이 제 번호에 안 걸렸다) 함께 세웠다.
판정 기준으로 요구된 `별지 제20호의3`=002003 · `별지 제17호의12`=001712 확인.
**N7 — encode 를 세운 커밋이 decode 를 남겨 뒀다.** `fromAnnexCode` 를 짝
연산으로 두고 `extractSelectorNumbers` 가 6자리 코드를 `17의12` 로 읽게 했다.
응답이 알려 준 코드를 사용자가 되돌려주는 왕복 경로가 죽어 있었다.
**N2 — 중복 방어 키가 발행 주체를 안 봤다.** 수집 목록은 여러 법령·자치법규가
섞인 상태로 온다(그래서 뒤에 `filterByRelatedLawName` 이 돈다). 자치법규의
`[별표1] 과태료 부과기준` 은 지자체마다 있는 흔한 이름이라, 주체를 빼면 서로
다른 지자체 별표가 서로를 지우고 "새 항목 0건" 방어가 정상 수집을 끊었다.
identity 에 `관련법령일련번호`(없으면 자치법규명·법령명·행정규칙명)를 더한다.
스텁 재현: 264건 중 100건에서 break → 264건 전량 수집.
**N3 — 절단 안내가 실제로 일어난 일과 무관했다.** 조건 없이 "500건까지만"을
출력해, 100건에서 끊긴 경우에도 틀린 원인과 "500건까지는 봤다"는 잘못된 안심을
줬다. `AnnexTruncationReason`(`page-cap`·`no-progress`·`unknown-total`)을
실어 사유별 문안과 실제 수집 건수를 적는다.
**N4 — `totalCnt` 부재 시 정확히 100건이어도 `truncated=false`.** 100건은 곧
페이지 크기라 "창 경계에 걸렸을 가능성"이 높은 신호인데, 이 경로에서만 커밋이
내건 원칙("부분 목록이 전체로 읽히면 '그 별표는 없다'는 오답")이 빠져 있었다.
**N5 — 3차 폴백의 `truncated` 가 필터 전 모법 총계 기준이었다.** 표시되는 것은
`normalizedLawName` 으로 거른 부분집합인데 절단 여부는 다른 모집단을 가리켰다.
절단 표시는 **실제로 표시 목록을 채운 단계**의 것만 채택하도록 `adopt()` 로
정리했다.
**#149 — admbyl 폴백만 100건 창에 남아 있었다.** 페이지 수집 정책을
`collectPages` 하나로 모으고 `collectAdminAnnexList` 를 그 위에 세웠다 —
상한·중복 방어·사유가 licbyl 사다리와 같아진다.
**N6 — 자치법규 별표의 자연어 문이 닫혀 있었다.** ordinbyl 분기·
`filterByRelatedLawName`·서울특별시 건축 조례 번호 충돌 사례가 전부 갖춰졌는데
`/조례/`(우선순위 5)가 annex(10)보다 먼저 삼켰다. **우선순위를 흔들지 않고**
#123이 세운 양보 기구(`yieldsTo`)를 썼다 — annex 가 실제로 받아 갈 때만
양보하므로 별표 없는 조례 질의는 그대로 `search_ordinance` 다.
**#133 — `get_annexes` 에 조문 슬롯.** Zod `jo` 를 열고(라우팅 배선은 B2 소관),
별표명의 `(제38조 관련)` 표기와 대조해 좁힌다. `filterByAnnexQuery` 와 같은
계약 — 못 맞히면 전체를 돌려주되 그 사실을 밝힌다. 조용히 0건이면 "그런 별표는
없다"는 오답이, 조용히 전체를 주면 "그 조문의 위임"이라는 오독이 된다.
**#128 — lsStmd XML 제거 가능성: 음성.** 실호출 4개 법령 대조 결과 JSON 은
XML 항목을 전부 포함하지만(XML 에만 있는 항목 0/4건) 두 집합은 같지 않다.
관세법 XML 22건 vs JSON 고유 97건이고, XML 의 22건은 JSON 에서 관세법(9)·
시행령(10)·시행규칙(22)에 흩어져 나타나 **어떤 소유 법령으로도 XML 집합을
재현할 수 없다.** XML 블록은 별도 큐레이션이라 치환하면 표시 집합이 조용히
넓어진다 — 호출 1회 절감보다 그쪽이 위험해 두 호출을 유지하고, 근거를 주석에
남겼다(기존 주석의 "JSON 에는 그 노드가 없다"는 서술도 정정).
**#138 사용처** — `[본문 미추출]` 을 `ErrorCodes.ANNEX_BODY_UNAVAILABLE` 로 교체.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
병렬화로 평균은 35초 → 12초가 됐지만 꼬리는 남았다. 같은 질의가 12초로도 71초로도
끝나는 것을 실측했고(A/B로 코드 기인 아님을 확인), 60초 클라이언트 한도를 넘기면
이용자는 부분 결과조차 못 받고 타임아웃만 본다.
- chain-deadline.ts 신설: 데드라인 시계 + 갈래 경주 + 미완 섹션 마커.
기본 45초 — 클라이언트 60초 한도에서 섹션 조립·truncateResponse(50KB)·전송과
시계 오차 몫으로 15초를 남긴 값이다. MCP_CHAIN_DEADLINE_MS 로 조정(5초~5분).
경계값은 통째로 정수 검사한다("20x → 20" 관용 거부).
- 만료 시: 완료된 갈래는 정상 조립, 미완 갈래 자리에는
"⏱ 시간 한도로 이 섹션은 수집하지 못했습니다 — 개별 도구(<도구명>)로 조회하세요".
침묵 탈락 금지. 부분 결과는 유효한 답이므로 isError 가 아니다.
- 진행 중 요청은 갈래들이 공유하는 AbortSignal 로 함께 끊는다
(runWithRequestContext 로 기존 취소 체계에 얹었다 — 소켓 잔류 없음).
- 데드라인 **전에** 난 실패는 삼키지 않고 그대로 던진다 — 시간 초과와 진짜 오류를
뭉뚱그리면 파싱 실패가 "느렸나 보다"로 둔갑한다.
- 적용: chain_action_basis(4갈래), chain_full_research(시계는 함수 진입부터,
뒤 3갈래를 남은 시간 안에서 경주). 순차 체인 3종은 실측 6.6/2.2/2.2초라 제외.
검증은 전부 mock 지연이다(실제 업스트림 대기 없음): 응답하지 않는 업스트림 +
데드라인 5초로 발동시켜 머리글·마커·대체 도구명·isError 아님을 확인.
실배선은 전후 동일(11.5초, 26,282자).
소유권 seam: 상수·정수 검증을 execution-limits/http-config 가 아니라 이 모듈에 뒀다
(그쪽을 다른 작업이 만지는 중). 검증 함수는 execution-limits.parseIntegerLimit 과
같은 모양이라 통합 시 한쪽으로 합칠 수 있다.
**1. 가지번호 무음 오선택 (수정).** `extractBundledSection` 이 `parseInt("17의12")`
= 17 로 읽어 묶음 문서에서 **별표 17 섹션을 조용히 돌려줬다.** 묶음 문서는 그 옆
번호를 실제로 담고 있으므로 오답이 정답처럼 보이는 자리다 — N7 짝 연산의 마지막 칸.
- 번호부를 `parseAnnexNumber`(annex-notation 단일 원본)로 읽고, 가지번호면
`17\s*의\s*12` 로 섹션을 집는다. 본번호 `17` 은 `[별표 17의12]` 를 삼키지 않는다
(닫는 괄호를 요구).
- 섹션 표제 정규식도 `ANNEX_KEYWORDS` 기반으로 올렸다(`BUNDLED_HEADING_SRC`).
- **못 찾으면 조용한 폴백 대신 명시 실패**: 가지번호를 물었는데 그 섹션이 없으면
묶음 전체를 주지 않고 `[NOT_FOUND]` 로 돌린다. 전체를 주면 그 안의 옆 번호를
답으로 읽기 때문이다. 실패문에 `listBundledSections()` 로 실제 담긴 섹션 목록을
실어 복구 경로를 준다. 본번호는 기존 폴백 유지(옆 번호 오답 위험이 없다).
**2. 별지 묶음 실재 — 음성 (미수정, 주석으로 고정).** licbyl 실호출 2건
(출입국관리법 시행규칙 100건 / 여권법 시행규칙 12건, 2026-08-17)에서 **범위 표기
제목 0건**이었고, 그 표본의 licbyl 제목은 애초에 `별표`·`별지` 접두어를 달고 오지
않았다 — 묶음 판정은 자치법규·행정규칙 제목에서 발화한다. `별지` 묶음의 실재를
확인하지 못했으므로 `isBundledAnnex` 의 `별표` 전용 표기를 근거 없이 넓히지 않고,
표본과 한계를 주석에 남겼다. (섹션 표제 쪽은 1번 수정의 일부로 이미 어휘 기반이다.)
**3. `annex.ts` 병합 실패 삼킴 — 커버됨 (미수정, 테스트로 고정).** 이전 라운드에
내가 "실패 삼킴"으로 올린 지점은 **오보였다.** 두 `fetchLawAnnexUnits` 호출부가
각각 #127 마커로 이어진다 — 목록 경로는 `mergeIssue` → `⚠️ 신설 별표 병합 확인
불가` 배너, 추출 경로는 `canonicalIssue` → `⚠️ 정본 링크 확인 불가` 배너.
목록 경로는 예산 초과를 흉내낸 테스트로 배너 출력을 고정했고(기저에서도 통과 =
커버 증명), 추출 경로는 배너가 파일 파싱 성공 뒤에 붙어 실제 픽스처가 필요하므로
"병합 실패가 도구를 죽이지 않고 licbyl 링크까지 도달한다"까지만 고정했다.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
# Conflicts: # src/tools/meta-tools.ts
…rLimit seam) 4트랙 머지가 끝나 이제 트랙 경계를 넘는 항목을 만질 수 있다. 동작은 한 줄도 바꾸지 않는다. #134 extractLawName 이름 충돌 — 같은 이름, 두 의미, 둘 다 export 였다. lib/query-extract.extractLawName → lawNameFromQuery (라우팅 질의에서 법령명) tools/verify-citations.extractLawName → lawNameFromCitationContext (인용 직전 문맥에서 법령명) 소비자 전부 갱신(route-patterns 8곳·chain-law-lookup·query-extract 내부·verify-citations 본문/테스트). grep 하면 이제 한 답만 나온다. #143 export 정리 — 리뷰 지적 22 목록을 **현재 코드로 전수 재검증**했다. 머지로 소비자가 생긴 것이 넷이라 그대로 지웠으면 빌드를 깼다: 생존 — CASE_CODE_PATTERN(route-patterns 가 사건번호 패턴을 여기서 세운다), COURT_CASE_PATTERN(compact-query-planner) 승격 — detectBadBody·normalizeText·looksLikeOrdinanceQuery 는 테스트 소비자가 생겼다 소비자 0 → export 제거: CASE_CODES · MAX_BOUNDARY_BACKTRACK · matchesAnnexKeywords (셋 다 자기 파일 안에서는 쓰이므로 심볼 자체는 유지) 죽은 import 발견: annex.ts 가 parseAnnexEnvelope 를 import 만 하고 쓰지 않았다 (collectAdminAnnexList 로 갈아탄 잔여물). import 제거 → 그 export 도 파일 안으로. 테스트 전용 8개는 그 사실을 한 줄로 적었다 — 지금까지 "공개 API"와 구분이 안 됐다. #144 유틸 사본 — 이번 배치가 이미 상당 부분 해소했고(annex-notation·body-shape·CASE_NO_SRC), 잔존분만 재조사해 통합한다: escapeRegex 3사본(precedent-body.escapeRe·similar-precedents·annex-select, 바이트 동일) → lib/escape-regex 한 벌. 이스케이프 문자 집합이 두 벌이면 한쪽만 낡는다. dot() → 서식 계산을 formatDateDot 에 위임. 빈 입력 계약만 다른 이유를 주석에 남겼다 (호출자가 `dot(x) || x` 로 원문 폴백을 쓰므로 "N/A"를 돌려주면 그 폴백이 죽는다). 상대 시점 어휘 → date-patterns 에 RELATIVE_PAST_WORDS·RELATIVE_NOW_WORDS 를 두고 query-extract 걷어내기·scenario-rules 판정이 그것을 쓴다(두 곳은 어휘가 이미 동일했다). ARTICLE_TAIL_RE 의 파일 밖 사본(ordinance-search) → query-extract.stripArticleTail 공개 창구. 합치지 않은 것과 그 근거를 주석으로 남겼다: date-patterns 의 `금년` — 여기만 어휘를 연 범위로 **해석**한다. 걷어내기 목록과 목적이 다르다. query-extract 의 좁은 "현행|지금|현재|오늘" 판정 — RELATIVE_NOW_WORDS 로 넓히면 "올해 개정"의 toDate 가 오늘로 바뀐다. 동작 변경이라 리팩터 범위 밖(별도 보고). applicable-law.fmtYmd — 출력 형태 자체가 다르다("2024.1.5." vs "2024.01.05"). route-patterns 의 조문 패턴들 — 앵커·자릿수 상한이 붙은 탐지용이라 추출용과 계약이 다르다. parseIntegerLimit seam — chain-deadline 이 execution-limits.parseIntegerLimit 과 같은 모양의 정수 검증을 자체 보유했다. 한쪽으로 합친다. 경계 검사가 두 벌이면 한쪽만 `parseInt("20x") → 20` 관용을 되찾는다. 상수(5초~5분)는 이 도메인의 것이라 chain-deadline 에 남긴다. 동작 불변 증거 — 스냅샷 105행 전부 바이트 동일 (라우팅 30질의 · 두 법령명 추출기 13케이스 · discover_tools 9의도 응답 전문 · 별표 표기/선택 후보 30 · 폴백 지역판정 6 · env 정수 파싱 13). 실호출 스모크 2건: get_annexes{annexNo:"28"} 20,481자 별표28 본문 도달, legal_analysis{mode:"cite_check"} 2013다61381 정상. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
이 배치는 문서 변경이 0건이었다(`git diff --name-only | grep -v '^src/'` → 빈 출력). 관례상 신규 모듈은 CLAUDE.md에 등재해 왔는데 이번만 빠졌다. CLAUDE.md - 규칙 5: "50KB 제한" → **5만 자(UTF-16 code unit)**. 한글은 UTF-8로 자당 3바이트라 5만 자는 최대 150KB다 — 바이트로 읽으면 한도를 3배 좁게 잡는다(#92). env 이름도 병기. - 규칙 1: `search-normalizer.ts`의 도로교통법 약칭이 **LexDiff 표 직접 편집**임을 명시. 커밋·테스트가 이를 "오버레이"라 불렀는데(리뷰 지적 7) 오버레이가 아니라 배열 안쪽 편집이라 LexDiff 동기화가 clean overwrite로 성립하지 않는다. 코드·테스트의 그 표현은 이미 정리됐고 여기 규칙에 사실을 남긴다. `citation-content-matcher.ts`가 같은 계열임도 함께 적었다. - 규칙 2: `route-patterns.ts` 예외 후보를 **(제안, 미확정)**으로 표기(risk-rules 와 같은 계열 — 본문 대부분이 단일 Pattern[] 데이터). 규칙을 넘긴 파일이 현재 38개라는 현실도 고지. - Structure: tools/ "50개 파일, 각 200줄 미만" → 76개(scenarios 포함). 사실이 아니었다. 약칭 "52개" → 표제 60개(실측). - Environment: `MCP_CHAIN_DEADLINE_MS` 추가 (기본 45s, 5s~5m, 만료 시 부분 결과). - Key Files: 신규 모듈 24개를 다섯 묶음으로 등재 — 라우팅·추출 / 인용·조문 앵커 / 업스트림 경계 / 별표·서식 / 기타 공용. 전부 실재 확인 후. docs/API.md - `get_annexes` 파라미터 표 신설 — `query`(번호를 모를 때)·`jo`(위임 조문)가 문서에 없었다. 별표번호 6자리 코드 체계(AAAABB)와 실측 예시, 목록 페이지네이션 상한도 명시. - `legal_analysis` mode별 필수/선택 파라미터 표 + `jo` 계약(자연어 표기·6자리 코드 모두 수용). - 메타 도구 절 신설 — `discover_tools` 응답 형식(1-hop 앞세우기, 노출 도구 포인터화, 결과에 따라 갈리는 말미 안내, 무매칭 축약)이 문서에 전혀 없었다. - 헤더 버전 v4.9.1 → v4.11.0 (레포 실제 버전과 어긋나 있었다). package.json 범프는 릴리스 결정이라 건드리지 않는다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
applicable_law만 자체 fmtYmd로 "2024.1.5."(월·일 무패딩 + 후행점)를 냈고, 코드베이스 나머지(law-system-tree·historical-law·life-law·time-travel-diff)는 formatDateDot의 "2024.01.05"를 쓴다. 같은 날짜가 도구마다 달리 보이는 상태였다 — 인용 정확성을 파는 서버에서 사용자가 그대로 재인용하는 출력이라 표기가 흔들리면 안 된다. **출력 변경이다.** 사용자 가시 문자열이 바뀐다: "행위시법 판단: … @ 2022.1.27." → "… @ 2022.01.27" "[시행 2010.4.1.]" → "[시행 2010.04.01]" "부칙 <제17907호, 2021.1.26>" → "부칙 <제17907호, 2021.01.26>" applicable_law 응답을 문자열로 검증하던 스냅샷 성격의 기존 단언 3개 (staggered 테스트의 [시행 …] 3곳)를 새 표기로 함께 갱신했다. 부수 효과 하나: 빈 시행일에 대해 종전 fmtYmd는 ""를, formatDateDot는 "N/A"를 낸다. "최초 시행일: " 뒤가 빈칸으로 끝나던 자리에 N/A가 찍히므로 의도한 개선으로 둔다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
두 시점 비교에서 뒤쪽 시점을 가리키는 말이 자리마다 달랐다. "관세법 2024 vs 현행"은 시점 비교로 갔지만 "vs 올해"·"vs 오늘"은 법체계 체인으로, "2024부터 올해까지"는 관세 시나리오로 샜다. 같은 질문이 표현만 바꿔 다른 도구로 간 것이다. - scenario-rules TIME_TRAVEL_TWO_POINTS: 뒤쪽 시점을 date-patterns 의 RELATIVE_NOW_WORDS 에서 파생(현행|지금|현재만 알던 것 → 오늘·올해 포함). 바로 아래 TIME_TRAVEL_RELATIVE 는 이미 같은 원본을 쓰고 있어 둘의 불일치도 사라졌다. - query-extract extractTimeTravel 의 toDate 기본값 판정도 같은 원본에서 파생. Phase 2 가 "넓히면 동작이 바뀐다"며 남겨 둔 자리다 — 릭으로 그 변화를 확인하고 넓혔다. 릭 R 44행 기준 대비 차이 0건(PASS 34 / AMBIG 7 / FAIL 3 불변).
두 문서 모두 v2.3.2(2026-04) 상태로 멈춰 있었다. 틀린 것을 고치고 이 배치가 만든
계층을 더한다 — 재서술은 하지 않았다.
**위험 정정 (최우선)**
- DEVELOPMENT.md 배포 절에 `flyctl deploy`가 그대로 있었다. CLAUDE.md가 🚫로 금지한
바로 그 명령이다 — 실행하면 통합 이미지를 law 단독 이미지로 덮어써 동거 중인
stats·patent·archhub·school까지 죽는다. 통합 호스트 절차(npm publish → Dockerfile 핀
갱신 → gomdori-mcp에서 배포)로 교체하고 금지 경고를 달았다.
- ARCHITECTURE.md도 "Remote (Fly.io) / fly.toml 256MB"로 적혀 있었다. 이 레포에 fly.toml은
없다(통합 호스트 쪽에 있다). 통합 호스트 기준으로 정정.
**사실 정정**
- 버전: 두 문서 v2.3.2 → v4.11.0
- DEVELOPMENT.md:39 `lib/ 13개` → 51개, `tools/ 40개` → 76개(scenarios 12 포함)
- `chains.ts 7개 체인` → 8개
- `server/sse-server.ts` — 실재하지 않는다. 실제는 `http-config.ts`
- 루트 트리의 `fly.toml` 항목 삭제(없는 파일)
- ARCHITECTURE 도구 카테고리 수 갱신(분석 9→10, 헌재/행심 6→8, 기타 4→6, 체인 7→8) +
누락 카테고리(연계·조약·학칙·특별행심·메타·킬러) 추가, `V3_EXPOSED 10개만 노출` 명시
- CLI 설명이 v1 기준이었다 — 자연어 한 줄 라우팅·REPL이 빠져 있었다
- 성능표의 레이턴시 수치에 "SLA가 아니다 · 실측은 UPSTREAM-PERF.md" 단서 추가
**새 계층 추가**
- ARCHITECTURE 다이어그램에 두 층 신설: 라우팅·추출(query-router/route-patterns/
query-extract/scenario-rules/date-*), 업스트림 경계(execution-limits/response-body/
body-shape/upstream-miss/chain-deadline)
- Component Deep Dive: 라우팅 · 실행 예산(한도표 4종 + env) · 미스 판정("실패도 HTTP 200") ·
체인 데드라인(부분 결과는 isError 아님) 절 신설
- Data Flow에 Pattern 5(자연어 라우팅) · 6(200인 실패 → 미스 확정) · 7(데드라인 → 부분 결과)
추가. Pattern 4(별표)는 페이지네이션·표기 정규화·정본 대조 단계를 반영
- 아키텍처 원칙에 Bounded Work(요청 단위 예산)와 Single Source per Domain Vocabulary 추가
- Security에 실행 예산·Origin 검증·scrubError/maskSensitiveUrl 추가
- DEVELOPMENT: 검증 루프(`npm test` / `typecheck` / `dead-code` / `gc`)와 스텁 주입
테스트 관례, console 금지·truncateResponse·단일 원본 규칙을 코드 규칙에 반영
(원본은 CLAUDE.md Critical Rules임을 명시)
코드 무변경. 618/618 통과 확인.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0137aUPTK5X7rxGUGTBrezMS
chrisryugj
added a commit
that referenced
this pull request
Aug 16, 2026
통합 배치(#150) 머지 전 4개 도메인 독립 리뷰에서 실측 재현된 결함을 수리한다. 전건 RED-before 확인 후 수정, 701 테스트 통과(base 623 → +78). ## 부존재 오단정 (이 계층의 최악 결함 클래스) - fetch-with-retry: 미스 확정을 시도 순번(attempt>=1)이 아니라 비정상 본문 관측 이력(sawBadBody)으로 판정 — 503·네트워크 오류 뒤 첫 빈 본문 1회로 확정되던 경로 차단 - precedents: HTML 폴백의 부존재 판정에 양성 신호(precSeq 마커 존재) 요구 — 마커 없는 점검·안티봇 페이지를 NOT_FOUND로 단정하던 격상 회귀 해소 - annex: getAnnexes에 checkHtmlError 적용 + 사다리가 "장애"와 "정상 0건"을 구분, 장애 관측 시 UPSTREAM_NO_DATA로 응답. ExecutionLimitError·취소는 즉시 전파 - api-client: singleRecordLookup을 endpoint→target 화이트리스트(prec/thdCmp/lsStmd)로 ## 별표 선택 - 가지번호(1의2) 요청은 단일 폴백·본번 후보 생성 금지 (별표 1 무음 오선택) - 6자리 코드를 fromAnnexCode로 해독 ("000102"→"102" 오매칭) - 호 중위 표기 매칭 ([별지 제59호의2서식], ## [별표 제1호의2]) ## 인용 검증 (false positive 방지) - article-anchor: "구 OO법" 접두는 different가 아니라 hold — 위헌심판 행위시법 인용의 false drop 해소 (군형법 vs 형법 확정 제외는 유지) - 제외 사유를 조문 축·법령 축으로 분리 표기 - case-citation: 수량 산문("2030도3000명") 추출 차단, 구 파산 부호 '파' 보강 - cite-check: 입력과 다른 판례가 특정되면 경고 병기 - time-travel-diff: 목(目)·조문 직속 호 합산 — 목만 개정 시 "동일" 무음 누락 ## 라우팅 - statistics가 양보 경로를 가로채 법령명·조문 전량 소실하던 결함 (_skip 양보) - 접속어 제거의 법령명 절단 ("과학기술기본법"→"학기술기본법") - cli-executor: dateRange 주입이 time_travel 앵커를 덮어쓰지 않게 (빈 자리만) - 지역 오인(지역구·놀이기구), 심판례 역선점, 고아 음절("결정문"→"문") ## 체인·응답 - 데드라인이 프리픽스를 포위 — 5초 설정에 19.5초 실행되던 구조를 부분 반환으로 (action_basis·full_research·dispute_prep 3종, 단계별 수신분 보존) - 요청하지 않은 섹션에 붙던 가짜 타임아웃 마커 제거 - MCP_CHAIN_DEADLINE_MS 부팅 시점 검증 (무증상 부분 장애 방지) - 서로게이트 쌍 반토막 절단, 요약 모드 꼬리 소실 해소 - discover_tools 티어 랭킹이 삽입 순서로 결정례통합을 자르던 문제 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DtUwA5jAL7FfAGwmf6zwF
Owner
chrisryugj
added a commit
that referenced
this pull request
Aug 16, 2026
PR #150 머지와 후속 하드닝을 릴리스로 묶는다. 테스트 196 → 701. 사용자 가시 변경 2건: 날짜 표기 통일(2024.1.5. → 2024.01.05, 빈 시행일 N/A), discover_tools 응답 형식(포인터화·티어 랭킹·섹션 상한 — 16 intent 65% 감축). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DtUwA5jAL7FfAGwmf6zwF
chrisryugj
added a commit
that referenced
this pull request
Sep 23, 2026
- verify_citations: 법령명은 요청당 한 번만 풀고(요청 단위 메모) 동시성 8로 검증. 인용 15건이 예산(48회) 소진으로 ✓0/⚠15 이던 것이 업스트림 16회·✓15 - withChainDeadline·pushLeg 헬퍼로 #131·#150 규칙을 한 곳에. legal_research 8종 중 document_review 를 뺀 7종이 데드라인을 쓰고, 기반 법령 뒤 독립 갈래는 동시에 출발해 원래 순서로 조립한다 - full_research 가 데드라인 때 이미 받은 해석례를 버리던 것, impact_map 인용 검색 실패의 "0건", cite_check 상세 조회 실패의 "✅ 계속 인용", 기반 검색 장애의 "법령 없음" 등을 명시적 실패로 - penalty 시나리오: 전체 목차 대신 벌칙 장·과태료 조문만(전문 1회 수신), 빈 본문은 실패로. delegation 의 죽은 갈래 제거, 판례 상세 요청 단위 메모, 같은 AI 검색 중복 제거 - findLaws: 요청 안의 같은 검색 공유, 공백만 다른 같은 검색 재호출 방지, 비법령 키워드 제거의 공백 백트래킹 제거(2만 자 663ms → 0.1ms) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

요약
법률 자문 시스템 관점의 3축 실측(법적 정합성 · 토큰 효율 · 응답 성능)에서 출발해, 등록 이슈 62건(#88~#149)을 전량 해결한 통합 배치다. 모든 수정은 재현 → 수정 → 재측정 사이클과 RED-before 회귀 테스트를 거쳤고, 영역별 적대적 자기리뷰 + 통합 diff에 대한 독립 코드 리뷰 3단(본리뷰 → 수정 → 델타 재리뷰 ×2)을 통과했다. 이슈 전제가 실측과 다른 경우는 고치는 대신 반증했다(#111 · #117 · #128 — 근거는 각 이슈 코멘트).
축별 핵심 결과 (로컬 실측, base v4.11.0/e667e9c)
EXTERNAL_API_ERRORUPSTREAM_NO_DATAget_three_tier/get_law_system_tree미스get_law_system_tree히트 (#113)1의2→별표 1 무음 오선택해결 이슈
Closes #88, closes #89, closes #90, closes #91, closes #92, closes #93, closes #94, closes #95, closes #96, closes #97,
closes #98, closes #99, closes #100, closes #101, closes #102, closes #103, closes #104, closes #105, closes #106, closes #107,
closes #108, closes #109, closes #110, closes #111, closes #112, closes #113, closes #114, closes #115, closes #116, closes #117,
closes #118, closes #119, closes #120, closes #121, closes #122, closes #123, closes #124, closes #125, closes #126, closes #127,
closes #128, closes #129, closes #130, closes #131, closes #132, closes #133, closes #134, closes #135, closes #136, closes #137,
closes #138, closes #139, closes #140, closes #141, closes #142, closes #143, closes #144, closes #145, closes #146, closes #147,
closes #148, closes #149
한계 고지
chain_full_research는 진입부 블록이 데드라인 밖(부분 경계 — 주석 명시).route-patterns.ts(규칙 테이블)는 예외 후보로 CLAUDE.md에 "제안, 미확정" 표기. 기존 위반 38개 파일은 현실 고지만.2024.1.5.→2024.01.05, 빈 시행일""→"N/A"), discover_tools 응답 형식(랭킹·상한·포인터 — docs/API.md에 신설 문서화).검증
flyctl deploy안내 제거 포함)