Commit Graph
106 Commits
Author SHA1 Message Date
kimandClaude Sonnet 5 54461fd26b fix: weather_openmeteo가 maxHourlyRows로 자른 걸 숨기던 문제
hours=168처럼 도구 스펙상 정상 범위(최대 384)를 요청해도 시간별 표는 maxHourlyRows(기본 48)에서
조용히 잘렸는데, 헤더는 여전히 '지금부터 168시간'이라고 적어 잘렸다는 사실을 모델에게 숨겼다.
그 결과 모델이 표에 없는 뒷부분을 지어내거나(다른 경로로 API를 재호출해 우회) 반대로 '데이터
부족'이라 정직하게 오보고했다(2026-09-27 실측, gemini-3.5-flash-lite로 '한주간 예보' 질의).

maxHourlyRows가 실제 병목(API엔 더 있는데 표만 자른 경우)일 때만 잘렸다고 명시하고 daily 변수로
재요청하라고 안내. API가 애초에 적게 준 경우(정상적 데이터 부족)는 기존 동작 유지 — 이 구분을
놓친 첫 시도는 기존 테스트가 실패로 잡아줬다. 회귀 테스트 2건 추가, 전체 736개 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 22:44:23 +09:00
kimandClaude Code b4f9a3df9f feat: ACTION-PROMISE 가드 — 실행 예고만 하고 도구 0개로 끝난 턴 1회 넛지
- 2026-09-25 작업실 실측(로봇 프로젝트 채팅): 모델이 '먼저 GitHub에서 관련
  리포지토리를 찾겠습니다'만 내놓고 도구 0개로 턴 종료, 사용자가 '?'로 되물어도
  사과+예고만 반복. deferral 넛지는 '잠시만 기다려'류만 잡고 실행형 요청은 아예
  제외라 이 케이스가 두 망 다 통과했다.
- isActionPromiseNoTool: 답 끝부분(마지막 220자)이 행동 동사+미래 서약
  (~하겠습니다/~해 보겠습니다/~시작합니다) 또는 let me / I'll … 로 끝나는지.
  과거형·의문형·의례적 맺음말은 제외, 800자 이상 완성 답변도 제외.
- handleChat: 이번 턴 도구 호출 0개일 때만 1회 넛지(라운드 소진 방지), 로그줄
  '[v2] ACTION-PROMISE POST-CHECK'가 발동 증거. 테스트 3건(실측 문구 포함) 추가.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 20:30:16 +09:00
kimandClaude Code 3761390d8e feat: stl_cad에 transform 액션 — 외부 STL도 채팅으로 크기변경/회전/거울/이동
- 소스 없는 다운로드 STL은 구멍/컷/불리언만 채팅 편집이 되고 크기·회전은 웹
  편집기에 의존했음(2026-09-25 사용자 요청으로 추가).
- transformStl: OpenSCAD import+변환 체인으로 재출력 — 법선·와인딩(거울 시 면
  방향)을 OpenSCAD가 올바르게 재생성. 회전·거울·배율은 bbox 중심 기준이라 모델이
  흩어지지 않음. 제자리 덮어쓰기(output_path 생략)도 임시파일→rename 원자적 처리.
  align_bottom 옵션으로 바닥 z=0 정렬.
- 함정 기록: OpenSCAD mirror([x,y,z])는 축이 아니라 법선 벡터 — [-1,1,1]은 x축
  거울이 아니라 기울어진 평면 반전(테스트로 발견·수정, 축 거울은 [1,0,0] 형태).
- 테스트 6건 추가.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 19:30:48 +09:00
kimandClaude Code 1125f3afb9 feat: 부품 바닥 z=0 규칙 — 컴파일 자동 정렬 + 편집기 정렬 버튼
- 계기: 로봇팔 부품들이 center=true 모델링이라 바닥이 z<0으로 내려가 편집기에서
  파묻혀 보이고 좌표 감각이 어긋남(2026-09-25 사용자 지적, CAD 전부 실측 확인).
- .scad 작성 규칙에 7번 추가: 인쇄용 부품 바닥은 z=0(프롬프트 규칙문+SHORT).
- scad_to_stl/compile: 컴파일 후 bbox min z<-0.05면 바이너리 z 평행이동으로
  자동 정렬하고 안내 문구 출력(translateStlZInPlace, 원자적 재기록).
- STL 편집기에 '바닥 z=0' 버튼 — 외부 STL도 처리(현재 회전·배율 상태의
  월드 bbox 최저점을 0으로). 테스트 3건 추가.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 18:58:14 +09:00
kimandClaude Code 7e22a3074f feat: 작업실 파일·폴더 삭제를 영구 rm → 휴지통 이동으로 변경
- 계기: 작업실 화면에서 CAD/ 폴더를 지우면 되돌릴 수 없었다(LeRobot SO-101
  프로젝트 CAD/ 소실, 2026-09-25 실측).
- 삭제 시 workshop/.trash/<projectId>/<타임스탬프-접미사>/ 로 rename만
  (같은 볼륨이라 즉시, UI 변경 없음). 14일 지난 항목은 다음 삭제 시 자동 정리.
- 프로젝트 삭제 시 첨부 폴더도 휴지통으로(기존엔 통째 rm).
- 휴지통 이동 실패 시에만 기존 rm으로 폴백. 스탬프에 고유 접미사로 연속
  삭제 덮어쓰기 방지. 테스트 5건 추가.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 18:25:54 +09:00
kimandClaude Code 875b08e70a feat: STL 3면도 + 외곽 치수선 자동 생성 (C안)
- stl-three-view.ts: OpenSCAD ortho 렌더 3뷰(정면/평면/우측면)를 실루엣 크롭→공통
  축척 리샘플로 제3각법 합성, 외곽 치수선(연장선·화살표·수치)+제목란까지 PIL 합성.
  동시실행 직렬화, 임시디렉토리 정리, ASCII STL 명확 에러.
- stl_cad 도구에 three_view 액션, /api/cad/render?view=three 라우트 추가.
- 테스트 5건(fmtMm 단위 + 실렌더 통합/픽셀검증/정리/에러).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-25 17:50:54 +09:00
kimandClaude Sonnet 5 cedf881f2d feat: 조립 설명서 호환 .scad 작성 규칙을 코드에 하드코딩 (규칙 문구·lint·SCAD/ 경로 보정)
배경: 테스트 프로젝트 로봇팔은 부품이 로컬 좌표 모듈이고 조립 자세를 robot_arm() 하나가 translate/rotate로 만드는
구조라, 조립 초안이 전체+하위 모듈을 중복으로 잡고 하위 모듈이 원점에 그려져 단계 강조가 안 보였다.

규칙(src/tools/scad-conventions.ts):
 1) 부품마다 조립 자세 모듈 asm_<이름>()(모듈 안에서 translate/rotate)  2) 파일 끝 조립 블록 PART == "all" — 모듈 호출만, 조립 순서대로
 3) 참고용 형상은 ref_ 접두어(조립 설명서에서 제외)  4) 모듈 위 주석 한 줄 = 한국어 부품 이름  5) 새 .scad는 SCAD/, STL은 CAD/
적용 지점:
 - 작업실 채팅 컨텍스트에 규칙 전문, write_scad 도구 설명(모델이 보는 정의 2곳)에 요약
 - write_scad가 저장 직후 lint(조립 블록 없음 / 블록 안에 변환·형상·반복문 / 부품 1개뿐)를 응답에 덧붙여 모델이 스스로 고치게 함
 - 프로젝트 안의 새 .scad를 SCAD/ 로 자동 보정(CAD/·루트·print/ → SCAD/, 사용자 폴더는 존중)하고 이동 사실을 안내
파서: 조립 블록 이름 별칭(all/assembly/full/whole/complete), ref_ 고스트 접두어, 한 줄에 압축된 module 인식, getAssemblyRegion 분리
초안 실패/경고 문구에 "채팅에 조립 블록을 만들어줘" 안내
테스트 24건 추가(lint 표, 경로 보정, 별칭, write_scad 실제 OpenSCAD 통합)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 11:18:29 +09:00
kimandClaude Sonnet 5 2bdc5563dc feat: .scad 소스를 프로젝트 SCAD/ 폴더로 분리 (STL은 CAD/ 유지) + 렌더 미리보기는 사진/
사용자 지시(".scad 폴더를 따로"): 변형 .scad가 많은 프로젝트(로봇팔 등)에서 STL과 섞이면 고르기 어려움
- 규칙: workshop/<id>/SCAD/(.scad 소스만) · CAD/(STL 등 모델, CAD/참고, CAD/다운로드) · 출력/ · 사진/ · 영상/ · 조립설명서/
  옛 "STL을 .scad와 같은 폴더" 규칙은 폐지. 사용자가 만든 폴더의 .scad는 건드리지 않음
- 이동 스크립트가 .scad→SCAD/ 처리(조립 명세의 scad 경로도 갱신). 09-25 papa 적용: .scad 12개 + 렌더 PNG 5개 이동, 무손실 검증
- stl_cad 렌더 미리보기 PNG를 프로젝트 STL이면 사진/ 에 저장(CAD/에 이미지가 다시 쌓이던 문제)
- 채팅 프롬프트/도구 안내: 새 .scad는 SCAD/, STL 출력은 CAD/. 파일 탭 권장 폴더에 SCAD 추가
- 테스트: 규칙 표 갱신, apply/undo 왕복, previewPngPath

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 10:38:41 +09:00
kimandClaude Sonnet 5 86d541d928 fix: .scad 선택 시 "새 경로 + 옛 부품" 어긋난 상태로 렌더가 실패하던 문제
- 원인: 다른 .scad를 고르면 경로만 먼저 바꾸고 부품은 나중에(또는 취소하면 영영) 안 바뀌어, 그 사이 저장 타이머/자동 렌더가
  새 파일에 없는 모듈(옛 파일의 motor())로 렌더 → "부품 motor: .scad에 module motor이(가) 없습니다"가 화면에 남음
- 확인창을 먼저 띄우고, 자동 초안이 성공한 뒤에만 경로+부품을 한 번에 교체. 취소/초안 실패 시 아무것도 바꾸지 않고 선택 상자만 원복
  (경로만 바꾸려면 "경로 직접 입력")
- 편집하면 이전 렌더 오류 문구를 지움. 서버 오류 문구에 파일 이름과 해결 방법(자동 초안으로 부품 재생성/모듈 이름 수정) 안내
- 회귀 하네스: 취소/초안 실패/도중 어긋난 상태 저장 감시 (옛 코드에서 4개 항목 실패 확인)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 10:31:53 +09:00
kimandClaude Sonnet 5 b611c045b6 fix: 조립 탭 ".scad 파일 선택이 안 된다" — datalist를 선택 상자로 교체
- 원인: <input list=datalist>의 후보(서버 조회)가 화면을 그린 뒤 도착하는데 목록을 다시 안 그려 항상 비어 있었고(옵션 0개),
  입력칸에 값이 있으면 브라우저가 그 값과 일치하는 항목만 보여줘 다른 파일을 고를 수 없었다
- <select>로 교체(최근 수정 순, 현재 파일 선택 표시, 후보 도착 시 제자리 갱신), 직접 입력칸은 아래에 유지(목록 밖 경로는 "(목록 밖)")
- 다른 .scad를 고르면: 부품이 비어 있으면 자동 초안+렌더, 이미 있으면 교체할지 확인(취소하면 경로만 변경)
- 회귀 하네스 tests/manual/scad-select-check.ts (11개 항목)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 10:21:00 +09:00
kimandClaude Sonnet 5 d235ee94f3 refactor: 작업실 핵심 스크립트를 기능별 파일로 분리 (core/parts/tasks/files/chat/init)
- workshop-app.js(83KB) → workshop-core.js(30KB: 인증·이스케이프·상태/저장/충돌/폴링·탭·지연 로더·모달/이력),
  workshop-parts.js(17KB), workshop-tasks.js(6KB: 작업+메모·개요), workshop-files.js(13KB: 파일·슬라이서),
  workshop-chat.js(18KB: 채팅·사진 첨부·리사이즈·모델 pill), workshop-init.js(1KB)
- 원래 한 파일의 섹션 순서 그대로 로드(core→parts→tasks→files→chat→init)해 로딩 순서 문제를 만들지 않음.
  escHtml/safeUrl은 여러 모듈이 쓰므로 core로 이동
- 무손실 검증: 분리 전후 코드 줄 비교(사라진 코드 줄 0, 함수 선언 118개 동일), 실브라우저 45개 항목 + 실서버 읽기 전용 확인
- vm 저장 로직 테스트는 workshop-core.js를 읽도록 변경

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 09:59:12 +09:00
kimandClaude Sonnet 5 0b7057f975 refactor: 작업실 스크립트에서 조립·장비 탭을 별도 모듈로 분리하고 지연 로딩
- workshop-app.js 114KB → 83KB(핵심), workshop-assembly.js(22KB)·workshop-equip.js(12KB)는 해당 탭을 처음 열 때만 로드
- 코드 내용 변경 없음(구간을 잘라 옮김). 모듈 밖에서 부르던 이름은 refreshAssemblyPane(shim)과
  startEquipTimers/stopEquipTimers 뿐이라 가드된 로더(loadWorkshopModule/withLazyModule)로 감쌈
- 로드 중엔 패널을 흐리게(.rb-loading)+클릭 차단, 로드 중 다른 탭으로 옮기면 실행 안 함,
  실패 시 안내 표시 + 다시 열면 재시도(같은 모듈은 한 번만 받음)
- 브라우저 하네스에 지연 로딩 5개 항목 추가(시작 시 미로드/탭별 로드/재요청 없음/실패 안내/재시도), 전체 43개 항목 통과

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 09:53:49 +09:00
kimandClaude Sonnet 5 6119be592d refactor: 작업실 HTML에서 CSS·JS를 별도 파일로 분리 — 내용 변경 없음
- web-ui/html/workshop-app.html 147KB → 9KB (구조만)
- web-ui/css/workshop-app.css(24KB), web-ui/js/app/workshop-app.js(114KB, classic script라 인라인 onclick 전역 핸들러 그대로)
- 분리 내용은 바이트 단위로 원문과 동일함을 검증, 실브라우저 하네스 38개 항목 통과
- vm 저장 로직 테스트는 분리된 JS를 읽도록 변경, 조립 하네스 기본 경로를 폴더 정돈 후 위치로 갱신

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 09:45:28 +09:00
kimandClaude Sonnet 5 ba193eec3c feat: 작업실 파일 폴더 규칙 통일 — STL을 .scad와 같은 폴더에, 프로젝트별 제각각 구조 정돈
규칙(workshop-layout.ts 한 곳에 정의, 도구·스크립트가 공유):
  workshop/<id>/CAD/(.scad+STL 같은 폴더) · CAD/참고/(비인쇄 참고 모델) · CAD/다운로드/ · 출력/ · 사진/ · 영상/ · 조립설명서/
  print3d/K2웹슬라이서/ · 모델검색/ · 일회성/ · profiles/
인쇄용 STL은 .scad 옆에 두되 참고용은 CAD/참고/로 분리 유지(09-22 "참고용을 인쇄 대상으로 착각" 방지)

- 이동 스크립트 scripts/migrate-workshop-layout.ts: 기본 dry-run, --apply(이동만·덮어쓰기 없음·충돌은 건너뛰고 보고),
  --undo <manifest>. .scad 경로 참조(조립 명세)와 문서 문구(CAD/print→CAD 등) 갱신, 빈 폴더만 정리. 멱등
- 도구가 같은 규칙으로 저장: print3d_model(모델→CAD/다운로드, G-code→출력, 프로젝트 없으면 print3d/일회성),
  model_download(CAD/다운로드 또는 print3d/모델검색), K2 웹 슬라이서(print3d/K2웹슬라이서). 옛 print3d/<프로젝트이름>/ 제거
- 채팅 프롬프트: STL은 .scad와 같은 폴더(옛 09-22 CAD/print 하위폴더 규칙 폐지), 프로젝트 폴더 규칙 안내
- 파일 탭: 권장 폴더를 비어 있어도 업로드 대상으로 표시하고 규칙 순서로 정렬
- 테스트 34건(규칙 표, 멱등, 충돌 미덮어쓰기, apply→undo 왕복 무손실, saveDirs, print3d 계획)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 09:37:55 +09:00
kimandClaude Sonnet 5 c4807c9400 fix: 작업실 전체 점검 — 저장 경쟁 상태(가짜 충돌)·팔레트 PNG 썸네일·빈 조립 명세·중복 응답 루프
- 저장 직렬화: 자동 흐름(초안→렌더의 flushNow)과 debounce 타이머가 saveProject를 동시에 불러 같은 기준 버전으로
  PUT → 두 번째가 자기 자신의 첫 저장과 어긋나 가짜 409("렌더 실패: 저장 충돌을 먼저 해결하세요"). 큐로 직렬화하고,
  409여도 서버 내용이 내 내용과 같으면 충돌로 보지 않는다. 회귀 테스트(이전 코드에서 재현 확인)
- 비전 썸네일: 팔레트(P) 모드 PNG가 "cannot write mode P as JPEG"로 실패해 큰 이미지를 모델이 못 보던 문제 → RGB 변환
- 빈 조립 명세(경로/부품/단계 없음)는 저장하지 않음(조립 탭을 열기만 해도 프로젝트에 남던 것)
- .scad만 연결돼 있고 부품/단계가 없는 프로젝트도 조립 탭에서 초안 자동 시도(중단된 자동 연결 복구)
- 중복 add 응답에 "이미 충족됨, 반복 호출 금지" 안내 — 모델이 같은 add_task를 반복하던 LOOP WARN 완화

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 09:20:34 +09:00
kimandClaude Sonnet 5 c625826325 feat: 조립 설명서 .scad 자동 연동 — 자동 연결·자동 초안·변경 시 자동 재렌더
- 자동 연결: 조립 탭을 열면 명세가 비어 있을 때 프로젝트에서 가장 최근 수정한 .scad를 자동 연결
- 자동 초안: .scad의 조립 블록(if (PART == "all") { … } 또는 최상위)이 호출하는 모듈을 부품으로 삼고,
  변수 인자(camera(+CAM_OFFSET))는 원본을 include한 probe로 값(80)으로 풀고, 부품별 bbox로 분해 offset
  (수평 중심 바깥 0.5배·수직 윗쪽 높이 기준 0.9배+층 간 여유)와 조립 순서(.scad에 나열된 호출 순서, 같은
  모듈은 첫 등장 위치에 한 단계)를 정한다. %/* 수식자·변환 붙은 호출·이름에 ghost/fov/axis/plane…·isGhost=true
  인자는 고스트로 제외하고 안내. 자동 흐름은 조립 블록이 있는 파일만 채워 데모/애니메이션 파일로 엉뚱한
  명세가 만들어지지 않게 한다(수동 ✨ 버튼은 허용)
- 자동 재렌더: write_scad가 .scad를 바꾸면 그 파일을 쓰는(렌더한 적 있는) 프로젝트 그림을 백그라운드로
  갱신(프로젝트별 합치기), 조립 탭을 열었을 때 stale이면 자동 재렌더. autoRender 체크박스로 끌 수 있음
- 채팅 도구 draft_assembly(apply=true면 저장), 라우트 POST /api/workshop/assembly-draft
- 테스트: 파싱/이름/실행(OpenSCAD)/자동 재렌더/도구 + 실브라우저 자동 연동 시나리오

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 09:06:48 +09:00
kimandClaude Sonnet 5 8677a090d5 feat: 작업실 "조립 설명서" — .scad에서 단계별 조립도·분해도·부품표를 자동 생성
- 명세(project.assembly): 부품(call=조립 모듈 호출, 분해 offset, BOM 연결, 수량) + 조립 단계(부품/체결부품/공구/설명).
  프로젝트 데이터라 이력·충돌감지·복제가 그대로 적용된다
- 렌더러: 원본 .scad를 수정하지 않고 `use <>` wrapper로 부품을 골라 OpenSCAD ortho 렌더. 안쪽 color가 우선이라
  색을 못 덮는 제약을 "전체 렌더 vs 부품 뺀 렌더"의 픽셀 차이로 해결 → 이번 단계 부품만 원색+주황 외곽선,
  나머지는 옅게, 부품 번호 풍선은 그 부품이 실제 보이는 영역에 배치(가림 처리 정확). 모든 그림 같은 카메라
  (bbox 기반 fit, 투영식 실측 검증), 공통 여백 크롭으로 단계 간 축척 유지
- call 검증: 모듈 1개 호출만(따옴표/세미콜론/중괄호/주석/import·surface 등 금지) + .scad 안에 있는 module만
- 대시보드 "조립" 탭: .scad 스캔→부품 추가, 분해 offset, 단계 편집, 그림 렌더, 작업 탭으로 가져오기
- 설명서 페이지 assembly-manual.html: 완성/분해도, 부품표(BOM 구매상태), 체결부품·공구 합계, STEP 카드, A4 인쇄/PDF
- 채팅 도구: scan_scad / get_assembly / set_assembly / render_assembly ("조립 설명서 만들어줘" → AI가 초안)
- 테스트: 순수함수·실제 OpenSCAD/PIL 렌더(가장자리 잘림 없음 검증)·라우트 HTTP 통합·실브라우저 하네스

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 23:03:43 +09:00
kimandClaude Sonnet 5 37630be619 feat: 작업실 대규모 업그레이드 — 이력/충돌감지/도구 확장/표·게이지·진행률/출력 감시 외
안전망
- 변경 이력: 저장 때마다 바뀌기 전 상태를 프로젝트당 30개까지 보존(45초 합치기), 대시보드 "🕘 이력"에서
  되돌리기, 삭제된 프로젝트 복구(첨부파일 제외). 되돌리기 직전 상태도 이력에 남는다
- 낙관적 락: 프로젝트 updatedAt으로 다른 탭/채팅 도구가 먼저 고친 프로젝트 저장을 409로 보류하고
  충돌 배너(내 변경으로 덮어쓰기/서버 버전 불러오기) 제공. 15초 폴링으로 외부 변경 반영(편집 중엔 미룸)
- 명령 주입 수정: resolveToolImageContent가 파일명을 셸에 그대로 넘기던 execSync → execFileSync

채팅 도구(workshop_project)
- set_notes(메모 전체 교체), update_task/delete_task, rename/delete_phase, add_parts(일괄), rename/
  archive/unarchive/duplicate/delete_project(confirm 필수), set_budget, check_price(다나와),
  list_history/restore_history, view_image(첨부 사진을 모델이 직접 봄)
- 프롬프트에 개요·진행률·남은 작업·예산 현황 주입

대시보드
- 부품 표 보기·상태 필터·정렬, CSV/MD 내보내기, 목표 예산 게이지, 작업 진행률·기한(지남/임박),
  프로젝트 복제(템플릿)/보관, 부품 시세 조회, STL 썸네일(OpenSCAD, 캐시), 채팅 사진 첨부(📷)

장비 연동
- print3d_model start 시 작업실 프로젝트/작업을 등록 → K2 출력 완료/실패를 감시해 작업 자동 완료 +
  메모 기록 + 텔레그램 알림(workshop-print-watch.ts, job 없으면 폴링 안 함)

테스트: 서버/도구/감시/프런트 vm 시뮬레이션 + 라우트 통합(HTTP) + 실브라우저 수동 검증 하네스(tests/manual)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 11:53:21 +09:00
kimandClaude Sonnet 5 8e9226e117 fix: 작업실 앱 전수 검토 결함 일괄 수정 — 채팅 저장 덮어쓰기·업로드 크래시·XSS 외
- 저장: saveDebounce 미해제로 채팅 도구 저장 내용이 새로고침 때 화면 옛 상태로 덮이던 버그.
  서버 동기 스냅샷 기반 dirty(바뀐 프로젝트만 PUT), 실패 시 재시도, 무변경 PUT 제거
- 업로드: 파일명 %(100%.stl)로 decodeURIComponent가 'end' 콜백에서 던져 프로세스가 죽던 버그 +
  수신 중 크기 제한, 콜백 예외 응답
- XSS: escAttr가 따옴표만 처리해 부품/프로젝트/파일명이 innerHTML에서 실행되던 것,
  항목 id 화이트리스트, 링크 href safeUrl
- 헛보고 가드: JS \b가 한글에 안 먹어 "저장했어/했다"가 통과하던 정규식, 명사 없는 후속 확인 질문
- 도구: 정확일치 우선 검색, 중복 생성/추가 방지, 세션(ws_<id>) 프로젝트 기본 대상
- 프로젝트 삭제 시 첨부파일·채팅 세션 정리, 손상 JSON 백업(.corrupt), 옛 전체문서 PUT 제거,
  PUT 본문 검증, 백그라운드 탭 장비 폴링 중지, 전송 중 프로젝트 전환 잠금
- 회귀 테스트 16건 추가(프런트 저장 로직은 vm 시뮬레이션)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 11:26:19 +09:00
kimandClaude Code eaa34714cf feat: WORKSHOP SAVE 가드 — 도구 없이 "저장했습니다" 헛보고를 강제 재유도로 차단
09-24 실측 2건: get(읽기)만 하고 add_note 없이 "메모탭에 저장했습니다" 보고.
프롬프트 규칙(b327eef)으로도 안 잡혀 MESSAGING POST-CHECK과 같은 패턴으로
가드 승격. ws_ 세션에서 저장 요청(메모/개요/부품+변경동사) + 답변이 저장
클레임(종결접미사 필수 — "저장했너?"/"저장했던" 제외) + 이번 턴에 변경
action(get/list 제외) 없음 → 1회 강제 재유도.

"이미 이전 턴에 저장돼 있습니다"는 재유도 답변에서 명시적으로 허용해
오탐(이전 턴 저장 상태 보고)이 무한루프 안 걸리게 함. 회귀테스트 7건.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 09:15:03 +09:00
kimandClaude Code eb516e2b74 feat: 작업실 저장 개편 — 전체문서 PUT을 프로젝트 단위 PUT으로 + LOW 2건
저장 개편(리뷰 #4 근본치료):
- PUT /api/workshop/project/:id — 서버가 디스크의 현재 문서에 해당 프로젝트만
  upsert. 대시보드 저장과 채팅 도구 저장이 겹쳐도 서로 다른 프로젝트 편집이
  통째로 사라지지 않음. DELETE /api/workshop/project/:id,
  PUT /api/workshop/active(포인터 단독) 신설. 옛 전체문서 PUT은 스테일 탭
  호환으로 유지
- 클라이언트: dirtyProjects 추적 → saveProject는 바뀐 프로젝트만 순차 PUT,
  활성 포인터는 putActive() 즉시 저장, 프로젝트 삭제는 DELETE 직접 호출,
  beforeunload flush도 프로젝트 단위 keepalive PUT으로 전환
- upsertProject/removeProject를 순수함수로 추출, 단위테스트 4건

LOW 2건:
- loadFiles에 요청 일련번호 — 빠른 프로젝트 전환 시 옛 응답이 나중에 도착해
  이전 프로젝트 파일을 새 판에 그리는 레이스 제거
- 슬라이서 인계 ?load=를 JSON 배열 인코딩으로 전환(파일명에 쉼표 있으면
  옛 쉼표 join이 깨지던 문제). 슬라이서 쪽은 JSON 우선, 옛 형식 폴백 유지

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 07:38:53 +09:00
kimandClaude Code 2da7b9fd98 feat: 작업실 개요를 파일 폴더 대신 개요 탭으로 + /api/files 텍스트 charset 수정
- workshop-app: 메모 탭과 동일 구조의 "개요" 탭(textarea) 추가 — project.description
  직접 편집, 채팅 set_description으로 쓴 내용은 응답 후 softRefresh로 갱신
- readme.md 자동생성(syncProjectReadmes) 철회 — 파일탭이 산만해져서 사용자 지정으로
  탭 방식으로 전환, 생성된 개요 폴더들도 삭제. description/set_description은 유지
- server.ts IMAGE_TYPES: 텍스트 계열(.md/.txt/.csv 등) charset=utf-8 명시 — charset
  없이 text/plain으로 뱉으면 브라우저가 latin1로 해석해 한글이 전부 깨짐(실측)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 16:48:12 +09:00
kimandClaude Code 91ed19bda0 feat: 작업실 프로젝트별 개요 폴더 + readme.md 자동생성
- workshop/<프로젝트id>/개요/readme.md — 대시보드 데이터(개요/예산/부품/작업/메모)에서
  자동 생성, saveWorkshop과 대시보드 data GET에서 내용 변화 때만 동기화(기존 프로젝트 백필 포함)
- WorkshopProject.description 신설 + workshop_project set_description 액션 — 채팅 모델이
  서술형 개요를 쓰면 readme 맨 위 개요 섹션으로 들어감
- formatProject/budgetSummary/fmtWon을 storage로 이동(도구 get과 readme가 같은 문서 소스)
- 파일탭에는 개요 폴더 그룹으로 자동 표시(walkCaseFiles)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 16:32:45 +09:00
kimandClaude Code 5a571f592e feat: 3D 모델 저장소 연동 — model_search/model_download (Printables·MakerWorld·Thingiverse)
- model_search: 3사이트 통합 검색(Thingiverse는 토큰 필요, Printables/MakerWorld는 익명 검색 실측 확인)
- model_download: 슬라이싱 가능한 STL/3MF 중 최대 파일 1개 워크스페이스 저장(Printables getDownloadLink 익명 동작, Thingiverse /v2/files download, MakerWorld는 Bambu 토큰 필요)
- print3d_model에 stl_path 추가 — model_download로 받아둔 파일 바로 슬라이싱
- 토큰은 vault secret(print3d.*_token/cookie), Thingiverse App Token 등록 완료
- 게이트: ws_ 세션 항상 + 메인 채팅은 3D 키워드

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 11:43:03 +09:00
kimandClaude Code f8d1c9f9df feat: thinking 분리를 thinking-separation.ts로 추출 + GLM 영어 셀프토크 구조 기반 분리
- server.ts의 separateThinkingFromContent를 chat/thinking-separation.ts로 추출 (단위 테스트 가능하게)
- stripEnglishScratchPrefix 신설: 영어 런 40자+ + 문장부호 뒤 무공백 한글 봉합점(seam)을
  구조로 탐지 — 키워드가 아니라 구조라 한글 조각이 섞인 영어 계획 단락도 처리
- 스크래치 증거 게이트(메타 라벨 또는 한글 없는 40자+ 단락 2개)로 오탐 방지,
  정상 혼합문("Open-Meteo 모델은")은 보존
- think 태그 내용을 버리지 않고 thinking으로 보존하던 손실 수정
- 실측 표본 2건(09-22 추석 날씨 세션)을 단위 테스트로 고정, 6/6 통과

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 00:41:49 +09:00
kimandClaude Sonnet 5 58a079fcb1 fix: nvr_snapshot 후속턴에 도구 사라지던 문제 + 캡처 이미지 채팅에 안 뜨던 문제
실사용(papa, 2026-09-07)에서 드러난 2건:
- "cctv확인해봐"로 도구가 돌았는데 다음 "아니다 cam2"/"사진 보여줘" 후속턴엔
  hasCctvKeyword가 현재 메시지만 보느라 도구가 빠졌고, 모델이 "저는 그런 툴이
  없습니다"를 3턴 반복 → topicText(직전 2턴 포함)로 검사, cam N·스냅샷·캡처 등 추가
- 캡처된 프레임을 모델이 프로즈로만 설명하고 이미지 마크다운을 안 뽑아 사용자가
  사진을 못 봄 → satellite_snapshot처럼 execute-tool에서 SSE로 이미지 직접 push
- 도구 결과 지시문을 "이전 대화 짐작 말고 이 프레임에 보이는 것만 구체적으로"로 강화
  (gemma4가 첫 응답에서 맥락에 기대 뭉뚱그린 사례)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:14:24 +09:00
kimandClaude Sonnet 5 1a40512270 feat: nvr_snapshot — 모델이 집 CCTV 화면을 직접 보고 답한다
"마당에 누구 있어?", "택배 왔나 봐줘" 같은 요청에 NVR/독립카메라의 현재 프레임을
ffmpeg로 한 장 캡처해 비전 모델에 임베드한다.

- src/tools/nvr.ts: RTSP → ffmpeg 단일 프레임 → workspace/nvr-snapshots/ → ![](/api/files/…).
  카메라는 이름("현관")·채널번호("2")·자유입력 모두 해석, 못 찾으면 목록 반환.
  main 실패 시 sub 스트림으로 1회 재시도. 파일명은 ASCII(썸네일 execSync 한글경로 회피).
- routes/nvr.ts: getNvrConfig/getExtraCameras/getChannelLabels export + buildRtspUrl 추출
  (라이브 스트림 라우트와 공유, 중복 제거).
- tool-scope.ts: hasCctvKeyword 게이트 — CCTV/카메라/마당/현관/택배/"누구 있" 등에만.
- handle-chat.ts: 비전 판정을 _modelSupportsVision 헬퍼로 통일 + **gemma 추가**
  (gemma4:31b-cloud는 멀티모달인데 4곳의 정규식에서 다 빠져 있어 weather_map_screenshot
  등에서도 "이미지 못 봄" 취급받고 있었음 — 2026-09-07 실측으로 비전 정상 확인).
  _imagingTools 3곳에 nvr_snapshot 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:05:13 +09:00
kimandClaude Opus 5 ddd276c15a docs: 수동 메모리 게이팅의 근거 정정 — USER.md는 prompts/ 아래 실재한다
a1033f6에서 "USER.md도 SOUL.md도 4명 전 사용자 누구에게도 없어서 memory_browse/read는
'not found' 에러만 낼 수 있다"고 적었는데 **틀렸다**. 감사 때 `ls workspace/*.md`로만 확인했고,
파일은 `workspace/prompts/` 아래에 있다 — resolvePromptPath가 PROMPT_FILES에 대해 그쪽을
**먼저** 본다(server.ts:1199). papa의 USER.md는 3.2KB에 카테고리 5개, 최종 수정 2026-08-06으로
homeclaw_memory 컬렉션의 최신 문서 날짜와 정확히 일치한다. 도구 셋 다 정상 동작한다.

게이팅 결정 자체는 유지한다 — 근거가 사용량이기 때문이다(1,011턴에서 browse 0 / read 2 /
write 1회인데 매 턴 357토큰). 다만 코드·테스트 주석에 박힌 거짓 근거를 걷어낸다. "죽은
레거시라 빼도 된다"와 "멀쩡하지만 거의 안 쓰여 게이팅한다"는 다음 사람이 내릴 판단이 달라진다.

memory_write는 파일이 없으면 만들지 않고 에러를 낸다는 점도 함께 정정했다(execute-tool.ts:1220).
앞선 커밋 메시지의 "없으면 새로 만든다"는 서술이 잘못이었다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 21:12:21 +09:00
kimandClaude Opus 5 a1033f6013 perf: USER.md 수동 메모리 도구 3종을 기억 의도가 있는 턴에만 싣는다
memory_browse / memory_write / memory_read는 지금까지 **매 턴 무조건** 실려 357토큰을 냈다.
실사용 1,011턴에서 호출은 browse 0회 / read 2회 / write 1회.

파고 보니 단순한 저사용이 아니었다: **USER.md도 SOUL.md도 4명 전 사용자 누구에게도 없다.**
resolvePromptPath에 템플릿 폴백이 없으므로(교차 오염 위험은 없다), memory_browse와 memory_read는
파일이 없어 "USER.md not found. Create it first." 에러만 돌려줄 수 있는 상태로 매 턴 실려 있었다.
실제 장기기억은 일별 로그 + Chroma 벡터 추출(자동)이 담당한다 — USER.md 경로는 그 시스템이
대체한 레거시다([[project_legacy_skill_system]]과 같은 유형: 죽은 하위시스템이 스키마 비용만 계속 냄).

제거가 아니라 게이팅인 이유: 사용자가 "기억해둬"라고 명시하는 턴에는 있어야 하고, memory_write는
파일이 없으면 새로 만들므로 그 경로 자체는 지금도 유효하다. topicText로 판정해 "그것도 기억해둬"
같은 후속 발화도 잡는다. memory_stats는 벡터 스토어를 보므로 게이트 대상이 아니다(13회 호출됨).

실측(고유 782턴): 도구 토큰 중앙값 1,221 → 864 (-357/턴), 누적 278,460tok 절감.
수동 메모리 도구가 실리는 턴은 0.3%로 떨어진다.

기존 계약 테스트 2개가 "memory_read는 핵심 도구라 항상 남는다"를 고정하고 있어 갱신했다 —
핵심 도구 목록에서 memory_read를 memory_stats로 바꾸고, 기억 의도가 있으면 다시 나타나는지를
새로 고정했다. 551개 통과.

⚠ 별건: 이번에 사용자가 "기억해둬"라고 5턴에 걸쳐 명시한 세션(0c367bd7 10925~10940)에서
memory_write가 한 번도 불리지 않았음을 확인했다. 모델은 매번 "기억해 두겠습니다"라고만 했다.
정보 자체는 일별 로그에 남아 벡터 추출이 받지만, 명시적 지시가 도구 호출로 이어지지 않는 것은
별도 문제다(MESSAGING 가드와 같은 유형의 도달 실패). 이 커밋은 그 문제를 고치지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 21:05:16 +09:00
kimandClaude Opus 5 46a5c3f59f perf: 검색 결과 읽기 지침을 시스템 프롬프트에서 빼 결과가 생긴 뒤에만 붙인다
효율 감사 실측(사용자 턴 1,011건 / LLM 호출 3,613건 / prompt 누적 29.1M tok):
  · 고정비 2,494tok/턴 = 전체 prompt의 31%
  · 출력 60토큰 미만 호출이 47% — 도구 라운드·가드 재프롬프트라 고정비가 라운드 수만큼 곱해진다
  · **59%의 턴이 grounding 도구를 한 번도 부르지 않는다**

searchReadingRule(481tok)에는 성격이 다른 두 가지가 한 덩어리로 섞여 있었다:
  ① 검색어를 어떻게 쓸 것인가(세대 추측 금지, 154tok) — 검색 **전에** 필요
  ② 돌아온 결과를 어떻게 읽을 것인가(스테일 타임스탬프·표 행 정렬, 333tok) — 결과가 **생긴 뒤**
     에만 쓸모가 있다
②를 groundingResultReadingRule()로 분리해, handle-chat이 첫 grounding 결과가 들어온 시점에
대화 뒤에 한 번 덧붙인다. 읽을 결과가 없는 59%의 턴은 이제 이 값을 아예 내지 않고, 부르는
턴도 검색어를 쓰는 1라운드에는 내지 않는다. 인사 턴 시스템 프롬프트 1,273 → 904tok(-29%).

**시스템 메시지를 갈아끼우지 않고 뒤에 덧붙인 이유는 KV 캐시다.** 시스템 프롬프트는 라운드 루프
앞에서 한 번 만들어지므로, 라운드마다 다시 만들면 프리픽스가 달라져 캐시가 통째로 무효화된다 —
라운드마다 7K 토큰을 다시 계산하게 되어 아낀 것보다 잃는 게 크다. 뒤에 붙이면 프리픽스는 그대로다.

부수 효과로 지침이 대상(검색 결과) 바로 옆에 놓인다. 제약이 겹치면 먼 것부터 버리는 모델에게는
이쪽이 유리하다([[feedback_local_model_needs_code_backstop]]).

2026-09-02 감사가 만든 hasGroundingTools 게이트는 사실상 죽어 있었다는 점도 이번에 드러났다 —
tool-scope에 web_search를 거르는 분기가 없어 그 조건은 항상 참이다. 이번 분리는 그 게이트에
의존하지 않는다.

기존 회귀 테스트가 이동을 정확히 잡아냈다(2026-08-10 캐시된 기상표 오독 사고의 예시 문자열).
테스트를 새 위치로 옮겨 내용이 사라지지 않았음을 계속 고정한다. 546개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 20:59:35 +09:00
kimandClaude Opus 5 b2075c62d8 fix: buildDirectPriceAnswer가 무관한 달러 값을 "Answer:"로 합성해 붙이던 문제
이 함수가 만드는 줄은 검색 결과 stdout 맨 앞에 붙는다. 시스템 프롬프트의 ANTI-HALLUCINATION은
"도구가 돌려준 것을 정확히 보고하라, 절대 무시하지 말라"고 지시하므로, 여기서 틀리면 모델에게
틀린 값을 신뢰하라고 시키는 셈이다. 게다가 합성된 숫자는 도구 출력 텍스트의 일부가 되므로,
모델이 그대로 옮기면 NUMERIC-GROUNDING이 "소스에 있다"며 통과시킨다 — 우리가 만든 숫자가
'근거 있음' 도장을 받아 나간다.

실제 사고(프로덕션 로그):
  USER: 컨테이너선 급유비가 얼마나 될까?
  TOOL: web_search("average fuel cost for 20,000 TEU container ship per voyage")
  TOOL OK: Answer: The current price is approximately $38.00 USD.
$38은 스니펫에 있던 "신조선 7일 항해 시 TEU 1개당 추가 연료비"였고, 실제 답은 항해당 수백만
달러다. 그 턴은 모델이 결과 [1]을 직접 읽어 스스로 바로잡았지만 그건 운이다.

* isPriceQuery에 단어 경계가 없어 통화·가격 낱말이 다른 단어 **안에서** 걸렸다. "eur"가
  "Europe"·"Neuralink"에, "cost"가 "costume"에, "value"가 "valuable"에 들어맞는다. 실사용
  고유 검색어 195건 중 19건이 통과했고 그중 8건이 가격과 무관했다("Europe drought status
  August 2026", "Neuralink Blindsight resolution pixels" 등). 경계를 넣어 19 → 11건.

* generic 자산 경로를 제거했다. 금·은·비트코인은 타당성 범위(온스당 300~10,000 등)와 자산명
  대조가 성립해 판정이 의미가 있다. generic은 둘 다 없다 — 허용 범위가 $0.5~$5,000,000이라
  사실상 모든 달러 표기를 받고, 문구도 무엇의 가격인지 말하지 못한 채 "The current price"라고만
  쓴다. 이 로그 표본에서 generic 경로가 실제로 답을 낸 유일한 사례가 위 컨테이너선 건이다.

* 자산명 대조를 가점(+2)에서 **필수 조건**으로 올렸다. 가점일 때는 자산을 한 번도 언급하지
  않은 스니펫의 숫자가 score 0으로 통과했다 — "금 시세" 질문에 다른 상품 가격이 답으로 나갈
  수 있다는 뜻이다. 티커(XAU/XAG/BTC)도 함께 본다.

순효과(실사용 고유 검색어 195건): 발동 19 → 11건, 그중 실제로 "Answer:" 줄이 생길 수 있는
것 0건. 이 표본의 가격 질문은 전부 GPU·옵션·펌프 같은 generic 대상이라, 원래도 이 기능이
맞는 답을 낸 적이 없다.

테스트 544개 통과(+8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:06:31 +09:00
kimandClaude Opus 5 10e54d38eb fix: 게이트 전수 감사 — 한국어 검색 랭킹이 관련성을 0으로 계산하던 버그 외 5건
실사용 로그의 사용자 메시지 1,011건(고유 782건)을 모든 게이트에 통과시켜 오탐/미탐을
실측했다([[feedback_calibrate_on_real_data]]).

가장 큰 건: rankResults의 토큰화가 `q.replace(/[^a-z0-9\s]/g, ' ')`였다. 이 문자 클래스는
한글을 전부 지운다. **모든 한국어 쿼리**에서 관련성 토큰이 빈 배열이 되고 rel 점수가 항상
0이 되어, 결과가 오로지 domainTrustScore로만 정렬됐다 — 질문에 답하는 페이지인지와 무관하게.
시어엔진 라이브 실측("영리의료법인 비영리 병원 비율"): 수정 전에는 "Chee Lai"라는 사람의
LinkedIn 프로필 4건이 1~4위였고 유일한 관련 기사가 아래로 밀려 있었다. 수정 후 그 기사가
1위가 된다. rankResults는 7개 provider 전부가 쓰므로 영향 범위가 검색 전체다.
유니코드 인식 토큰화로 교체(한글·한자 2자 / 영문은 기존 4자 유지). 시점 표현도 영어만
있어서 한국어 "지금/현재/최신"은 배수를 받은 적이 없었다.

나머지:

* isCancelIntent 오탐 — "하지마 유적지는 바다와 떨어져 있는데?"에서 지명 하지마(波島)가
  `하지\s*마`에 걸렸다. 참이면 진행 중이던 작업이 일시정지되고 지리 질문이 작업제어 응답으로
  하이재킹된다(server.ts 두 호출부 모두). 취소는 문장 끝 명령이므로 종결 위치로 앵커를 걸었다.
  영어 쪽은 표본에 사례가 없어 건드리지 않았다.

* isExecutionLikeRequest 오탐 — 맨 "패치"가 목록에 있어 "인슐린 패치는 혈당 측정 기능이
  없나?", "패치는 많이 비싼가?"가 실행형으로 분류됐다. 이 함수는 검증 강제에서 면제시키는
  쪽이라, 실존 의료기기의 기능·가격을 검색 없이 기억으로 답해도 아무도 막지 않았다.
  "구속"을 체포 맥락으로 좁힌 것과 같은 처리 — 소프트웨어 패치 활용형일 때만 걸린다.

* isFactualInfoRequest 미탐 — "얼마" 계열 수량·가격 질문. 표본에서 이 표현으로 물었는데
  어떤 검증 게이트에도 안 걸린 5건이 있었고 전부 정당한 사실 질문이었다(오탐 0):
  "tesla v100 32gb 메모리 대역폭은 얼마지?", "미국 올 해 예산은 전 부 얼마지?"(실제로
  기억에서 $7조 5,400억이 나갔다), "이자 비용은 연간 얼마지?" 등. 맨 "얼마나"는 비-사실
  용법이 흔해 넣지 않았다.

* tool-scope climate 오탐 — "메모리 가격 추세"가 기후 재분석 아카이브 4종(~820토큰)을
  통째로 실었다. "추세"/"장기"는 기후·기상 명사와 같이 나올 때만 걸리게 좁혔다.

* tool-scope kakao 오탐 — `![KakaoMap_….png](…)` 첨부 파일명이 kakao_send_message를 실었다.
  executeTool은 스키마 소속을 확인하지 않으므로([[project_tool_schema_leak]]) 이름만 언급돼도
  호출될 수 있는 종류의 도구다. 첨부 파일명을 판정에서 빼고 카카오맵도 제외했다.

순효과 측정(고유 782건): 검색 강제 리마인더 주입률 23.8% → 25.4%(+13건). 새로 잡힌 13건은
전부 정당한 사실 질문이고, 잃은 건 0건이다.

감사에서 이상 없음으로 확인한 것: EMPTY-GROUNDING·MESSAGING(전제를 직접 검증),
decideAutoRecover·correctNewsCategoryForBreadth(도구 출력 대조 안 함), link-validator(URL 직접
프로브), satellite 게이트("도달 > 비용" 원칙상 유지), 0건 발동 게이트 6종(전용 앱 탭 경로).

테스트 536개 통과(+15).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:01:26 +09:00
kimandClaude Opus 5 db6a8634c3 fix: 가드가 맞는 답을 지우던 경로 — 무관한 검색 결과에 대고 판정하던 문제
2026-09-04 메인챗 세션(0c367bd7) 감사. 사용자가 "옵션 프리미엄이 실물의 몇 %냐"를
다섯 턴에 걸쳐 물었고, 검색 6회에 산출물 0건으로 끝났다. 로그를 보니 원인이 모델이
아니라 우리 가드였다:

  TOOL: web_search("typical option premium as percentage of underlying asset price")
  TOOL OK: [1] TYPICAL Definition & Meaning - Merriam-Webster
  NUMERIC-GROUNDING POST-CHECK (1/1): 3 figure(s) not found in tool output [10%, 1%, 2%]

모델은 매 턴 답을 냈다(10%/1%/2%, 다음 턴엔 3~10%/0.1~1%). 검색엔진이 "typical"의
사전 뜻풀이를 물어왔고, 그 뭉치에 숫자가 없다는 이유로 답이 날조 판정을 받아 지워졌다.
이 가드의 전제는 "검색이 그 주제를 다뤘는데 숫자가 없다 → 지어냈다"인데, 앞부분을
한 번도 확인한 적이 없었다. 무관한 말뭉치에서의 부재는 날조의 증거가 아니다.

* 관련성 관문(sourceCoversQuery): 검색어의 내용어 중 결과에 등장하는 비율이 0.34
  미만이면 판정을 포기한다. 실측 — 사전 뭉치 0.14 / 정상 결과 0.86. 수치·날짜·부재주장
  세 갈래가 같은 전제 위에 있어 한꺼번에 게이트했다. 검색어를 못 넘기는 호출부는 기존
  동작 유지.

* SEARCH-ABANDONMENT도 같은 결함이 있었다(감사 중 발견). countSubstantiveResults는
  스니펫 길이만 봐서 사전 페이지 4건을 전부 "알맹이 있음"으로 세고, 모델에게 "그걸로
  답하라"고 민다 — 답을 지우는 대신 틀린 답을 강요하는, 같은 전제의 반대 방향 피해다.
  같은 관문을 공유시켜 기준이 갈라지지 않게 했다.

* 그 가드는 애초에 죽어 있었다. NOTHING_FOUND가 "정보가 없다"류만 담고 있어, gemma4가
  실제로 쓰는 "결과에 …이 포함되어 있지 않다"(주어가 결과)를 하나도 못 잡았다. 이
  세션의 포기 답변 6건 중 0건 탐지. 사과로 시작하면 판정 대상인 첫 문장이 "죄송합니다."가
  되는 두 번째 구멍도 함께 수정(한글 뒤 \b 미매칭 함정 주의).

* dead-end-fallback (신규): 두 번째로 빈손이면 검색을 끊고 "아는 것을 검증 못 했다고
  라벨 붙여 말하라"로 돌린다. 문제의 스레드에서 3턴 앞서 끊긴다. 재프롬프트가 숫자를
  요구하지 않아 NUMERIC-GROUNDING과 충돌하지 않는다.

* 제품·모델 선택 질문이 isFactualInfoRequest를 통째로 빠져나갔다("모델이 있을까",
  "고른다면", "신형/최신"). 그래서 소설-모델 3턴이 도구 0개로 굴러가 2026년 9월에
  "Gemma 2 27B"를 로컬 추천으로 냈다. 주제어가 앞 턴에만 있는 짧은 후속 질문용
  isFactualThreadFollowUp 추가.

* confirmsUserGuessWithoutGrounding: 직전 턴에 확인 못 했다고 해놓고 사용자 추측에
  "네, 맞습니다"로 동조하는 경로 차단(맥미니 M6 건). 4중 조건으로 좁혀 정당한 맞장구는
  건드리지 않는다.

* cms_hospital_compare 게이트가 "병원"에만 걸려 있어 사용자가 내내 쓴 "의료법인"으로는
  8턴 동안 스코프에 들지 않았다. 그 사이 공공 비중 답이 45~50%→39~40%→약 50%로 흔들렸다.

테스트 521개 통과(+65). 실사용 로그의 실제 발화·답변을 표본으로 고정했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 17:51:12 +09:00
kimandClaude Sonnet 5 e658f6a634 fix: 사진 요청이 AUTO-RECOVER 재프롬프트 루프를 타던 문제
앞 커밋(2537b04)에 이어진 후속. papa "눈향나무 사진" 재현 로그:
  SKIP: duplicate search_images / AUTO-RECOVER (2/2) "live-data query answered
  without a grounding tool call" / LOOP WARN: search_images x3

원인: 모델이 사진 대신 눈향나무 설명 프로즈를 냈는데 그 안의 "5cm·30cm·500년"이
looksLikeUnverifiedSpecClaim에 걸려 unverifiedSpecClaim→liveDataRequest가 참이 됨.
search_images는 GROUNDING_TOOL_PATTERN에 없어(citable text 아님) hasGroundingToolCall
false → 재프롬프트 → gemma가 search_images 반복 호출.

수정: decideAutoRecover 앞에 사진 요청 + search_images 호출 시 재시도 안 함 단락.
이미지 누락은 appendDroppedSearchImages가 복구하므로 재프롬프트가 고칠 게 없음.
대조군 테스트로 unverifiedSpecClaim 가드 자체는 유지 확인. 474 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-03 16:06:03 +09:00
kimandClaude Sonnet 5 2537b04c7a fix: search_images 결과를 모델이 프로즈로 바꿔치기해 사진이 안 나오던 문제
papa 세션 실측(2026-09-03): "눈향나무 사진 찾아줘" → search_images가 ![alt](url)
3개를 정상 반환했는데 gemma4가 "눈향나무 사진들입니다" + 설명만 내고 링크를 통째로
누락. 다음 턴 "사진이 어딧지?"에도 "죄송합니다..." 하며 또 0개.

- appendDroppedSearchImages(reply-content.ts): 최종 답변에 이미지 마크다운이
  하나도 없고 이번 턴 search_images가 성공했으면, 도구 결과의 ![](url)를 뒤에 붙임.
  하나라도 있으면 모델이 시도한 것으로 보고 안 건드림
- personality-context photo 프롬프트: "![alt](url) 줄을 VERBATIM으로 복사, 프로즈로
  대체 금지"로 강화 (코드가 backstop, 프롬프트는 보조 — [[feedback_local_model_needs_code_backstop]])

참고: 눈향나무 같은 특정 수종은 Pexels/Unsplash 커버리지가 나빠 "빨간 배경 분재"가
매칭되기도 함 — 그건 스톡 API 한계로 별개 이슈.

tests +5 (473 통과).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-03 16:00:42 +09:00
kimandClaude Sonnet 5 b0c098e76d feat: drug_dur_check — 식약처 DUR 의약품 안전정보 조회 도구
닥터앱 "복약·검사" 탭이 핵심인데 약 정보를 조회할 수단이 없어, 상호작용·금기를
모델이 기억으로 답하거나 web_search로 블로그를 물어오고 있었다.

- data.go.kr DURPrdlstInfoService03(품목 기반) 연동. 제품명 부분일치로
  병용금기/임부금기/특정연령대금기/용량주의/투여기간주의/효능군중복/분할주의 조회
- 병용금기는 상대 성분 + 사유별로 묶어 요약(제품 단위론 수백 행이라 무의미)
- getOldSderTabooInfoList03(노인주의)는 API가 폐기 응답 → 목록에서 제외
- 성분 기반 서비스(DURIrdntInfoService03)는 이 키에 미승인 상태라 미사용
- 게이트: 닥터앱(dr_) 세션 무조건 + 일반 챗은 약물 상호작용 문맥 키워드
- 키는 vault(mfds.dur_api_key), config.json엔 참조만. SECRET_FIELD_MAP 등재

tests +2, 468개 통과. 실 API로 타이레놀·심바스타틴 병용금기 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-02 17:11:28 +09:00
kimandClaude Sonnet 5 7b0223b071 feat: 닥터앱 탭에 학술 검색 도구(PubMed/OpenAlex) 개방
닥터앱은 기본 도구만 받아서 "이 약 장기복용 괜찮나?", "이 백신 효과 몇 년?",
"이 수치 정상 범위인가?" 류 질문에 web_search로 블로그를 물어오고 있었다.
academicToolNames(pubmed_search/pubmed_fetch/pubmed_fulltext/openalex_search/
semantic_search)를 dr_ 세션 안에서만 키워드 없이 개방 — lawyer 탭이 법령 도구를,
investor 탭이 news_search를 무조건 받는 것과 같은 앱-탭 패턴.

- dr_main / dr_case_<id> / dr_<rand> 전부 dr_ 접두사라 한 번에 잡힘
- 메인챗은 불변(키워드 게이트 그대로) — 테스트로 누출 없음 확인
- 앱 상단 면책("진단·처방 대체 아님")은 유지되므로 페르소나 불변
- 프롬프트 추가 비용 0 (hasGroundingTools는 web_search로 이미 항상 참)

tests +1, 467개 전부 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-02 16:10:35 +09:00
kimandClaude Sonnet 5 e12a327c9b fix: 탐정·투자 앱 탭에 세션 게이트 누락 — 탭 안에서 전문도구가 0개였음
앱 탭은 세션 접두사로 무조건 게이팅하는 설계인데(탭 안 후속질문은 키워드가 없어도
그 앱의 맥락이므로), 접두사를 쓰면서 게이트가 없는 앱이 둘 있었다.

- detective-app.html → 'dt_' : 페이지에 판례 12회·법률 11회 언급인데 법령 도구가
  하나도 안 실렸다. lawyer 탭과 같은 도구군이 필요 → 법령 3종 + news_search 개방.
- investor-app.html → 'iv_' : 환율 6회·주가·증시·엑셀·뉴스. "그럼 작년 대비?" 같은
  후속질문에 읽을 게 아무것도 없었다 → news_search + excel 개방.

doctor('dr_')·language('lg_')는 의도적으로 제외 — 전자는 개인 의료기록 앱
(기록 검색/복약·검사 항목), 후자는 튜터라 기본 세트로 충분하고 전문 스키마는 순비용.

메인챗 회귀 없음(키워드 없으면 그대로 닫힘)을 테스트로 고정. tests 466 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 15:22:21 +09:00
kimandClaude Sonnet 5 1ef06637a5 fix: 레거시 스킬 토글이 도구를 막던 문제 — 키워드 게이트로 일원화
사용자 확인: 스킬 on/off UI는 예전 것이고 지금은 앱별 게이팅을 쓴다. 그런데
skills_state.json이 tool-scope의 meteorologist/lawyer/presenter 게이트를 통해
여전히 도구를 **막는** 유일한 소비자로 남아 있었고, 아무도 갱신하지 않는 값이라
조용한 기능 상실을 만들고 있었다.

실측된 라이브 영향: papa는 meteorologist:false → "오늘 서울 미세먼지 어때?"에
weather_kma / weather_openmeteo 만 실렸다. weather_kma엔 대기질 데이터가 아예
없으므로(모델 프로필에도 명시돼 있음) 답은 실패 아니면 창작 둘 중 하나였다.
바로 옆에 있던 weather_airkorea(에어코리아, 키 승인·정상)가 죽은 플래그에 막혀 있었음.

- meteorologist/lawyer/presenter 스킬 조건 제거 → 키워드 게이트만 사용
- ToolScopeInput.isSkillEnabledForUser는 유지(앱세션 게이트용, 스킬이 실제
  프롬프트 파일을 갖게 되면 다시 쓸 자리)
- 검증: papa·jasmine 모두 미세먼지/발표자료/판례 도구 정상 노출

tests 463 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 15:08:22 +09:00
kimandClaude Sonnet 5 99d3d387bc fix: 법령 "100분의 N" 표기를 %와 동일하게 인정 (오탐 재측정 후속)
과거 NUMERIC-GROUNDING 발동 20건을 현재 코드로 리플레이한 결과:
- 날씨 4건(87%/55%/65%/70~90%, weather_openmeteo 표) → 전부 통과로 전환 ✅
- 진짜 날조 1건(weather_kma에 없는 "예보 불확실성 40%") → 여전히 발동 ✅
- 나머지 15건 중 7건이 하나의 연금 감액률 스레드였는데, 한국 법령·행정규칙은
  퍼센트를 "%"가 아니라 "100분의 N"으로 쓴다 → 답변 "50%"가 출처 "100분의 50"과
  대조되지 못해 매번 오탐. unitVariants의 % 목록에 "100분의" 추가.

출처에 없는 비율은 여전히 발동함을 테스트로 고정. tests +2, 463개 전부 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 14:42:22 +09:00
kimandClaude Sonnet 5 58f77152a8 perf: 프롬프트/도구 스키마 고정비 12% 절감 (효율 감사 ①②③)
실측: LLM 호출 3,409회 중 47%가 출력 60토큰 미만(도구 라운드/가드 재프롬프트)이고
매 라운드가 프리픽스를 통째로 재전송 → 고정비 절감이 라운드 수만큼 곱해짐.

① system-prompt: TEMPORAL CONSISTENCY(2,463자)와 OUTPUT FORMAT(1,460자)이 무조건
   블록이었는데 문장 대부분이 "검색 결과를 어떻게 읽고 표시할지"라 그라운딩 도구가
   없는 턴엔 사문. searchReadingRule / newsFormatRule / weatherFormatRule로 분리해
   도구 목록으로 게이팅. 날짜=ground truth와 "3개 이상이면 표"는 무조건 유지.
② tool-scope: ERA5/CDS/CMIP6/NASA POWER는 기후 재분석·시나리오 아카이브인데 평범한
   날씨 키워드에 실려 "오늘 날씨"마다 820토큰 낭비 → climateToolNames + 기후 키워드
   게이트 신설. 일상 예보(kma/openmeteo/search/airkorea)는 그대로.
③ news_search query 파라미터 설명 1,900자→640자. 사고 경위 서술을 규칙만 남기고 압축
   (경위는 git 이력과 project_search_query_routing에). 스키마 2,978→2,089자.
④ 죽은 가드 7종은 제거하지 않음 — browser/desktop/messaging은 의도적 휴면이고
   EMPTY-GROUNDING은 유닛테스트로 정상 동작 확인(검색이 늘 뭔가 반환해서 드물 뿐).

절감 실측(스키마+프롬프트 합):
  인사/일반 3,213→2,847 (-11%) | 날씨 5,254→4,279 (-19%) | 뉴스 4,174→3,594 (-14%)
  전체 평균 -12%

tests +2 (게이팅 계약을 테스트로 고정), 461개 전부 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 14:36:33 +09:00
kimandClaude Sonnet 5 885bab088b fix: NUMERIC-GROUNDING 가드 전면 점검 — 철자단위·표형출처·숫자경계
프롬프트 가드 전체 감사 중 numeric-grounding에서 오탐 3종 발견·수정:

1. 철자 단위 불일치: 출처가 "25 percent"/"24 gigabytes"인데 답변이 "25%"/"24GB"면
   근접매칭이 실패 → 오탐. unitVariants()로 %↔percent↔퍼센트, GB↔gigabyte↔기가바이트,
   토큰↔tok, tok/s↔tps↔초당 등 동의 표기 허용.
2. 표형 출처(어제 커밋의 weather 범례) 보강 + 정직한 주석: 큰 표는 사실상
   fabrication 체크 면제됨(실패모드는 wrong-row인데 그건 원래 못 잡음).
3. 숫자 부분일치: "5"가 "157.66" 안에서 매칭돼 날조를 가려주던 문제 →
   숫자 매칭에 경계 적용 (앞: 숫자/점 금지, 뒤: 숫자 금지·점은 허용해 반올림 수용).

감사 결과 나머지 가드(AUTO-RECOVER, EMPTY/SEARCH-ABANDONMENT, MESSAGING,
INTENT-ONLY, 재시도예산)는 정상. tests +2, 전체 459 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 14:21:29 +09:00
kimandClaude Sonnet 5 61cfb180f1 fix: 표형 도구출력에서 NUMERIC-GROUNDING 오탐 → gemma "죄송합니다" 루프
weather_openmeteo 등은 단위를 헤더 범례에만 쓰고("precipitation_probability=%")
값은 열에 맨숫자로 넣는다. checkNumericGrounding의 근접매칭(40자)이 "55"와 "%"를
멀다고 판단 → 정확한 강수확률·습도 답변마다 NUMERIC-GROUNDING POST-CHECK 재프롬프트가
걸렸고, 재프롬프트 지시문이 "죄송합니다, 임의의 수치를 사용했습니다"류 답변을 유도했다.
사용자 신고: "메인챗에서 젬마가 찾은 결과 안 보여주고 죄송합니다만 함".

- numeric-grounding.ts: hasUnitLegend() 추가 — 출처가 "<name>=<unit>" 또는 "단위:" 범례를
  쓰는 표면, 근접매칭 실패 시 "숫자가 독립 토큰으로 출처에 존재"만 확인(열 값 매칭).
  표에 아예 없는 수치는 여전히 잡힘.
- handle-chat.ts: NUMERIC-GROUNDING 재프롬프트에 "죄송합니다로 시작하지 말고 결과에
  뭐가 있었는지 명시하라" 추가.
- tests +2. 전체 458 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 13:48:40 +09:00
kimandClaude Sonnet 5 88ba78c537 fix: 메인챗 두 가지 — 검색어 세대 추측 + "잠시만 기다려" 후 정지
2026-09-01 맥미니 M5/M6 세션 사후분석에서 나온 두 버그.

1. 세대 추측 검색어 오염: 사용자가 "맥미니 신형"(칩 미지정)이라 물었는데
   gemma4가 학습시점 지식으로 web_search("M4 Mac mini …")를 던졌고, SearXNG는
   시킨 대로 M4(2024) 기사를 정확히 반환 → 답 전체가 구세대 기준으로 틀어짐.
   - system-prompt TEMPORAL CONSISTENCY에 "신형/최신인데 세대 미지정이면
     쿼리에 기억 속 세대를 넣지 말고 현재 연도로 검색" 규칙
   - handle-chat: hasUnversionedNewestIntent + assumedGenerationTokenInQuery로
     감지해 1회 재프롬프트(오염된 검색 실행 전 차단)

2. "잠시만 기다려 주세요" 후 턴 종료: 사용자가 오류 지적하자 모델이
   "다시 확인해 보겠습니다. 잠시만 기다려 주세요."만 내고 도구 0개로 정지.
   isIntentOnlyReply는 답 속 "2026년 9월"을 실질내용으로 오인, lastRoundWas
   GuardReprompt도 false라 기존 넛지 미발동.
   - isDeferralPromiseReply 신규(좁은 "wait for me" 패턴), 가드 재프롬프트
     선행 없이도 1회 강제 continuation

tests/intent-only-reply.test.ts +12 케이스. 전체 456 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-01 17:29:19 +09:00
kimandClaude Sonnet 5 493a37eb9e feat: 위치/장소 질문에 구글맵 인라인 임베드 유도하는 MAPS 시스템프롬프트 블록
markdown.js는 이미 독립된 줄의 google.com/maps URL을 keyless 임베드
iframe으로 렌더하고 handle-chat.ts는 그 URL의 browser_open을 가로챈다.
빠져 있던 건 "언제 그 URL을 내놓을지" 모델에게 알려주는 지시뿐이었음.

CHEMISTRY NOTATION과 같은 메시지 게이팅 예외로 추가 — 출력 형식 제약이라
키로 삼을 도구가 없고, 키워드를 놓쳐도 대가가 지도 하나 안 뜨는 것뿐.
/maps/search·/maps/place 형식만 유도(마크다운 변환기가 /maps/dir 미지원).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
2026-08-31 11:31:21 +09:00
kimandClaude Opus 5 7ac143652a fix: 논문 도구가 발표자료 키워드에만 묶여 있어 일반 채팅에서 안 켜지던 문제
pubmed_search·pubmed_fetch·pubmed_fulltext·openalex_search·semantic_search
다섯 개가 hasPptxKeyword에만 걸려 있었다. "논문 찾아줘"로는 스키마가 아예
안 실려서 모델이 web_search로 때웠다 — 실사용 로그에서 확인했다.

학술 의도 자체를 게이트로 추가했다. 학술 검색은 web_search보다 확실히 낫다:
openalex_search는 키 없이 2억 건에서 제목·저자·연도·저널·DOI·인용수·오픈액세스
여부를 구조화해 준다.

정규식에서 한 번 걸렸다 — `\bpaper\b`가 복수형 "papers"를 못 잡는다(뒤의 \b가
s 앞에서 끊긴다). papers?·citations?·journals?로 고쳤다. 한글 뒤에는 \b를 쓰지
않는다(ASCII 경계라 절대 매칭 안 됨 — 같은 파일의 hasEmailKeyword 주석 참고).

기존 pptx 경로는 그대로 열려 있다(논문 수집 → create_presentation).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:20:07 +09:00
kimandClaude Opus 5 4ac9919449 feat: 날씨에 도로 안개 추가 (CCTV 기반 도로날씨) — 안개 있을 때만 표시
안개는 ASOS·AWS·Open-Meteo 어디에도 없는 값이라 도로 CCTV 영상 분석이 유일한
소스다. 전국 593개 도로 지점에서 안개/비/눈 코드가 온다.

**앞선 판단 정정**: 08-15~18에 이 API가 0행이라 서비스 종료를 의심했는데,
소급 반영이 늦었던 것이었다. 지금 조회하면 08/17이 83,478행으로 채워져 있고
08/14도 57,247 → 67,937행으로 늘었다. "지금 0행"으로 종료를 단정하면 안 된다.

안개가 있을 때만 한 줄 붙인다 — 매번 "안개 없음"을 쓰면 소음이 된다:
  도로 안개: 보통 (CCTV 양재 관측, 13km · 11:20 기준)

어느 지점에서 몇 km 떨어진 값인지 밝혀 대표성 판단은 사용자에게 남긴다.
30km 밖이면 표시하지 않는다(도로변에만 있어 성기게 깔려 있다).

성능: 처음엔 응답이 7초 → 11초로 느려졌다. 조회 창을 90분에서 30분으로 줄이고
(지점 커버리지 584/593로 사실상 동일, 591KB → 87KB) 전국 한 벌을 5분 캐시해
구미 기준 7.1s → 2.7s로 되돌렸다.

같은 지점이 몇 분 간격으로 반복 보고되므로 지점별 최신 행만 남긴다.
야간에는 영상 분석이 없어 데이터가 안 나온다 — 안개 줄이 없는 게 정상이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:54:10 +09:00
kimandClaude Opus 5 ae4439efba fix: 가드 재프롬프트에 예고만 하고 끝나는 답변을 한 번 밀어준다
"계산하다가 멈춘다"는 신고. 생성이 끊긴 게 아니었다 — 로그에는 답변이
끝까지 있었고, 그 답변의 내용이 예고문뿐이었다.

  USER: 우크라이나 전쟁 미사일 총 CO2?
  1. AUTO-RECOVER (1/2)        검증 안 된 수치 1070자 → 재프롬프트
  2. web_search 호출 → 결과 받음
  3. NUMERIC-GROUNDING (1/1)   "존재하지 않는다"가 검색결과에 없음 → 재프롬프트
  4. 모델: "죄송합니다… 다시 정밀하게 검색해 보겠습니다."  (도구 호출 없음)

3번 재프롬프트는 "다시 검색해서 그 결과로 답하라"고 지시했는데 모델은
예고만 했다. (1/1)이라 재시도 예산이 바닥이어서 그 예고문이 그대로 최종
답변으로 나갔다. 사용자 화면에는 계산하다 멈춘 것으로 보인다.

이걸 잡는 가드는 있었지만 상수 false로 꺼져 있었다:
  // DISABLED: Force continuation was causing excessive unnecessary tool calls
너무 광범위해서 멀쩡한 답변까지 재시도로 끌고 갔던 것이다.

그래서 **가드 재프롬프트 직후로만** 좁혀 되살렸다. 그 상황은 "실행하라고
지시했는데 예고만 함"이라 오탐 여지가 거의 없고, 평상시 답변은 건드리지
않는다. 한 턴에 한 번만 개입한다.

isIntentOnlyReply 판정도 좁다 — 400자 이내 + 미래형 예고 + 실질 내용 없음.
숫자·목록·표·코드·링크가 하나라도 있으면 실질 답변으로 본다:
  "계산해 보겠습니다. 총 배출량은 50,000톤입니다." → 통과
  "정리해 드리겠습니다.\n- 첫째\n- 둘째"          → 통과
  "다시 정밀하게 검색해 보겠습니다."               → 걸림

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:17:00 +09:00
kimandClaude Opus 5 c4911520f7 fix: 지오코딩 count 10→20 — "New York"이 네브래스카 York로 잡히던 문제
고르는 규칙(나라 명시 우선, 아니면 인구 최다)은 멀쩡했다. 문제는 후보
목록에 뉴욕시가 아예 없었던 것이다. count=10으로 받으면 응답이
York(네브래스카, 7,864명) / Florence(인디애나, 80명) / New York(영국) /
New York(자메이카)로 채워지고 인구 880만의 뉴욕시는 10위 밖으로 밀린다.
그래서 "남은 것 중 최다"가 네브래스카 York를 골랐다 — 에러 없이 조용히.

count=20이면 뉴욕시가 후보에 들어온다. 주요 도시 23곳을 10 vs 20으로
대조했고 바뀐 건 New York 하나뿐이다: York는 영국 요크 유지(뉴욕에
끌려가지 않음), Rome/Paris/London/Cairo/Sydney/Springfield 동일,
Los Angeles·Mexico City·Ho Chi Minh City 등 복합어 10곳도 동일.

후보 선택을 pickGeocodeResult()로 빼서 네트워크 없이 회귀 검증되게 했다.
당시 실제 응답을 픽스처로 넣고, 이 함수의 한계도 테스트로 명시했다 —
목록 밖의 정답은 구제할 수 없으므로 방어선은 호출부의 count이고, 둘을
한 몸으로 봐야 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 17:34:21 +09:00
kimandClaude Opus 5 d0ddce1361 feat: weather_kma에 과거 날짜 조회(type="past") 추가
"어제 제주 비 얼마나 왔어"를 물었더니 도구에 과거 조회가 없어서 모델이
web_search로 새고, 2026년 달력·기상청 도움말 페이지만 긁어온 뒤 "정확한
수치를 찾아드리지 못해 죄송합니다"로 끝났다. 검색 실패가 아니라 도구
공백이었다. ASOS 일자료는 한 번의 호출로 그 값을 준다(제주 8/15 = 13.9mm).

date는 "어제"·"그저께"·"3일 전"·"2026-08-15"·"8월 15일"·"08-15"·"20260815"를
받는다. 기준은 KST — 서버가 UTC라 new Date()로 날짜를 뽑으면 한국 시각
09시 이전에 하루 어긋난다. endDate를 주면 일별 + 기간 합계를 낸다.

모델 실수 두 가지를 코드에서 흡수한다:
 - date만 주고 type을 빠뜨리는 호출이 흔해서, date가 있으면 past로 본다.
 - 오늘 날짜를 넣으면 그냥 실패시키지 않고 "일자료는 전날까지, 오늘은
   type=current" 라고 안내한다. 맨 실패로 두면 또 web_search로 샌다.

sumRn의 빈 값은 결측이 아니라 그날 비가 안 왔다는 뜻이다(시간자료 rn과
같은 규칙, 서울 8/10~8/13이 빈 값이고 그 기간 무강수인 것으로 확인).
처음엔 "관측값 없음"으로 내보냈는데 그대로 뒀으면 맑았던 날마다 모델이
"자료가 없습니다"라고 답했을 것이라 0으로 말하되 미량("0.0")과 구분한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 14:24:20 +09:00