이 저장소에는 릴리즈 이력이 전혀 없습니다. git 태그 0개(`git tag | wc -l` = 0, `git for-each-ref refs/tags` = 0), 릴리즈 커밋 0개(`git log --all --oneline` 33개 커밋 전부 first commit / docs / fix: ... (감사 A-xxx) / Merge pull request 형식이며 release·버전·semver 패턴 매치 없음), CHANGELOG.md·docs/RELEASE*.md 없음, GitHub Release 목록 비어 있음, .github/workflows 없음(러너 제공 정보와 일치), 릴리즈/패키징 스크립트 없음(추적되는 .sh·Makefile·pyproject.toml·setup.py·package.json·Chart.yaml·Dockerfile·compose 파일 0개). 배포는 버전이 아니라 브랜치 기준입니다 — .gitlab-ci.yml 이 main/develop push 에 반응해 배포 디렉터리에서 `git reset --hard origin/$CI_COMMIT_BRANCH` 후 k8s rollout restart/podman restart 를 하며 태그·버전을 전혀 참조하지 않습니다. 버전 문자열은 api.py:147 의 FastAPI(version="1.0.0") 하나뿐인데 이는 OpenAPI 문서용 하드코딩 값이고 `git log -S` 결과 first commit(2075c62) 이후 17개 PR 동안 한 번도 변경된 적이 없어 릴리즈 시 갱신하는 버전 파일로 볼 수 없습니다(docs/IMPROVEMENTS.md 의 version="2.0.0" 은 제안 코드 스니펫, docs/ROADMAP.md 의 R0~R5 "Release" 는 소프트웨어 버전이 아니라 로드맵 단계입니다). 따라서 절차 5에 따라 아무것도 만들지 않았습니다 — 버전 체계·태그 형식·릴리즈 노트 위치를 새로 정하는 것은 사람의 결정입니다. 커밋·태그·파일 변경 없음, worktree 는 clean 상태로 둡니다. 참고로 이번 머지분(acfc432, 감사 A-113 Milvus 적재 실패 보고 수정)은 main 에 이미 반영되어 있어 기존 브랜치 기반 GitLab CI 배포로 그대로 나갑니다.
HTTP 200+code 299/499, 빈 문자열, None, HTTP 500 혼재는 그대로. 클라이언트에 traceback 을 그대로 돌려주는 것도 A-106 과 연계해 정리 필요. 응답 형태 변경이라 클라이언트 영향이 크므로 라우트를 나눠 단계적으로 진행해야 한다. /run_confluence_pipeline_spacefile 이 전형적인 예다(bare except -> return traceback.format_exc()). A-113 으로 이 라우트의 성공 문구는 실패 파일을 담게 됐지만 예외 경로의 traceback 반환은 남아 있다.
2026-09-10
GitLab CI 에 deploy 앞단 test stage 추가
4/3/L
대기
러너가 shell 태그 기반이라 python 실행 환경이 불확실하고, 실패 시 배포가 막힐 위험이 있어 러너 환경 확인이 선행돼야 한다.
2026-09-10
A-112 후속: fetch_unprocessed_feedback 이 DB 조회 실패와 '피드백 없음' 을 구분하지 않음
3/1/S
대기
service/feedbackservice.fetch_unprocessed_feedback 은 except 에서 로그만 남기고 [] 를 돌려주므로 process_feedback_batch 가 '처리할 피드백 없음' 으로 정상 종료하고 A-112 heartbeat 도 성공으로 기록된다. DB 가 계속 죽어 있어도 /health 의 background_tasks 는 running 이다(별도의 portal_db/athena_db probe 가 있어 완전한 사각지대는 아니지만, 풀 고갈이나 테이블/권한 문제처럼 SELECT 1 은 통과하는 실패는 여전히 안 보인다). 최소 수정은 조회 실패를 None(또는 예외)로 구분해 process_feedback_batch 가 실패를 반환하고 루프가 heartbeat 를 건너뛰는 것. psycopg2 미설치라 검증은 AST + 주입 가능한 helper 로 나눠야 한다. A-113 과 같은 결(실패를 반환값으로 삼키지 않기)이다.
2026-09-10
pipeline/law/library/xml_parser_core.py, json_formatter.py 단위 테스트
3/1/M
대기
fixture XML/JSON 을 만들어야 함. 순수 변환 로직이라 외부 의존성 없이 테스트 가능.
2026-09-10
A-113 후속: Milvus 삭제 경로도 실패를 삼킨다
3/2/S
대기
신규(2026-09-10 발견). util/milvus_confluence.delete_to_milvus_space_file 은 except 에서 logger.error 만 남기고 None 을 돌려주며, /run_confluence_pipeline_delete_spacefile 은 그 None 을 그대로 응답한다(삭제 실패인지 '지울 게 없음' 인지 구분 불가). delete_to_milvus 는 아예 try 가 없어 실패가 페이지 파이프라인 전체를 중단시킨다 — 두 함수의 실패 계약이 반대 방향으로 어긋나 있다. A-113 에서 만든 MilvusInsertError 옆에 삭제용 예외를 두고 같은 패턴으로 맞추면 된다. 삭제 실패를 알리면 '삭제 안 된 채 재색인' 이라는 중복 원인도 드러나므로 스페이스 재색인 삭제 순서 항목과 이어진다.
2026-09-10
A-104 후속: collection name allowlist 와 field 별 타입 검증
3/2/M
대기
표현식 escape 는 끝났으나 요청이 지정하는 collection/table 이름은 여전히 검증 없이 Collection() 에 전달된다.
2026-09-10
스페이스 첨부 재색인 시 기존 attachment 삭제가 호출자 책임인 문제
3/3/M
대기
/run_confluence_pipeline_spacefile 은 삽입만 하고, 기존 attachment 삭제는 별도 endpoint /run_confluence_pipeline_delete_spacefile(delete_to_milvus_space_file)로 분리돼 있다. 같은 스페이스를 두 번 색인하면 A-111 수정 후에도 이전 회차의 entity 가 남아 중복이 쌓인다. 페이지 파이프라인(confluence_piepeline_detail)은 루프 안에서 delete -> insert 를 함께 하므로 대조군이 있다. 삭제를 색인 앞에 넣으면 삽입 실패 시 데이터가 사라지므로 순서·실패 처리 설계가 필요해 M. A-113 으로 삽입 실패가 이제 판별 가능해져 '삭제했는데 삽입이 실패' 를 감지할 수단은 생겼다.
2026-09-10
A-102/A-206 후속: 추천 질문·토큰 캐시를 프로세스 간 공유
3/3/M
대기
캐시 교체는 정리됐지만 여전히 worker 프로세스마다 별도 캐시라 worker 수만큼 LLM 호출이 중복되고 응답이 worker 별로 다르다. 외부 cache 또는 sticky routing 이 필요. A-112 의 heartbeat 도 프로세스별이라 /health 응답이 worker 마다 다를 수 있다는 점이 이 항목과 얽힌다.
2026-09-10
A-107 후속: 재시도·circuit breaker·공용 requests.Session 도입
3/3/M
대기
timeout 은 전부 지정됐으나 재시도·backoff·connection pooling 은 없음. idempotent 호출에만 제한 재시도를 붙이는 설계가 필요. util/startup_wait.py 의 backoff 계산을 재사용할 여지가 있다. A-112 로 '계속 실패하는 루프' 가 관측 가능해졌으므로 재시도의 효과를 확인할 수단이 생겼다.
2026-09-10
except 절 범위 축소 (bare except -> 구체 예외)
3/3/M
대기
저장소 전반의 bare except 가 KeyboardInterrupt/CancelledError/GeneratorExit 까지 삼킨다. /status_completions, 추천 질문 갱신 루프, util/pipeline_module.extract_logic 은 좁혔음. service/pipelineservice.confluence_pipeline_kcblaw 와 api.py 의 파이프라인 라우트(예: /run_confluence_pipeline_spacefile)에 남아 있다. CancelledError 를 삼키는 것은 A-003 의 shutdown 판정과도 얽힌다. 파일별로 나눠 진행. tests/unit/test_extract_text.py 에 extract_logic 한정 bare except 검사가 있어 확장할 자리가 있다.
2026-09-10
api.py 의 미사용 import 정리 (A-204 일부)
2/1/S
대기
pyflakes 가 api.py 에서만 20건 이상의 'imported but unused' 를 낸다(저장소 전체 297건). 정리하면 pyflakes 를 CI 게이트로 쓸 수 있다. util/search_module.py 의 utility·RRFRanker, util/pipeline_module.py 의 is_softcamp_drm_stream·exec_ocr_confluence, service/pipelineservice.py 의 pymilvus 스키마 심볼·중복 settings import, util/milvus_confluence.py 의 pymilvus 스키마 심볼도 같은 상태다. 다만 side-effect import 여부를 하나씩 확인해야 한다.
2026-09-10
milvus_collection·milvus_confluence·pipeline 의 default alias 를 ensure_connection 으로 통일
2/2/M
대기
A-007 후속 작업에서 search_module 만 주소별 alias 로 옮겼다. 나머지는 항상 MILVUS_HOST 한 곳만 보고 호출 직전에 스스로 connect(alias='default') 하므로 지금은 동작이 깨지지 않아 가치가 낮다. 다만 연결이 매 호출 재확인되고 alias 규칙이 두 갈래로 남는다. 옮기려면 각 파일의 Collection()/utility 호출 전부에 using= 을 붙여야 해서 변경 폭이 크다.
2026-09-10
Milvus 적재 실패가 조용히 성공으로 보고되는 문제 (감사 A-113)
3/2/S
완료
insert_to_milvus_cf 가 실패를 traceback 문자열로 반환하고 호출부가 무시해, 전부 실패해도 api.py 가 'N 건 적재 종료' 를 돌려줬다. MilvusInsertError 로 올리고 index_space_files 가 SpaceFileIndexResult(inserted, failed_files) 로 실패 파일을 세게 했으며 confluence_piepeline_detail 은 total_failed 를 반환한다. 커밋 acfc432.
2026-09-10
교훈 (깨졌던 변경)
2026-09-09 review-rejected — 백그라운드 task 종료 판정을 task 상태('cancelled')로 잡으면 CancelledError 를 삼키고 정상 반환하는 루프에서 cancelled()==False·exception()==None 이라 'stopped' 로 보여 정상 종료마다 오탐 ERROR 가 난다. shutdown 여부는 _shutting_down 플래그로 판정할 것. 또한 cancel() 이 곧 cancelled()==True 라고 가정하는 FakeTask 대역은 이 결함을 구조적으로 못 보므로 진짜 asyncio.Task 로 검증할 것. /health 같은 폴링 경로에서는 상태 전이 시점에만 로깅할 것. (링크)
원장 (에이전트가 남긴 기록)
2026-09-02
선택: mask_sensitive_data NameError 수정 + pytest 테스트 기반 도입 (가치 5 / 위험 1 / 작업량 M)
결과: 성공
요약: util/sensitive_data_masker.py 의 mask_sensitive_data() 가 dict/list 입력에서 존재하지 않는 _mask_sensitive_data() 를 호출해 NameError 로 실패하는 실제 버그를 재현·수정했다. 동시에 저장소에 전혀 없던 테스트 기반(pytest.ini, tests/conftest.py, requirements-dev.txt)을 만들고 외부 의존성 없는 순수 unit 테스트(sensitive_data_masker, parse_promptmanager, dpe_merge, text_renderer)와 저장소 전체 Python AST parse 테스트를 추가했다. python -m pytest 로 177 passed 확인, README/docs/TESTING.md 를 실제 상태에 맞게 갱신. 커밋 8122984.
보류 아이디어:
GitLab CI 에 deploy 앞단 test stage 추가 — 가치 4 / 위험 3 / L (러너가 shell 태그 기반이라 python 실행 환경 불확실, 실패 시 배포 차단 위험)
collection_script/* 의 import-time collection create/drop 부작용 제거 (감사 A-006) — 가치 4 / 위험 3 / M
feedback loop 의 undefined ENV 수정 (감사 A-003) — 가치 4 / 위험 2 / S
pipeline/law/library/xml_parser_core.py, json_formatter.py 단위 테스트 (fixture 필요) — 가치 3 / 위험 1 / M
util/extract_minor.py 의 except Exception: return e (예외 객체 반환) 정리 및 테스트 — 가치 3 / 위험 2 / S
2026-09-02 (2회차)
선택: 정의되지 않은 이름으로 인한 런타임 오류 일괄 수정 + 정적 회귀 테스트 (가치 5 / 위험 2 / 작업량 M)
결과: 성공
요약: pyflakes 로 저장소를 훑어 NameError/UnboundLocalError 를 일으키는 실제 결함 전부(감사 A-003 undefined ENV, A-004 Collection(collection), api.py 의 payload 선참조·typing import 누락, kai/cf workflow 의 guard_rail_completions_* import 누락, chunker 의 traceback import 및 text_chunking(file) 인자 누락, truncate_table 의 rows/collection_name 참조)를 수정했다. 함께 A-005(close_all_db_connections.closeall() → 함수 호출)와 A-109(finally 가 try 지역 conn 참조하는 8곳에 conn = None 초기화, fetch_unprocessed_feedback 의 중복 conn.close() 제거)도 정리했다. 회귀 방지로 운영 의존성 없이 AST 만 쓰는 tests/unit/test_undefined_names.py(pyflakes 기반, 미설치 시 skip)와 tests/unit/test_cleanup_paths.py(finally/종료 경로 검사)를 추가하고 requirements-dev 에 pyflakes 를 넣었다. python -m pytest 352 passed(기존 177 → 352), python -m pyflakes . 의 undefined name 0건 확인. docs/TESTING.md·CURRENT_STATE_AUDIT.md 를 실제 상태로 갱신. 커밋 89cf479.
보류 아이디어:
A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M (기동 동작 변경이라 운영 검증 필요)
collection_script/* 의 import-time collection create/drop 부작용 제거 (감사 A-006) — 가치 4 / 위험 3 / M
A-206 추천 질문 캐시 무한 append → 원자적 교체·중복 제거 — 가치 3 / 위험 2 / S
pipeline/law/library/xml_parser_core.py, json_formatter.py 단위 테스트 (fixture 필요) — 가치 3 / 위험 1 / M
util/extract_minor.py 의 except Exception: return e (예외 객체 반환) 정리 및 테스트 — 가치 3 / 위험 2 / S
2026-09-03
선택: 외부 HTTP 호출 timeout 누락 일괄 수정 (감사 A-107) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: requests 는 timeout 이 없으면 무한 대기하는데, FastAPI 라우트와 LangGraph 노드가 이 호출을 동기로 하므로 상대 서비스가 멈추면 worker 가 그대로 묶인다. 설정 모듈에 의존하지 않는 util/http_timeouts.py(DEFAULT 5/30s, LLM 5/300s, FILE 5/600s, 환경변수로 조정 가능)를 추가하고 timeout 이 없던 호출 53곳(api.py, service/, util/, pipeline/*, config/eval_class.py)에 용도별 값을 지정했다. AST 로 저장소 전체의 requests/Session 호출을 검사해 timeout 누락·timeout=None 재발을 막는 tests/unit/test_http_timeouts.py 를 추가했고, python -m pytest 450 passed(기존 352 → 450), python -m pyflakes . undefined name 0건 확인. docs 3종(CURRENT_STATE_AUDIT/TESTING/CONFIGURATION) 갱신. 커밋 be6f163.
보류 아이디어:
A-104 Milvus filter 표현식 직접 조립 → 안전한 expression builder + 입력 검증 — 가치 4 / 위험 3 / M
A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M
collection_script/* 의 import-time collection create/drop 부작용 제거 (감사 A-006) — 가치 4 / 위험 3 / M
A-206 추천 질문 캐시 무한 append → 원자적 교체·중복 제거 — 가치 3 / 위험 2 / S
A-107 후속: 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
2026-09-03 (2회차)
선택: Milvus filter 표현식 주입 방지용 expression builder (감사 A-104) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: session id, filename, page, doc id, space key 등 외부 입력이 f-string 으로 Milvus 표현식에 그대로 들어가, filename 이 a' or session_id != ' 이면 다른 세션 파일까지 삭제되는 구조였다. field 이름을 식별자로 제한하는 field() 와 \/'/"/개행을 escape 하는 quote(), eq/ne/lt/in_/and_ 를 가진 util/milvus_expr.py 를 추가하고 조립 지점 전부(util/search_module, milvus_collection, milvus_confluence, milvus_batch, pipeline/kai·kcb·law)를 교체했다. 요청 body 의 req.key_field 가 field 이름 자리에 들어가던 경로와, f"...'{a}'" + f"and ..." 로 이어 붙여 'value'and 처럼 공백이 빠지던 버그 2곳도 함께 고쳤다. 기존 코드가 모두 문자열 literal 비교였으므로 builder 도 값을 항상 문자열로 인용해 의미를 바꾸지 않는다. 단위 테스트(test_milvus_expr.py)와 저장소 전체 AST 회귀 테스트(test_milvus_expr_usage.py)를 추가해 python -m pytest 574 passed(기존 450), python -m pyflakes . undefined name 0건 확인. docs 4종(CURRENT_STATE_AUDIT/TESTING/MILVUS_SEARCH/SQL_SECURITY) 갱신. 커밋 53ce1f7.
보류 아이디어:
A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M
collection_script/* 의 import-time collection create/drop 부작용 제거 (감사 A-006) — 가치 4 / 위험 3 / M
A-105 오류 응답 계약 통일 (except block 의 미할당 response 포함) — 가치 4 / 위험 3 / L
A-206 추천 질문 캐시 무한 append → 원자적 교체·중복 제거 — 가치 3 / 위험 2 / S
A-104 후속: collection name allowlist 와 field별 타입 검증 — 가치 3 / 위험 2 / M
2026-09-04
선택: collection_script 의 import-time 컬렉션 생성·삭제 제거 (감사 A-006) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: collection_script/ 의 여러 스크립트가 모듈 최상위에서 Milvus 에 연결하고 컬렉션을 만들거나 지웠다. 특히 api.py 의 /create_filestorage_collection 이 라우트 안에서 create_collection_FILE_STORAGE 를 import 하므로 라우트 호출만으로 최상위 create("FILE_STORAGE") 가 실행됐고, delete_collection.py 는 import 시점에 KCBLAW_VIEW_BOX 를 drop 했다. 호출 시점에만 연결하는 connect() 와 파괴적 작업 확인용 confirm() 을 가진 collection_script/common.py 를 추가하고, 최상위 실행(FILE_STORAGE·CONFLUENCE·KAI·KCBLAWVIEW·delete_collection)을 argparse 기반 main() 과 __main__ 가드로 옮겼다. 하드코딩된 Milvus 주소 3곳(192.168.120.99×2, 192.168.116.99)을 connect() 로 교체하고, KAI 스크립트에 중복 정의된 컬렉션 목록을 configs.local_variable.COLLECTION_LIST_KAI 로 통일했으며, delete_collection 은 컬렉션 이름을 CLI 인자로 받고 --yes 없이는 확인을 요구한다. api.py 가 쓰는 create/create_law_detail/create_law_viewer signature 는 유지했다. AST 만 쓰는 tests/unit/test_collection_script_side_effects.py(import 시점 부작용 호출·최상위 실행 구문·하드코딩 주소·삭제 확인 절차)를 추가했고, 옛 코드를 되돌려 넣어 실제로 실패하는지 확인했다. python -m pytest 611 passed(기존 574), python -m pyflakes . undefined name 0건. docs 5종(CURRENT_STATE_AUDIT/CODEBASE_MAP/INSTALL/TROUBLESHOOTING/TESTING) 갱신. 커밋 8ae2a04.
보류 아이디어:
A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M
A-101 워크플로 모듈 전역 original_question 제거 → GraphState 로 전달 (동시 요청 간 질문 오염) — 가치 4 / 위험 3 / M
A-105 오류 응답 계약 통일 (except block 의 미할당 response 포함) — 가치 4 / 위험 3 / L
A-206 추천 질문 캐시 무한 append → 원자적 교체·중복 제거 — 가치 3 / 위험 2 / S
2026-09-05
선택: 워크플로 전역 original_question 제거 → GraphState 로 전달 (감사 A-101) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: KAI·KCB·법률·Confluence 4개 LangGraph 워크플로의 entry function 이 모듈 전역 original_question 을 요청마다 덮어쓰고 노드들이 그 전역을 읽고 있었다. 같은 프로세스에서 요청이 겹치면 A 사용자의 질문이 B 사용자의 guard_rail/query_extension/eval_weight_search/intro/check_search 에서 쓰여 가드레일 분기·검색 가중치·조항 조회가 엉뚱한 질문 기준으로 수행된다. retrieve 이후 노드가 question 을 확장 질의로 덮어쓰기 때문에 기존 question 필드만으로는 원본 질문을 보관할 수 없어, original_question 전용 필드를 각 GraphState 에 추가하고 helper util/workflow_state.py(with_original_question() 으로 inputs 에 주입, get_original_question(state) 로 읽되 없으면 question 으로 fallback)를 도입했다. global 선언 4곳과 전역 읽기 8곳을 전부 교체하고, kai_workflow 의 미사용 전역 original_question·last_rewritten 도 제거했다. 진입점이 *_langgraph 4개뿐임을 확인해 호출 계약은 그대로다. langgraph 가 설치돼 있지 않아 워크플로 모듈은 import 할 수 없으므로, helper 는 실제 동시 실행(asyncio.gather) 테스트로(tests/unit/test_workflow_state.py), 워크플로 모듈은 AST 검사로(tests/unit/test_workflow_globals.py: global 선언 금지, 모듈 전역 요청 상태 금지, 노드의 전역 읽기 금지, 진입점의 with_original_question() 호출, GraphState 필드 선언) 검증했다. 옛 law_workflow.py 를 되돌려 넣어 신규 테스트 4건이 실제로 실패하는지 확인했다. python -m pytest 654 passed(기존 611), python -m pyflakes . undefined name 0건. docs 3종(CURRENT_STATE_AUDIT/LANGGRAPH_WORKFLOW/TESTING) 갱신. 커밋 f4fb71b.
보류 아이디어:
A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M
A-105 오류 응답 계약 통일 (except block 의 미할당 response 포함) — 가치 4 / 위험 3 / L
A-206 추천 질문 캐시 무한 append → 원자적 교체·중복 제거·최대 크기 — 가치 3 / 위험 2 / S
A-108 Confluence 그래프의 retrieve → llm_response → END 명시적 edge 추가 — 가치 3 / 위험 3 / S (LangGraph 실행 확인 필요)
2026-09-06
선택: 오류 경로의 미할당 이름 참조 수정 (감사 A-105 부분 해결) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: api.py 의 5개 라우트(/newchatsession, /getrefreshtoken, /history_list, /delete_history_session, /history_session)가 except 에서 response.json() 을 호출했는데, 서비스 호출 자체가 실패하면 response 가 대입되지 않아 UnboundLocalError 가 원래 오류를 덮고 HTTP 500 이 됐다(history 계열은 성공 경로가 dict 를 반환하므로 response 가 있어도 .json() 이 없어 항상 실패). 저장소 표준인 {"code":499, "message": traceback.format_exc()} 로 교체하고, /status_completions 의 스트리밍 generator 는 last_state 를 try 밖에서 초기화하고 오류 시 종료 프레임을 실제로 yield 하며 GeneratorExit/CancelledError 를 삼키지 않도록 except Exception 으로 좁혔다. 같은 패턴인 kcb_pipeline_milvus.remove_legacy_file 의 collection_name 과 milvus_collection.sync_collection 의 response 초기화(및 list.append 결과를 재대입해 data 가 None 이 되던 버그, 반환 arity 불일치)도 함께 고쳤다. 회귀 방지로 기존 tests/unit/test_cleanup_paths.py 에 finally 검사와 같은 스코프 분석을 쓰는 except 블록 검사와 검사기 자체 self-test 2건을 추가했다(수정 전 저장소에서 7건 검출 확인). python -m pytest 751 passed(기존 654), python -m pyflakes . undefined name 0건. docs 3종(CURRENT_STATE_AUDIT/API_REFERENCE/TESTING) 갱신. 커밋 4587666.
보류 아이디어: A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: A-206 추천 질문 캐시 무한 append → 원자적 교체·중복 제거·최대 크기 — 가치 3 / 위험 2 / S
보류 아이디어: A-108 Confluence 그래프의 retrieve → llm_response → END 명시적 edge 추가 — 가치 3 / 위험 3 / S (LangGraph 실행 확인 필요)
2026-09-06
선택: 추천 질문 캐시 무한 append → 원자적 교체 (감사 A-206) (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: api.py 의 QuestionStore.auto_refresh_question 이 30분마다 수집 결과를 기존 리스트에 append 만 해서 프로세스가 오래 살수록 캐시가 무한히 커지고 같은 질문이 갱신 횟수만큼 중복 저장됐다. 더 심각하게는 question_recommend 가 실패 시 {"questions": "질문 생성 실패"} 라는 문자열을 돌려주는데 루프가 이를 그대로 순회해 한 글자짜리 질문이 캐시에 쌓였고, 피드백 질문 병합 단계에서는 str + list 로 TypeError 가 나 백그라운드 태스크가 조용히 죽어 캐시가 영구히 멈췄다. 원자적 교체·중복 제거·최대 크기(QUESTION_CACHE_MAX, 기본 500)·updated_at·sample() 을 가진 util/question_cache.py 와 응답에서 리스트일 때만 질문을 꺼내는 extract_question_list() 를 추가하고, 갱신 루프는 앱별 수집을 _collect_for_app() 으로 분리해 앱 하나가 실패해도 나머지 앱과 다음 주기가 계속되게 했으며 수집 결과가 비면 기존 캐시를 유지한다. question_store.questions 는 스냅샷 property 로 남겨 읽기 경로 계약을 유지했다. api.py 는 kiwipiepy 미설치로 import 할 수 없어 캐시 모듈은 단위 테스트로, 갱신 루프는 AST 검사(append 금지·cache.replace() 존재·bare except 금지·헬퍼 import)로 검증하는 tests/unit/test_question_cache.py 를 추가했고, 옛 api.py 를 되돌려 넣어 정적 테스트 4건이 실제로 실패하는지 확인했다. python -m pytest 691 passed(기존 654), python -m pyflakes . undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/API_REFERENCE/CONFIGURATION/TESTING) 갱신. 커밋 0ab2740.
보류 아이디어: A-002 startup 무기한 대기(policy token) 에 timeout/backoff 추가 — 가치 4 / 위험 3 / M
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: A-102/A-206 후속 — 추천 질문 캐시를 프로세스 간 공유(외부 cache 또는 sticky routing) — 가치 3 / 위험 3 / M
보류 아이디어: util/extract_minor.py 의 except Exception: return e (예외 객체 반환) 정리 및 테스트 — 가치 3 / 위험 2 / S
보류 아이디어: A-108 Confluence 그래프의 retrieve → llm_response → END 명시적 edge 추가 — 가치 3 / 위험 3 / S (LangGraph 실행 확인 필요)
2026-09-07
선택: 기동 시 정책 토큰 무기한 대기 제거하고 degraded 기동 도입 (감사 A-002) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: api.py 의 lifespan 이 while token_store.policy_token is None 에서 timeout 없이 0.5초 폴링을 반복해, 토큰 endpoint 장애·잘못된 credential·빈 URL 이면 startup 이 영원히 끝나지 않았다. FastAPI 는 lifespan 이 yield 하기 전까지 요청을 받지 않으므로 /health 조차 호출할 수 없고(docs/OPERATIONS.md 에도 알려진 제약으로 적혀 있었다) 원인 로그도 남지 않았으며, 토큰 갱신 루프는 600초마다 재시도하므로 첫 실패 후 10분간 무의미한 폴링만 돌았다. 총 대기 제한(STARTUP_WAIT_TIMEOUT 기본 60초)·exponential backoff(0.5초→5초)·ready/timeout/aborted 원인과 경과 시간·확인 횟수를 돌려주는 wait_until() 을 담은 설정 모듈 비의존 util/startup_wait.py 를 추가하고, lifespan 이 결과와 무관하게 yield 해 degraded 상태로 기동하도록 바꿨다(실패 시 DEGRADED 로그, 갱신 task 가 이미 죽었으면 abort 로 즉시 종료하고 그 예외를 로깅). degraded 상태를 관측할 수 있게 /health 에 checks.m2m_token 을 추가했고(HTTP 는 계속 200, body status 만 unhealthy — 기존 계약 유지), 동의어 API 장애가 갱신 루프 task 를 죽여 토큰이 영영 발급되지 않던 경로를 막으려 util/token_store.py 의 Kiwi·동의어 초기화를 try/except 로 감쌌다. sleep/monotonic 을 주입할 수 있어 실제로 기다리지 않고 backoff·deadline·abort 를 검증하는 단위 테스트와, api.py·token_store.py 는 import 불가라 AST 정적 검사(무기한 폴링 금지·wait_until 사용·무조건 yield·m2m_token 검사·초기화 보호)를 tests/unit/test_startup_wait.py 에 추가했다. 옛 코드를 되돌려 넣어 정적 검사 5건이 실제로 실패하는지 확인했다. python -m pytest 691 passed(기존 654), python -m pyflakes . undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/OPERATIONS/CONFIGURATION/TESTING) 갱신. 커밋 90199dc.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: util/extract_minor.py 의 except Exception: return e 정리 — 예외 객체가 extract_logic → fileobj["text"] → Milvus 색인까지 흘러가고, 지원하지 않는 확장자는 None 이 된다. "err" 로 통일 필요 — 가치 3 / 위험 2 / S
보류 아이디어: A-102/A-206 후속 — 추천 질문·토큰 캐시를 프로세스 간 공유(외부 cache 또는 sticky routing) — 가치 3 / 위험 3 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
보류 아이디어: api.py 의 미사용 import 정리 후 pyflakes 를 CI 게이트로 승격 — 가치 2 / 위험 1 / S
2026-09-07
선택: /health 가 공용 Milvus default alias 를 끊지 않도록 전용 alias 도입 (감사 A-007) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: api.py 의 /health 가 Milvus 상태를 확인하려고 connections.has_connection("default") 이면 disconnect("default") 한 뒤 기본 주소로 다시 connect(alias="default", ...) 했다. default 는 util/search_module.py·util/milvus_collection.py·util/milvus_confluence.py 등 검색·색인 경로가 실제로 쓰는 alias 이고 readiness probe 가 /health 를 주기적으로 호출하므로, 같은 순간 진행 중이던 요청이 끊긴 연결을 쓰다 실패할 수 있었다. 또 KAI 전용 Milvus 로 default 를 연결해 둔 흐름을 헬스 체크가 기본 주소로 되돌려 다음 호출이 엉뚱한 서버를 보게 된다. 설정·pymilvus 비의존 helper util/health_check.py 를 추가해 전용 alias health_check 로만 연결하고 finally 에서 그 alias 만 닫으며, 이전 검사가 남긴 alias 는 연결 전에 정리하도록 했다(정리 실패는 삼켜 검사 결과를 바꾸지 않고, 연결 실패는 그대로 전파해 라우트의 기존 except Exception 처리와 응답 형태를 유지). 저장소의 Milvus 호출부가 모두 사용 직전에 스스로 connect(alias="default", ...) 함을 확인해 헬스 체크가 default 를 만들어 둘 필요가 없음을 검증했다. api.py 는 kiwipiepy·pymilvus 미설치로 import 할 수 없어, helper 는 가짜 connections 객체로 6건의 단위 테스트(전용 alias 사용·정리·default 무접촉·실패 전파·stale alias 정리·disconnect 실패 무시)를, 라우트는 AST 정적 검사 3건(/health 안에서 default 문자열로 connect/disconnect/has_connection 금지, probe_milvus 호출, import 존재)으로 tests/unit/test_health_check.py 에 넣었다. 옛 코드를 되돌려 넣어 정적 검사 3건이 실제로 실패하는지 확인했다. python -m pytest 809 passed(기존 790), python -m pyflakes . undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/OPERATIONS/API_REFERENCE/TESTING) 갱신. 커밋 be496d6.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: A-007 후속 — util/search_module.py 의 hybrid_search 가 진입 시 default 를 disconnect 하고 KAI/기본 Milvus 를 같은 alias 로 번갈아 연결. host 별 alias 로 분리 필요 — 가치 4 / 위험 3 / M
보류 아이디어: A-003 후속 — 백그라운드 task 에 add_done_callback 로그와 /health checks 노출 — 가치 3 / 위험 2 / S
보류 아이디어: util/extract_minor.py 의 except Exception: return e 정리(예외 객체가 Milvus 색인까지 흘러감) — 가치 3 / 위험 2 / S
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
2026-09-08
선택: 검색 경로의 Milvus default alias 재사용 분리 (감사 A-007 후속) (가치 4 / 위험 3 / 작업량 M)
결과: 성공
요약: util/search_module.py 의 hybrid_search 가 진입할 때마다 connections.disconnect("default") 를 호출한 뒤, 컬렉션이 COLLECTION_LIST_KAI 에 있으면 KAI_MILVUS_HOST, 아니면 MILVUS_HOST 로 같은 default alias 를 다시 연결했다. default 는 util/milvus_collection.py·util/milvus_confluence.py·pipeline 이 공유하는 alias 이므로, 단일 프로세스에서 요청이 겹치면 (1) 무조건적인 진입 시 disconnect 가 다른 요청이 사용 중이던 연결을 끊고, (2) KAI 검색이 default 를 KAI 서버로 연결해 둔 사이 다른 요청이 기본 주소로 같은 alias 를 연결하면 pymilvus 가 주소 불일치로 실패하거나 앞선 요청이 엉뚱한 서버를 보게 된다. 설정·pymilvus 비의존 helper util/milvus_connection.py 를 추가해 alias_for(host, port) 가 milvus_<주소>_<sha1 8자> 형태의 결정적 alias 를 만들고(normalize_endpoint 로 int 19530 과 str "19530" 을 같은 alias 로 합치며, 해시 덕에 정규화 충돌이 없다), ensure_connection() 이 그 alias 가 없을 때만 연결한 뒤 alias 를 돌려주도록 했다. search_module 의 연결 지점 9곳을 전부 helper 로 바꾸고 Collection(...) 에 using=alias 를 명시했으며 진입 시 disconnect 는 제거했다. default 를 그대로 쓰는 milvus_collection.py 등은 항상 기본 주소 하나만 보고 호출 직전에 스스로 연결하므로 동작이 바뀌지 않아 범위에서 제외했다. pymilvus 미설치라 helper 는 가짜 connections 로 단위 테스트(주소별 alias·default 무접촉·재사용·두 서버 동시 연결 유지·빈 host 거부·실패 전파)하고, search_module 은 AST 정적 검사(disconnect 금지·default 문자열 금지·using= 누락 금지·helper import·KAI 주소 선택 유지)로 검증하는 tests/unit/test_milvus_connection.py 를 추가했다. 옛 search_module.py 를 되돌려 넣어 정적 검사 4건이 실제로 실패하는지 확인했다. python -m pytest 875 passed(기존 848), python -m pyflakes . undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/MILVUS_SEARCH/CODEBASE_MAP/TESTING) 갱신. 커밋 5d67513.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: A-003 후속 — 백그라운드 task 에 add_done_callback 로그와 /health checks 노출 — 가치 3 / 위험 2 / S
보류 아이디어: util/extract_minor.py 의 except Exception: return e 정리(예외 객체가 Milvus 색인까지 흘러감) — 가치 3 / 위험 2 / S
보류 아이디어: util/milvus_collection.py·util/milvus_confluence.py·pipeline 의 default alias 도 ensure_connection 으로 통일 — 가치 2 / 위험 2 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
2026-09-09
선택: 첨부파일 추출이 예외 객체·None 대신 항상 문자열을 반환 (감사 A-110) (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: util/extract_minor.py 의 extract() 가 except Exception as e: return e 로 예외 객체 자체를, extract_undrm() 이 암묵적으로 None 을, util/pipeline_module.extract_logic() 이 지원하지 않는 확장자에서 None 을 반환했다. 이 값들은 service/pipelineservice.confluence_pipeline_spacefile_detail 의 if result == 'err' 검사를 통과해 fileobj["text"] 에 들어가고 util/chunker.proc_chunker_confluence_file 의 split_text(text) 에서 AttributeError 를 내며, 호출부의 except Exception: raise 를 타고 올라가 첨부파일 하나 때문에 스페이스 전체 색인이 중단되고 원인 파일도 로그에 남지 않았다. 설정·pandas 비의존 helper util/extract_text.py 를 추가해 json/csv/html/xlsx/일반 텍스트 분기를 모으고 실패를 EXTRACT_ERROR(“err”), UTF-8 디코딩 불가를 DRM_MARKER(“DRM”) 문자열로 통일했으며(pandas 는 xlsx 분기에서만 지연 import 해 모듈을 테스트에서 그대로 import 할 수 있다), extract()/extract_undrm() 은 sniff_drm 만 다른 wrapper 로 남겨 NUL 바이트 조기 판정 유무라는 기존 차이를 유지했다. extract_logic() 은 지원하지 않는 확장자에서 경고 로그와 함께 "err" 를 반환하고 bare except: 를 except Exception: 으로 좁혀 CancelledError 를 삼키지 않게 했다. 검증은 tests/unit/test_extract_text.py 에서 helper 를 실제 import 해 22건(각 확장자 변환·DRM 판정·실패 시 문자열 반환·모든 경로 str 반환)으로, import 할 수 없는 두 모듈은 AST 정적 검사(저장소 전체 except ... as e: return e 금지, wrapper 가 공용 helper 사용, extract_logic 마지막 문장이 return, bare except 금지)로 했고, 옛 코드를 되돌려 넣어 정적 검사 4건이 실제로 실패하는지 확인했다. python -m pytest 907 passed(기존 885), python -m pyflakes . undefined name 0건. docs 3종(CURRENT_STATE_AUDIT/CODEBASE_MAP/TESTING) 갱신. 커밋 efd0878.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: A-003 후속 — 백그라운드 task(question/feedback)에 add_done_callback 로그와 /health checks 노출 — 가치 3 / 위험 2 / S
보류 아이디어: A-102/A-206 후속 — 추천 질문·토큰 캐시를 프로세스 간 공유 — 가치 3 / 위험 3 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
보류 아이디어: confluence_pipeline_spacefile_detail 의 루프 안 insert_to_milvus_cf 누적 재삽입 — 파일마다 누적된 insert_batch_list 전체를 다시 삽입해 N개 파일이면 N(N+1)/2 건을 쓴다 — 가치 3 / 위험 2 / S
2026-09-09
선택: 백그라운드 task 종료를 로그·/health 로 관측 가능하게 (감사 A-003) (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: api.py 의 lifespan 이 asyncio.create_task() 로 띄우는 무한 루프 task 3개(추천 질문 갱신·M2M 토큰 갱신·피드백 배치)는 아무도 await 하지 않아, 본문에서 예외가 새어 나가면 예외가 task 안에 갇힌 채 조용히 죽고 로그 한 줄 남지 않았다. A-002 작업으로 토큰 task 만 startup 시점 한 번의 degraded 로그를 얻었을 뿐, 기동 이후에 죽는 경우와 나머지 두 task 는 관측 수단이 없어 추천 질문이 영원히 갱신되지 않거나 피드백이 DB 에 반영되지 않는데도 /health 는 계속 healthy 를 돌려줬다. 설정·pymilvus 비의존이고 task 를 done()/cancelled()/exception()/add_done_callback() duck typing 으로만 다루는 util/task_supervisor.py 를 추가해, register(name, task) 가 done callback 으로 종료 이유(예외/취소/정상 종료)를 로그에 남기고(예외를 실제로 읽으므로 asyncio 의 “Task exception was never retrieved” 경고도 사라진다) snapshot()/stopped() 로 상태를 돌려주게 했다. 무한 루프이므로 예외 없는 종료(stopped)도 비정상으로 판정하며, 정상 shutdown 시의 취소는 begin_shutdown() 이후 info 로 낮춘다. lifespan 은 세 task 를 이름(question_refresh/m2m_token_refresh/feedback_batch)으로 등록하고 yield 직후 begin_shutdown() 을 호출하며, /health 는 checks.background_tasks 로 각 task 상태를 노출하고 하나라도 살아 있지 않으면 전체 status 를 unhealthy 로 만든다(HTTP 는 계속 200 — 기존 계약 유지). api.py 는 kiwipiepy·pymilvus 미설치로 import 할 수 없어, helper 는 가짜 task·로거로 11건의 단위 테스트(예외/취소/정상 종료 로그 레벨·상태 판정·예외 읽기·shutdown 구분·callback 이 절대 예외를 내지 않음)로, api.py 는 AST 정적 검사 5건(세 task 등록 누락 금지·supervisor 를 거치지 않는 create_task 금지·begin_shutdown 호출·/health 의 snapshot/stopped 호출과 background_tasks 검사 항목·helper import)으로 tests/unit/test_task_supervisor.py 에 넣었다. 옛 api.py 를 되돌려 넣어 정적 검사 5건이 실제로 실패하는지 확인했다. python -m pytest 933 passed(기존 907), python -m pyflakes . undefined name 0건. docs 5종(CURRENT_STATE_AUDIT/OPERATIONS/API_REFERENCE/CODEBASE_MAP/TESTING) 갱신. 커밋 1bf2426.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: confluence_pipeline_spacefile_detail 의 루프 안 insert_to_milvus_cf 누적 재삽입 — 파일마다 누적된 insert_batch_list 전체를 다시 삽입해 N개 파일이면 N(N+1)/2 건을 쓴다 — 가치 3 / 위험 2 / S
보류 아이디어: A-102/A-206 후속 — 추천 질문·토큰 캐시를 프로세스 간 공유 — 가치 3 / 위험 3 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
보류 아이디어: 백그라운드 task 의 “마지막 성공 시각” 노출 — 지금은 task 가 살아 있기만 하면 healthy 라, 루프는 도는데 매 주기 예외를 삼키는 상태를 구분하지 못한다 — 가치 3 / 위험 2 / S
2026-09-09
선택: 백그라운드 task 종료 관측 재구현 — shutdown 오탐 ERROR 제거 (감사 A-003, PR #13 리뷰 반영) (가치 4 / 위험 2 / 작업량 M)
결과: 성공
요약: 닫힌 PR #13 의 목적(무한 루프 task 3개가 조용히 죽는 것을 로그·/health 로 관측)은 유지하되, 리뷰가 지적한 두 결함을 고쳐 다시 구현했다. util/task_supervisor.py 의 _on_done() 이 shutdown 예외 처리를 'cancelled' 상태에만 적용해, CancelledError 를 잡아 break 하고 정상 반환하는 feedback_batch_loop(Python 3.8+ 에서 cancelled()==False·exception() is None → 'stopped')이 서버 정상 종료마다 오탐 ERROR 를 남기던 것을, 판정 기준을 task 상태가 아닌 _shutting_down 플래그로 바꿔 제거했다(예외로 끝난 failed 는 shutdown 중에도 ERROR 유지). /health 는 readiness probe 폴링 경로이므로 note_state_change() 를 추가해 상태 전이 시점에만 로깅하고, docs/OPERATIONS.md 의 상태 표는 stopped/cancelled 가 정상 종료 시에도 나타남을 명시하고 판정을 로그 레벨(INFO ... 종료 (서버 shutdown / …) vs ERROR)로 하도록 고쳤다. 검증은 리뷰 요구대로 대역이 아닌 진짜 asyncio.Task 로 실제 shutdown 경로(begin_shutdown() → cancel() → gather())를 재현하는 테스트 6건을 추가했고(취소를 삼키는 루프·전파하는 루프·shutdown 아닌 조기 종료·예외 종료), /health 의 전이 로깅은 AST 정적 검사로 잡았다. 옛 구현을 되돌려 넣어 핵심 3건(test_shutdown_of_real_task_swallowing_cancel_logs_no_error 포함)이 실제로 실패하는지 확인했다. python3 -m pytest -q 941 passed(기존 907), python -m pyflakes . undefined name 0건. docs 5종(CURRENT_STATE_AUDIT/OPERATIONS/API_REFERENCE/CODEBASE_MAP/TESTING) 갱신. 커밋 effe9e0.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: 백그라운드 task 의 마지막 성공 시각(heartbeat) 노출 — 이제 task 생존은 보이지만 루프가 살아 있으면서 매 주기 예외를 삼키는 상태는 여전히 healthy 로 보인다 — 가치 3 / 위험 2 / S
보류 아이디어: confluence_pipeline_spacefile_detail 의 루프 안 insert_to_milvus_cf 누적 재삽입(N개 파일이면 N(N+1)/2 건) — 가치 3 / 위험 2 / S
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
보류 아이디어: bare except → 구체 예외로 범위 축소(service/pipelineservice.confluence_pipeline_kcblaw 등 잔여) — 가치 3 / 위험 3 / M
2026-09-09
선택: 스페이스 첨부 색인의 누적 배치 반복 삽입 제거 (감사 A-111) (가치 4 / 위험 2 / 작업량 S)
결과: 성공
요약: service/pipelineservice.confluence_pipeline_spacefile_detail 이 파일마다 insert_batch_list.extend(result) 로 청크를 누적한 뒤 루프 안에서insert_to_milvus_cf(CONFLUENCE_COLLECTION_NAME, insert_batch_list) 로 누적 목록 전체를 다시 삽입했다. util/milvus_confluence.insert_to_milvus_cf 는 upsert 가 아니라 collection.insert() 이고 Milvus 는 primary key 가 같아도 삽입을 막지 않으므로, 파일 N 개짜리 스페이스를 색인하면 첫 파일의 청크가 N 번·두 번째가 N-1 번 들어가 전체 쓰기량이 N(N+1)/2 배치로 불어나고 컬렉션에 같은 id 의 중복 entity 가 쌓인다(중복은 검색 결과에 그대로 나타나고, 파일마다 flush()/load() 를 다시 도는 탓에 스페이스가 클수록 색인이 급격히 느려진다). 같은 파일 위쪽의 confluence_piepeline_detail 은 루프 밖에서 한 번만 삽입하므로 대조군이 있었다. 설정·Milvus 비의존 helper util/space_file_indexer.py 를 추가해 다운로드·추출·청킹·삽입을 주입받는 index_space_files() 에 파일 단위 루프를 담고 삽입에 이번 파일의 청크만 넘기도록 했으며, 청크가 없으면 삽입을 건너뛰고 로그는 파일별 건수와 누적 건수를 함께 남긴다. 반환값은 여전히 전체 누적 목록이라 api.py 의 len(insert_list) 적재 건수 로그 계약이 유지되고, 파일 하나의 예외가 스페이스 색인을 중단시키던 기존 의미와 EXTRACT_ERROR 안내 문구 처리도 그대로다. 검증은 helper 를 실제 import 한 단위 테스트 14건(파일당 1회 삽입·중복 id 0·삽입 건수가 파일 수에 비례(2/4/8)·반환 누적 목록·빈 청크 스킵·추출 실패 문구·예외 전파·파일별 로그)과, import 할 수 없는 service/pipelineservice.py 의 AST 정적 검사 4건으로 했다. 정적 검사는 “같은 루프 층에서 .extend() 로 누적한 이름을 insert_to_milvus_cf 에 다시 넘김” 패턴을 파일 전체에서 막되 중첩 루프는 건너뛰어, 안쪽 루프에서 누적하고 바깥에서 한 번 삽입하는 confluence_piepeline_detail 은 통과시킨다. 옛 코드를 되돌려 넣어 정적 검사 4건이 실제로 실패하는지 확인했다. python3 -m pytest -q 969 passed(기존 941), python -m pyflakes . undefined name 0건. docs 4종(CURRENT_STATE_AUDIT/PIPELINE/CODEBASE_MAP/TESTING) 갱신. 커밋 6b83be1.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: 백그라운드 task 의 마지막 성공 시각(heartbeat) 노출 — task 생존은 보이지만 루프가 살아 있으면서 매 주기 예외를 삼키는 상태는 여전히 healthy 로 보인다 — 가치 3 / 위험 2 / S
보류 아이디어: bare except → 구체 예외로 범위 축소(service/pipelineservice.confluence_pipeline_kcblaw, api.py 의 /run_confluence_pipeline_spacefile 등 잔여) — 가치 3 / 위험 3 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
보류 아이디어: A-104 후속 — collection name allowlist 와 field 별 타입 검증 — 가치 3 / 위험 2 / M
2026-09-10
선택: 백그라운드 루프의 마지막 성공 시각(heartbeat)을 /health 로 노출 (감사 A-112) (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: A-003 으로 task 생존 여부는 /health 에 드러났지만, 세 무한 루프는 모두 주기 안의 예외를 잡고 다음 주기로 넘어가므로 “살아 있으면서 매 주기 실패” 하는 상태가 여전히 healthy 로 보였다(token_store.fetch_external_token 은 실패를 None 으로 삼키고, auto_refresh_question 은 수집이 비면 기존 캐시를 유지한 채 계속 돌며, feedback_batch_loop 도 주기 예외를 로그만 남긴다 — policy token 이 한 번도 발급되지 않은 경우만 checks.m2m_token 으로 드러났고, 발급 후 갱신이 계속 실패하는 경우는 보이지 않았다). util/task_supervisor.py 에 heartbeat(name) 과 register(..., max_silence=N) 을 추가해, 마지막 성공 이후 N 초가 지난 task 를 stale 로 만들고 stale() 로 조회하게 했다 — 살아 있는 상태이므로 중단(stopped())과 분리해 /health 가 “중단된 task” 와 “지연된 task” 를 나눠 알리고, 전이 시점에만 로깅하는 기존 폴링 규칙은 그대로 쓴다. 경과는 시스템 시각 변경에 흔들리지 않도록 monotonic 시계로 재고(테스트 주입 가능), 첫 성공 전에는 등록 시각을 기준선으로 삼아 기동 후 한 번도 성공하지 못한 경우도 잡는다. 허용 간격을 주지 않은 task 의 응답 형태는 그대로다. heartbeat 는 성공한 주기에만 호출한다(추천 질문은 캐시를 실제로 교체한 주기, 토큰은 policy token 을 실제로 받은 주기, 피드백 배치는 예외 없이 끝난 주기). 허용 간격은 루프 주기의 두 배 + 여유(1800→3900, 600→1500)라 한 주기를 건너뛴 것만으로는 경보가 나지 않는다. task 이름은 TASK_* 상수로 모아 루프 본문의 오타를 막았다. 검증은 supervisor 를 가짜 시계로 직접 호출하는 단위 테스트 9건(stale 판정·회복·기준선·죽은 task 를 감추지 않음·전이 판정)과, kiwipiepy·psycopg2 미설치로 import 할 수 없는 세 파일(api.py/util/token_store.py/service/feedbackservice.py)에 대한 AST 정적 검사 5건(세 루프의 heartbeat 호출·그 호출이 성공 분기 안인지·등록의 max_silence·/health 의 stale 노출)으로 했고, 옛 코드를 되돌려 넣어 정적 검사 5건이 실제로 실패하는지 확인했다. python3 -m pytest -q 984 passed(기존 969), python -m pyflakes . undefined name 0건. docs 5종(CURRENT_STATE_AUDIT/OPERATIONS/API_REFERENCE/CODEBASE_MAP/TESTING) 갱신. 커밋 277d90e. 남은 한계는 문서에 명시: fetch_unprocessed_feedback 이 DB 조회 실패를 빈 목록으로 돌려주므로 조회가 계속 실패하는 상태는 피드백 배치의 성공으로 기록된다.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: Milvus 적재 실패가 조용히 성공으로 보고되는 문제(insert_to_milvus_cf 가 traceback 문자열을 return 하고 호출부가 무시) — 가치 3 / 위험 2 / S
보류 아이디어: fetch_unprocessed_feedback 이 DB 조회 실패와 “처리할 피드백 없음” 을 구분하지 않는 문제(A-112 후속) — 가치 3 / 위험 1 / S
보류 아이디어: bare except → 구체 예외로 범위 축소(service/pipelineservice.confluence_pipeline_kcblaw, api.py 의 /run_confluence_pipeline_spacefile 등 잔여) — 가치 3 / 위험 3 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M
2026-09-10
선택: Milvus 적재 실패가 조용히 성공으로 보고되는 문제 수정 (감사 A-113) (가치 3 / 위험 2 / 작업량 S)
결과: 성공
요약: util/milvus_confluence.insert_to_milvus_cf 는 실패를 예외로 올리지 않고 logger.error 뒤 return traceback.format_exc() 로 반환했고(insert_to_milvus 는 아무것도 반환하지 않았다), 호출부인 util/space_file_indexer.index_space_files 와 service/pipelineservice.confluence_piepeline_detail 은 반환값을 보지 않았다. 그래서 Milvus 가 죽어 있거나 스키마가 어긋나 모든 배치의 적재가 실패해도 파이프라인은 끝까지 돌고 api.py 는 /run_confluence_pipeline_spacefile 에 “N 건 적재 종료”, /run_confluence_pipeline 에 total_insert 가 채워진 dict 를 돌려줬다 — 운영자는 색인이 끝난 줄 알지만 컬렉션은 비어 있고 재색인 판단 근거가 사라진다. MilvusInsertError(RuntimeError) 를 두고 두 적재 함수의 except 절이 원인을 체이닝해(raise ... from e) 올리도록 고쳤으며(적재할 데이터가 없는 경우는 실패가 아니므로 기존대로 조용히 반환), index_space_files 는 삽입만 try/except 로 감싸 그 파일만 실패로 세고 다음 파일로 넘어가고(파일 하나의 적재 실패로 스페이스 전체를 중단하지 않던 동작 유지) 반환값을 SpaceFileIndexResult(inserted, failed_files) 로 바꿔 inserted 에 적재에 성공한 청크만 담게 했다. confluence_piepeline_detail 도 배치 적재를 감싸 성공한 배치만 total_insert 에 더하고 실패분을 total_failed 로 돌려주며, api.py 는 스페이스 응답 문구에 실패 파일 목록을 덧붙여 ERROR 로그를 남기고 페이지 파이프라인은 total_failed 가 있을 때 ERROR 를 남긴다. 검증은 helper 를 실제 import 하는 단위 테스트(실패 파일이 성공 건수에 섞이지 않음·전부 실패 시 0 건·나머지 파일 계속 처리·실패 ERROR 로그)와, pymilvus·psycopg2·kiwipiepy 로 import 할 수 없는 세 파일에 대한 AST 정적 검사 8건(traceback 반환 금지·모든 except 가 raise·전용 예외 사용·적재 호출을 직접 담은 try 안에서만 total_insert 증가·반환 dict 의 total_failed·두 라우트의 실패 노출)으로 했고, 옛 코드를 되돌려 넣어 정적 검사 7건과 helper 테스트 9건이 실제로 실패하는지 확인했다. python3 -m pytest -q 1001 passed(기존 984), python -m pyflakes . undefined name 0건. docs 5종(CURRENT_STATE_AUDIT/PIPELINE/API_REFERENCE/CODEBASE_MAP/TESTING) 갱신. 커밋 acfc432. 남은 한계는 문서에 명시: delete_to_milvus_space_file 등 삭제 경로는 여전히 실패를 삼킨다.
보류 아이디어: A-105 후속 — 공통 오류 응답 model 도입과 traceback 노출 제거(A-106 연계) — 가치 4 / 위험 3 / L
보류 아이디어: fetch_unprocessed_feedback 이 DB 조회 실패와 “처리할 피드백 없음” 을 구분하지 않는 문제(A-112 후속) — 가치 3 / 위험 1 / S
보류 아이디어: Milvus 삭제 경로(delete_to_milvus_space_file·delete_to_milvus)도 실패를 삼키는 문제(A-113 후속, 신규) — 가치 3 / 위험 2 / S
보류 아이디어: bare except → 구체 예외로 범위 축소(service/pipelineservice.confluence_pipeline_kcblaw, api.py 의 파이프라인 라우트 잔여) — 가치 3 / 위험 3 / M
보류 아이디어: A-107 후속 — 재시도·circuit breaker·공용 requests.Session 도입 — 가치 3 / 위험 3 / M