Skip to content

장마감이후 호출하는 경우 KeyError에 대한 방어코드 추가함. - #76

Open
visualmoney wants to merge 225 commits into
Soju06:mainfrom
visualmoney:main
Open

장마감이후 호출하는 경우 KeyError에 대한 방어코드 추가함.#76
visualmoney wants to merge 225 commits into
Soju06:mainfrom
visualmoney:main

Conversation

@visualmoney

Copy link
Copy Markdown

🛠️ PR Summary

🌟 요약

어떤 것이 변경되었나요? 간략히 설명해주세요.

인증 토큰을 자동으로 관리하는 기능을 추가했습니다.

📊 주요 변경 사항

주요 변경 사항을 적어주세요.

  • utils.workspace.py 파일을 추가했습니다.
    PyKis 라이브러리의 개인 작업 공간을 관리하는 기능을 추가했습니다.
  • kis.py에서 PyKis 메인 클래스 생성자에 keep_token 인자를 추가했습니다.
    keep_token이 True이면 인증 토큰을 개인 작업 공간에서 자동으로 관리합니다.
  • 웹소켓 Ping을 로깅하는 코드를 제거했습니다.

🎯 목적 및 영향

  • 목적: 왜 이 PR이 필요한가요?
    인증 토큰을 자동으로 관리하는 기능을 추가하여 라이브러리를 손쉽게 사용할 수 있도록 합니다.

  • 영향: 이 변경 사항이 어떤 영향을 미치나요?
    토큰 로드 및 저장을 자동으로 처리하므로 비전문 사용자가 토큰을 관리하는 부담이 줄어듭니다.

…래스가 정상 동작하는지 확인 3. 지 않은 서브클래스는 여전히 추상으로 취급되는지 확인)
visualmoney and others added 30 commits August 28, 2026 23:10
CLAUDE.md 전문 269줄에 "이슈", "GitHub", "PR", "라벨" 이 한 번도 나오지
않았습니다. 새 세션의 AI 는 이 문서만 읽으면 작업 상태가 마크다운에 있다고
결론짓습니다. 실제로 그렇게 해서 116줄짜리 복제본이 생겼습니다.

삭제한 것

  - "To-Do List 작성" 3곳 (108 / 222 / 254행)
  - "Phase별 문서 요구사항" 절 전체

Phase 폐기의 근거는 실측했습니다. 2026 이후 커밋 47건 중 Phase 표기 0건인데,
"Phase 완료 시 완료 보고서" 규칙이 만든 산출물 4건은 동결된 채 남아 있습니다.

신설한 것

  - "작업 상태는 어디에 사는가" — 무엇이 어디에 사는지 9행 표
  - "세션 시작 시" — 대기열 조회

착수 전에 문서가 자기 규칙을 어기고 있는 것을 발견했습니다. 문서 체계 트리
바로 위에 "실제 존재하는 파일만 적습니다"라고 적어 놓고 존재하지 않는 경로
4개를 가리켰습니다 (ARCHITECTURE_REPORT_V3_KR.md, DEVELOPMENT_REPORT_*.md,
user/QUICKSTART.md, user/TUTORIALS.md). #25 #29 #31 과 같은 결함이, 그
결함을 경고하는 문서 안에 있었습니다.

AGENT_WORKFLOW_RULES.md 의 사실 오류 2건도 고쳤습니다.

  - "apply_patch 로 편집" — git grep 결과 이 문서에만 등장하는 도구
  - "reports/coverage_html" — 실제 경로는 reports/htmlcov/

"매 프롬프트마다 프롬프트 문서 작성"은 지켜진 적이 없습니다. 2026-08-28 에
개발 일지 12건이 쌓이는 동안 프롬프트 문서는 5건이었습니다. 실제 운영에
맞춰 "작업을 시작하는 요청 하나당 한 건"으로 바꿨습니다.

문서에 적은 gh 명령은 전부 실행해 보고 넣었습니다. 검증 안 된 명령을 규칙
문서에 넣는 것이 이 개정이 고치려는 실패 양상 그 자체입니다.


Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
같은 모양의 while True 루프가 8곳에 복사돼 있었고, 다른 것은 "어느 필드에
누적하는가" 한 줄뿐이었습니다. api/account/ 순 -104줄.

이슈는 루프 11곳을 한 종류로 봤지만 두 계열이었습니다.

  KIS 커서 연속조회  8곳  KisPage + is_last + next_page. 골격 동일
  날짜/시간 커서     3곳  KisPage 를 아예 안 씀. 봉 시각에서 커서를 도출

차트 계열은 종료 조건이 이질적이고(빈 봉 / cursor < last / start 가
timedelta 일 때 재계산 / 해외는 시각별 dedup), 해외 당일차트는
for i in range(FOREIGN_MAX_PERIODS) 로 NMIN 을 늘리는 다른 기제입니다.
억지로 밀어 넣으면 역효과이므로 8곳만 덮습니다.

설계는 merge 콜백을 골랐습니다. 이슈의 "제외" 항목이 응답 클래스 변경을
배제했고, __merge__ 는 누적 규칙이 루프에서 멀어지며, 문자열 필드명은 타입
검사를 잃습니다.

밟은 함정: response_type 은 팩토리여야 합니다. dynamic.py:257 이 인스턴스를
받으면 그 인스턴스에 그대로 파싱하므로, 하나를 돌려 쓰면 모든 페이지가 같은
객체가 되고 merge 가 자기 자신을 이어붙여 결과가 조용히 불어납니다. 예전
루프들이 매 반복 응답 객체를 새로 만든 이유입니다. 인스턴스를 주면 TypeError
로 막습니다.

페이징 루프를 직접 검증하는 테스트가 하나도 없었습니다. 신설한 9건에 대해
네 가지 변이(continuous 무시 / is_last 무시 / 첫 페이지 continuous / merge
생략)를 만들어 전부 실패하는지 확인했습니다.

order_profit.py 의 fetch 2곳은 #43 대상 목록에 없었지만 페이징 이관에
스펙이 필요해 함께 만들었습니다.


Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
614b68e 가 "타이밍 단언의 상한 여유 확대"로 상한 6곳을 SCHEDULING_SLACK 으로
바꾸면서 이 한 곳을 빠뜨렸습니다. 하필 스레드 4개를 동시에 돌려 스케줄링에
가장 민감한 테스트인데 여유가 가장 좁았습니다(기대 1.0 에 +0.3).

커버리지를 켜면 10회 중 1회꼴로 터졌습니다.

하한 0.9 는 그대로 둡니다. 하한은 "유량 제한이 실제로 걸렸는가"를 검증하므로
엄격해야 합니다.

상한만 늘리고 통과만 보면 회귀를 못 잡는 상태가 될 수 있어, 프로덕션 코드를
변이시켜 두 경계를 각각 확인했습니다.

  대기 sleep 제거      -> assert 0.9 <= 0.0009…        하한이 잡음
  대기에 period*3 추가 -> assert 4.05… <= (1.0 + 2.0)  상한이 잡음

수정 후 커버리지 ON 20회 연속 통과.


Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
이슈 2건(#43 #44)을 닫고 작업 관리 방식을 마크다운에서 이슈 트래커로
옮겼습니다. PR 6건 머지.

새 규칙에 따라 To-Do List 마크다운은 만들지 않습니다. 다음 세션이 볼 곳은
gh issue list --label next-up 입니다.


Claude-Session: https://claude.ai/code/session_01UJA8JX9PzQNq7zdeNrnMth

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: import-linter 계약으로 역방향 의존 회귀 차단 (#50)

ARCHITECTURE.md 불변식 2번("새로운 모듈-레벨 역방향 간선을 만들지 않습니다")을
기계화합니다. #17 · #18 이 없앤 간선 2건이 되살아나는 것을 CI 가 막습니다.

계약을 넣으며 세 가지가 드러났습니다.

1. grimp 이 패키지의 1/4 만 보고 있었습니다. src/vmkis 의 디렉터리 18개 중
   13개에 __init__.py 가 없어(암묵적 네임스페이스 패키지) root_package 단수로는
   모듈 20개만 잡힙니다. root_packages 복수로 나열해 92개 전부를 담습니다.
   빠뜨려도 조용히 초록이 되므로 그래프 커버리지 가드 테스트를 함께 넣습니다.

2. 세 번째 역방향 간선이 있었습니다. utils/diagnosis.py 의 모듈 레벨
   `import vmkis` 가 루트 파사드를 통해 kis/api/client/scope 전체를 끌어옵니다.
   필요한 값은 버전과 배포명 둘뿐이라 vmkis.__env__ 를 직접 봅니다.

3. ignore_imports 는 위치를 보지 않습니다. 면제된 지연 import 를 모듈 레벨로
   올려도 계약은 통과합니다(실측). AST 테스트가 그 자리를 막습니다.
   그 지연 import 에 없던 사유 주석도 달았습니다(불변식 3번).

되돌려 확인: 위반 5종을 일부러 만들어 3종이 계약에, 2종이 테스트에 잡히는 것을
확인했습니다. 상세는 개발 일지에 있습니다.

Closes #50

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq

* docs: 남은 미결을 #63 · #64 로 분리 (#50)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#38 이 VmKis.close() 에 getattr 가드를 넣어 근본 원인을 고쳤으므로 그것을
우회하던 @patch("vmkis.kis.VmKis.__del__", ...) 3곳을 지웁니다.

지우기만 해서는 이슈의 목적("회귀 탐지력 회복")이 절반만 달성됩니다.
실측: 패치를 지운 뒤 close() 의 getattr 가드를 걷어내고 돌리면

    42 passed, 3 warnings

입니다. 회귀가 로그에 보이지만 CI 를 막지 않습니다. 그래서 이슈가 "(선택)"으로
남긴 filterwarnings 를 함께 넣습니다.

    filterwarnings = ["error::pytest.PytestUnraisableExceptionWarning"]

같은 조작이 이제 패치가 붙어 있던 바로 그 3건을 실패시킵니다.

전역 filterwarnings = ["error"] 는 쓰지 않습니다. 스위트에 남은 무해한 경고
9건까지 전부 실패로 만듭니다.

close() 의 가드에는 무엇이 자기를 지키는지 역참조 주석을 달았습니다.
가드 옆에 그것이 없으면 다음 사람은 "불필요한 방어"로 읽습니다.

Closes #42


Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* build: 서브패키지 13곳에 __init__.py 추가 (#64) + event→api 판정 (#63)

## #64 — A 채택

src/vmkis 의 디렉터리 18개 중 13개에 __init__.py 가 없었습니다. 암묵적
네임스페이스 패키지라 grimp 이 스캔에서 건너뛰고, 루트 하나만 주면 모듈 92개 중
20개만 잡힌 채 계약이 초록으로 통과했습니다(#50).

설계가 아니었음을 먼저 확인했습니다. git 이력상 삭제된 적이 없고(업스트림 원본),
문서에 namespace 언급 0건, pkgutil/walk_packages 사용처 0건입니다.

측정 결과 A 는 손해가 없습니다.

    그래프 모듈    20 -> 92        설정  root_packages 6줄 -> root_package 1줄
    테스트     1052 passed 유지    계약  2 kept, 0 broken 유지
    고의 위반 2종  여전히 잡힘     휠    80 -> 93 파일

13개 전부 주석만 넣고 비워 둡니다. 재export 허브가 되면 그것이 순환의 시작이고,
__init__.py 를 만든다는 것은 "편의 import 를 넣고 싶은 자리" 13개를 만드는
일이기도 합니다. 각 파일이 스스로 그러지 말라고 말합니다.

## #63 — 의도적 (코드 변경 없음)

event -> api 3건은 전부 어노테이션 전용이라 TYPE_CHECKING 으로 옮길 수 있습니다.
실제로 옮겨 보고 되돌렸습니다.

  1. api <-> event 는 양방향 순환입니다(12 <-> 4). event -> api 만 없애도 순환은
     남습니다. 이미 의도적으로 동결한 api <-> adapter(6 <-> 32)와 구조가 같습니다.
  2. 옮기면 get_type_hints(KisSimpleProduct/KisSimpleOrderNumber) 가 동작하던
     것이 NameError 가 됩니다. isinstance 는 계속 되므로 테스트 1052건은 전부
     통과합니다 - 테스트로는 이 손실이 안 보입니다.

불변식 2번 동결 표에 행을 추가하고, 판정을 뒤집을 유일한 조건(MARKET_TYPE 의
자리)을 불변식 4번에 적었습니다.

Closes #63
Closes #64

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq

* docs: 로컬 428 vs CI 359 수치 불일치의 원인 정정 (#64)

앞 커밋의 개발 일지와 PR 본문이 "root_packages 로 겹치는 루트를 주면 중복
계상된다"고 적었습니다. 틀렸습니다.

간선 집합을 덤프해 비교하니 작업트리와 깨끗한 클론이 470개로 완전히 동일합니다.
차이는 그래프가 아니라 .grimp_cache 였습니다.

    lint-imports --no-cache   작업트리 428 / 클론 428   ← 일치

grimp 의 캐시는 파일 단위로만 무효화되고 세션 설정 변경(root_packages 복수 ->
root_package 단수)은 무효화하지 않습니다. 옛 설정으로 만든 캐시가 재사용됐습니다.

계약 판정 자체는 오염되지 않습니다 - 따뜻한 캐시에서 고의 위반을 넣어
"1 kept, 1 broken" 을 확인했습니다. 틀어지는 것은 보고되는 수치뿐입니다.

.grimp_cache 는 grimp 이 스스로 .gitignore(*) 를 써 넣으므로 저장소에 들어가지
않습니다. pyproject.toml 의 계약 설정 옆에 --no-cache 안내를 남깁니다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
requires_api 17개가 전부 tests/unit/ 에 있었습니다. 실제 KIS 서버에 HTTP 요청을
보내고 실계좌 자격증명이 필요한 테스트입니다. 디렉터리와 마커가 서로 다른 말을
하면 "tests/unit/ 을 돌린다"의 의미가 깨집니다.

갈라 옮길 필요는 없었습니다. 두 파일 다 pytestmark 로 파일 전체가 requires_api
입니다(6/6, 11/11). git mv 두 번으로 끝났고 from tests.env import 도 새 위치에서
그대로 동작합니다.

## "(선택)" 항목이 선택이 아니었습니다

이슈가 괄호로 남긴 디렉터리 단위 마커를 재 보니 이미 어긋나 있었습니다.

    tests/integration  전체 29개 수집 / integration 마커 9개

tests/performance/conftest.py 가 만들어진 이유(30개 중 8개)와 같은 상황이 같은
저장소에서 두 번째로 반복된 것입니다. 게다가 이 이동 자체가 마커 없는 파일을
5개에서 7개로 늘릴 참이었습니다. tests/integration/conftest.py 를 함께 넣습니다.

게이팅은 바뀌지 않습니다. CI 게이트는 -m 'not requires_api and not performance'
라 integration 을 제외하지 않습니다.

되돌려 확인: 마커 없는 새 파일을 넣어 conftest 유무로 1 / 0 을 확인했고, 저장소
전체 -m integration 이 46개(29+17), tests/unit 은 0개임을 확인했습니다.

## 함께 고친 것

CONTRIBUTING.md 의 테스트 구조 트리가 없는 경로 4개를 가리키고 있었습니다
(tests/fixtures/, test_stock_quote.py, test_websocket.py, test_load_config.py).
CLAUDE.md 가 자기 트리에 대해 적어 둔 것과 같은 문제라 ls 해서 다시 썼습니다.

docs/developer/DEVELOPER_GUIDE.md 의 트리는 6개 전부 허구지만, 그 문서는 트리만
틀린 게 아니라 전체가 옛 레이아웃 기준이라 범위 밖으로 남깁니다.

Closes #41


Claude-Session: https://claude.ai/code/session_0131C5Hk3yw8oKZKkUT1KpFq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`.claude/` 를 통째로 무시하면 성격이 반대인 두 파일이 같이 묻힙니다.

    settings.json        git mv / gh pr / git diff — 머신 독립적, 공유 대상
    settings.local.json  절대경로 + PowerShell 하드코딩 — 다른 머신에서 무의미

앞으로 `.claude/agents/`, `.claude/commands/` 를 만들어도 같이 사라질
참이었습니다. CLAUDE.md 를 이만큼 관리하면서 AI 작업 규칙의 나머지 절반을
추적하지 않는 것은 앞뒤가 맞지 않습니다.

추적으로 돌리면서 다시 매칭될 일이 없는 일회성 허용 규칙 2건을 뺐습니다.

- 저장소 이름 변경 때 한 번 쓴 `xargs -0 sed -i ...QuantumOmega...`
- `gh issue create --title 'fix(tests): 벤치마크 테스트가 시계 해상도...'`
  — 이슈 하나를 만들기 위한 제목 문자열이 통째로 영구 권한 규칙이었습니다.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#55 는 needs-decision 이슈였고 산출물은 결정입니다. 코드는 바꾸지 않았습니다.

결정: 변경 (live/paper). 근거는 `real` 이 벤더 표기가 아니라는 것과, 사용자가
사실상 0명인 지금이 가장 싸다는 것입니다.

착수 전 실측에서 세 가지가 나왔고 전부 후속 이슈의 범위를 바꿨습니다.

- 이슈가 든 근거 하나가 무너졌습니다. `Realtime` 개수(219/278)는 정확했지만
  충돌은 부분일치에서만 생깁니다 — 단어 경계로 재면 46건 중 realtime 은 1건.
  개수가 맞다고 그 개수가 뒷받침한다는 문장까지 맞는 것은 아닙니다.
- 이슈가 "가장 중요한 한 줄"로 꼽은 위험이 개명 이후가 아니라 **이미** 열려
  있었습니다. `cfg.get("virtual", False)` 는 기본값이 실전이라, 오타
  `virtaul: true` 가 지금도 조용히 실전 계좌로 붙습니다.
- 그 읽기 코드가 5벌 복붙돼 있고 4벌이 examples/ 입니다. helpers.py 만 고치면
  사용자가 복사해 가는 쪽은 그대로입니다.

그래서 순서를 뒤집었습니다. 개명은 곧 "옛 키"를 만드는 행위라 가드 없이
개명하면 개명 자체가 사고의 원인이 됩니다.

  #69  load_config 5벌 통합 + 미지의 키에 명시적 실패   (next-up)
  #70  real/virtual → live/paper 개명                  (blocked, 선행 #69)

#70 에는 YAML 스키마 변경도 넣었습니다. 프로필 이름과 플래그가 같은 사실을 두 번
적고 어긋났을 때의 정의가 없어서, 불리언 `virtual: true/false` 를
`mode: live|paper` enum 으로 바꿉니다. 불리언은 "없음"이 곧 False(실전)지만
enum 은 "없음"이 그냥 없음입니다.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`cfg.get("virtual", False)` 는 기본값이 실전이었습니다. `virtaul: true` 오타 하나로
모의투자 의도가 실전 주문이 됐고, 경고 한 줄 없었습니다.

    설정 파일이 말하는 것 : virtaul(오타) = True   -> 사용자 의도: 모의투자
    create_client 가 볼 값: virtual = False        -> 실전 계좌

같은 코드가 5벌이었고 4벌이 examples/ 였습니다. 예제는 사용자가 그대로 복사해
가는 코드라 helpers.py 만 고쳐서는 막히지 않습니다.

- `load_config` 를 1벌로. 예제 4개는 `from vmkis import load_config`
- `_validate_profile` 신설 — 모르는 키 / 필수 키 누락 / 판정 키 누락에 예외
- `create_client` 에서 `.get(..., False)` 제거. 기본값을 두지 않습니다
- 읽기와 쓰기가 `_MODE_KEY` 상수 하나를 공유. 한쪽만 바뀌어 어긋나던 것을 막습니다
- `load_config` 를 패키지 루트에 공개 (추가이므로 하위호환 유지)

## 테스트가 그 위험을 사양으로 못 박고 있었습니다

    tests/unit/test_helpers.py:133
        def test_virtual_key_defaults_to_false(...):
            """`virtual` 키가 없으면 실전으로 간주한다."""

막으려던 동작이 통과해야 할 사양으로 적혀 있었습니다. 뒤집었습니다.

`examples/01_basic/get_quote.py` 의 복사본을 importlib 로 끌어와 테스트하던
파일도 있었습니다 — 중복을 테스트가 고착시키고 있었습니다. 배포되는
config.example*.yaml 3개를 파싱하는 값어치는 남기고 대상만 라이브러리로 옮겨
test_config_examples.py 로 바꿨습니다.

## 되돌려 확인

`_validate_profile` 무력화 + `.get(..., False)` 복원 상태에서 7건이 실패하고,
복원 해제 후 1058건이 통과하는 것을 확인했습니다.

    pytest -m 'not requires_api'   1058 passed, 8 skipped
    coverage                       91.58% (게이트 90), helpers.py 100%
    ruff / lint-imports            통과

Closes #69

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(config): 설정 스키마 문서 신설 + #70 을 코드 개명으로 축소

사용자가 config.example*.yaml 3개를 스키마 전면 재설계로 고쳤습니다. 그것은 #70 이
아닙니다 — #70 은 코드 식별자 개명이고, 새 스키마는 라이브러리에 대응 개념이 없는
새 설정 계층입니다.

    base_url / ws_url   __env__.py:10-14 하드코딩 상수. 주입 지점 없음
    apps / accounts     KisAuth 는 필드 5개. 앱 개념 없음
    broker              브로커 개념 자체가 없음
    token_path          기본 ~/.vmkis/ + 해시 파일명

그래서 쪼갰습니다. 코드 개명은 어떤 설정 설계에서도 살아남지만, 스키마는 설계
확정이 필요합니다. 묶으면 스키마가 막힐 때마다 개명까지 멈춥니다.

## 새 스키마 — 3블록

운용 시스템 쪽 설정을 참고했으나 대부분 옮기지 않았습니다. 자금 배분 정책, 원장
표시용 이름표, 레거시 브리지용 이중 명명, 다중 브로커는 전부 이 라이브러리가
읽지 않는 값입니다. 이 저장소는 KIS 전용 API 클라이언트이지 운용 시스템이
아닙니다.

    version / apps / accounts / default_account

## 초안 결함 5건을 반영했습니다

- config.example.real.yaml 의 default_account 가 그 파일에 없는 계좌를 가리킴
  -> R5/R6 양방향 참조 검사
- VMKIS_PROFILE 이 가리킬 대상이 없음 -> 선택 축을 default_account 로 일원화
- token_path 기준 경로 미정의 -> 설정 파일 기준, 파일명은 앱 이름에서 파생
- user_agent 따옴표 이중 -> 필드 제거
- accounts 가 스칼라 키와 블록 혼재 -> default_account 를 최상위로

token_path 를 파생시킨 것이 설계 변경입니다. 초안은 "앱키별로 다르게 지정해야
한다"고 경고했는데, 사용자가 지켜야 하는 불변식은 사용자가 안 지킵니다. 두 앱이
같은 경로를 가리켜도 아무도 못 막고 증상은 "가끔 인증이 풀린다"로 나타납니다.

하위 호환은 넣지 않습니다(사용자 결정). 다만 옛 파일은 version 키가 없어 R1 에서
읽을 수 있는 메시지로 거부됩니다 — 조용히 오독되지 않습니다.

사용자 초안은 draft/config-schema-v2 에 보존했습니다. #70 이 virtual 을 369곳
건드리므로 확정되지 않은 스키마를 작업 트리에 두지 않습니다.

Refs #70, #75

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(config): user_agent·endpoints 되살리고 따옴표 규칙(R9) 추가

검토 중 세 건이 나왔습니다. 둘은 제가 잘못 잘라낸 것입니다.

## user_agent — 최상위로 되살림

초안의 broker_env_kis.etc.user_agent 를 블록째 지웠는데, 항목의 값어치를 따로
재지 않은 실수였습니다. 이미 있는 손잡이인데 하드코딩돼 있습니다 — __env__.py 의
USER_AGENT 가 kis.py 에서 세션 헤더에 들어갑니다. 초안 주석이 브라우저 UA 복사를
안내한 것은 실제 필요로 읽힙니다.

브로커 블록이 아니라 최상위입니다. 클라이언트 전체에 걸리는 값입니다.

## endpoints — 추가

"스테이징 서버가 없으니 쓸 사람이 없다"는 판단을 정정합니다. 사용 사례를 잘못
상정했습니다. 진짜 사례는 벤더가 주소를 바꿨을 때의 자력 복구입니다.

지금은 사용자가 손을 쓸 수 없습니다. from-import 가 값을 복사하므로 __env__ 를
고쳐도 소비 모듈은 옛 값을 봅니다.

    $ python -c "import vmkis.__env__ as env, vmkis.kis as k; \
                 env.REAL_DOMAIN='https://patched.example.com'; print(k.REAL_DOMAIN)"
    https://openapi.koreainvestment.com:9443

모듈마다 따로 패치해야 하는데 문서에 없고, 새 모듈이 그 상수를 import 하면 또
깨집니다. 남는 수단은 릴리스를 기다리는 것뿐이고 장중이면 그날은 끝입니다.

mode 로 키를 잡고 부분 지정을 허용합니다 — 벤더가 웹소켓 포트만 바꾸는 일이
흔합니다. 소비 지점 2곳이 이미 객체를 들고 있어 새 배선이 필요 없습니다.

## R9 — 따옴표 함정

    account_no: 00000000    ->  0      (int)     계좌번호가 사라집니다
    product_code: 01        ->  1      (int)
    mode: paper             ->  'paper' (str)    이건 안전합니다

mode 는 따옴표 유무가 같은 결과라 문서에서 빼고 적고 있었는데, 그걸 본 사용자가
"따옴표는 선택"으로 읽으면 account_no 에서 값이 조용히 0 이 됩니다. 안전한 값
하나를 따옴표 없이 적는 대가로 위험한 값에서 따옴표가 빠집니다.

예시를 전부 따옴표로 통일하고(version 만 예외 — 실제로 정수), 문자열 자리에
int/bool 이 오면 따옴표를 씌우라고 말해주며 거부하는 R9 을 넣었습니다.

Refs #75

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#76 에서 docs/guidelines/CONFIG_SCHEMA.md 를 추가하면서 INDEX.md 갱신을 빠뜨렸습니다.
INDEX 는 guidelines 를 개별 나열하는 표라, 파일만 늘고 목록이 그대로면 새 문서를
찾을 방법이 없습니다.

양방향으로 대조해 다른 누락·죽은 링크가 없는 것을 확인했습니다.

    INDEX 에만 있음 (죽은 링크): 없음
    파일만 있음 (미등재)      : 없음

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
설정 파일을 새 스키마로 갈아엎습니다. 하위 호환은 없습니다.

    version / apps / accounts / default_account

apps 를 계좌와 분리하는 근거는 토큰 수명 하나입니다. KIS 토큰은 app_key 단위로
발급되므로 같은 앱키를 쓰는 계좌 N개가 토큰 1개를 공유합니다. 그것이 KIS 의 실제
제약이라 라이브러리가 알아야 합니다.

## 초안에서 고친 것

- default_account 가 그 파일에 없는 계좌를 가리킴  -> R5/R6 양방향 참조 검사
- token_path 기준 경로 미정의                      -> 설정 파일 기준, 앱 이름에서 파생
- accounts 가 스칼라 키와 블록 혼재                 -> default_account 를 최상위로
- user_agent 따옴표 이중                            -> 값 한 겹으로

token_path 를 파생시킨 것이 설계 변경입니다. 초안은 "앱키별로 다르게 지정해야
한다"고 경고했는데, 사용자가 지켜야 하는 불변식은 사용자가 안 지킵니다. 두 앱이
같은 파일을 가리켜도 아무도 못 막고 증상은 "가끔 인증이 풀린다"로 나타납니다.

## R9 — YAML 의 함정

    account_no: 00000000   ->  0  (int)
    product_code: 01       ->  1  (int)

결함을 되살린 상태에서 KisAuth 가 받는 계좌가 '0-1' 이 되는 것을 확인했습니다.
사용자의 오타가 아니라 형식의 함정이라, 오류 메시지가 따옴표를 씌우라고 말합니다.

## 템플릿 위치

configs/ 안에 둡니다. 토큰 폴더가 설정 파일 기준이라, 템플릿이 저장소 루트에
있으면 제자리에서 채웠을 때 토큰이 루트에 떨어지고 그건 .gitignore 에 없습니다.

.gitignore 가 `configs/` 가 아니라 `configs/*` 인 이유도 실측했습니다 —
디렉터리째 제외하면 git 이 안으로 내려가지 않아 `!` 예외가 통하지 않습니다.

## user_agent / endpoints 배선

파싱만 하고 안 읽는 키를 내보내지 않기 위해 VmKis 까지 배선했습니다. endpoints 는
벤더가 주소를 바꿨을 때의 탈출구입니다 — from-import 가 값을 복사하므로 사용자가
__env__ 를 고쳐도 소용이 없고, 지금은 릴리스를 기다리는 수밖에 없습니다.

## 함께 고친 것

영문 문서가 한 번도 맞은 적이 없는 API 를 적고 있었습니다. load_config 가
{'kis': ...} 를 준 적이 없는데 VmKis(**config['kis']) 라고 적혀 있었고,
VmKis(app_key=, app_secret=, account_number=, server=) 는 네 인자 모두
존재하지 않습니다. 설정에 직결된 곳만 정정하고 나머지는 #78 로 남겼습니다.

## 되돌려 확인

R2·R6·R9 무력화 시 7건 실패, 복원 후 전체 통과를 확인했습니다.

    pytest -m 'not requires_api'   1088 passed, 8 skipped
    coverage                       91.79% (게이트 90), config.py 100%
    ruff / lint-imports            통과

Closes #75

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
개별 일지(05~08)의 요약이 아니라 반복해서 드러난 것을 적었습니다.

## 수치는 맞는데 그 수치가 뒷받침한다는 문장이 틀렸다 (4회)

Realtime 219곳은 정확했지만 "충돌한다"는 단어 경계로 재면 46건 중 1건이었습니다.
dotenv 가 dependencies 에 있는 것도 사실이지만 src 사용은 0건이었습니다. 영문
문서의 예제는 그럴듯했지만 한 번도 실행된 적이 없었습니다. .gitignore 의
`configs/` + 예외는 그럴듯했지만 실측하면 무효였습니다.

근거로 인용된 수치를 재는 것과, 그 수치가 주장을 뒷받침하는지 재는 것은 다른
작업입니다.

## 조용한 실패가 이 저장소의 지배적 결함 유형이다 (오늘만 5건)

전부 같은 구조입니다 — 기본값이 있거나, 없는 것을 없다고 말하지 않습니다.
오늘 넣은 R1~R9 가 전부 "모르면 거부하고 왜인지 말한다" 인 것도,
mode 를 불리언이 아니라 enum 으로 정한 것도 같은 이유입니다.

## 테스트가 결함을 사양으로 고정하고 있었다 (2건)

둘 다 구현을 끝낸 뒤에야 드러났습니다. 놓친 이유가 같습니다 — 호출하는 줄만
grep 하고 단언을 읽지 않았습니다. 호출 지점 목록은 영향 범위이지 사양이 아닙니다.

## 내 판단 오류 네 건이 전부 같은 형태였다

dotenv·user_agent·endpoints·템플릿 위치. 넷 다 컨테이너를 보고 내용물을 판단한
것이고, 넷 다 사용자 질문으로 드러났습니다. 스스로 잡은 것이 없습니다.

덜어내기로 결정한 묶음은 항목을 세로로 적고 각각에 "이걸 빼면 무엇이 안 되는가"를
답하기로 합니다.

## 되돌려 확인이 두 번 다 값을 했다

두 번 다 "테스트가 실패한다"보다 "그때 실제로 무슨 값이 들어가는가"를 찍은 것이
PR 본문의 핵심이 됐습니다.

next-up 을 #70 / #72 / #73 세 건으로 재배치했습니다. 미결 논의는 없습니다.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`vmkis/__init__.py` 가 `ImportError` 를 삼켜 `create_client`,
`save_config_interactive`, `SimpleKIS` 를 `None` 으로 만들고 있었습니다.
폴백이 걸리면 사용자가 받는 것은 호출 지점의

    TypeError: 'NoneType' object is not callable

이고, 원인 모듈 이름이 어디에도 나오지 않습니다. 예제 9개가
`from vmkis import create_client` 로 시작하므로 그 비용은 가장 디버깅을
못 하는 사용자에게 갑니다.

`SimpleKIS` 쪽도 함께 지웠습니다. `simple.py` 의 import 는
`from vmkis.kis import VmKis` 뿐인데 `__init__.py` 가 그 위에서 이미
무조건 같은 import 를 하므로, 그 `except` 가 잡을 수 있는 것은
`simple.py` 자신의 버그뿐이었습니다 — 결함 은닉 기능만 남은 코드입니다.

회귀 테스트는 `sys.modules[name] = None` 로 하위 프로세스에서 고장을
흉내냅니다. 같은 프로세스에서는 `vmkis` 가 이미 캐시돼 `__init__.py` 가
재실행되지 않아 아무것도 검사하지 못합니다.

폴백을 지우자 import 블록이 하나로 합쳐져 정렬이 `# 핵심 인증/클래스`
그룹을 쪼갰습니다. `# isort: split` 으로 의미 단위를 유지합니다.

`pyproject.toml` 의 pyyaml 필수 사유가 "없으면 __init__.py 가 삼켜서
조용히 None 이 된다"였습니다 — 나쁜 실패 모드를 덮으려고 의존성을 고정한
것입니다. 실제 근거로 교체했습니다.

결함을 되살려 확인: `try/except` 를 되돌리면 회귀 2건이
`SWALLOWED ... None` 로 실패합니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`python-dotenv` 가 `[project] dependencies` 에 있는데 `src/` 가 한 줄도 쓰지
않았습니다. `cd66359` 가 테스트를 목적으로 런타임과 poetry dev 그룹 양쪽에
넣었고, `eb7ab9a`(Poetry → uv)가 dev 그룹만 걷어내면서 런타임 항목이
살아남은 것입니다.

`load_dotenv()` 는 프로세스 전역 `os.environ` 을 변형합니다. `import vmkis`
만으로 호스트 애플리케이션의 환경이 바뀔지는 라이브러리가 아니라
애플리케이션이 정할 일입니다.

문서를 함께 고칩니다. 이슈 본문은 USER_GUIDE 하나만 지목했지만 grep 을 다시
돌리니 두 곳이 더 나왔습니다.

  - docs/user/USER_GUIDE.md      `pip install python-dotenv` 안내 추가
  - docs/developer/DEVELOPER_GUIDE.md  같은 안내 (본문 누락분)
  - docs/architecture/ARCHITECTURE.md  런타임 트리 → 개발 의존성.
    같은 블록에 pyyaml 이 원래 빠져 있어 함께 채웠습니다
  - docs/FAQ.md 는 requirements.txt 예시에 이미 명시적으로 적고 있어 조치 없음

`tests/env.py` 의 `try/except ImportError: pass` 도 지웠습니다. dotenv 가
test 그룹의 선언된 의존성이 되면 pytest 자신이 같은 그룹에 있으므로 폴백이
걸릴 수 있는 경우가 없습니다. 걸렸다면 skip 메시지가 ".env 를 만들어
채우세요" 라고 시키는데 .env 를 만들어도 아무 일이 없었을 것입니다.

검증 — 빈 venv 에 휠만 설치:

    Requires-Dist 7건, python-dotenv 없음
    import vmkis 성공, dotenv 설치됨: False
    from dotenv import load_dotenv → ModuleNotFoundError

마지막 줄이 USER_GUIDE 스니펫의 첫 줄입니다. 문서 갱신 없이 의존성만 빼면
사용자가 정확히 이 오류를 받습니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#55 의 결정을 코드에 반영합니다. `real` 은 한국투자증권의 표기가 아닙니다 —
KIS 는 실전/모의라고 쓰고 real/virtual 은 이 라이브러리가 고른 번역이었습니다.
영어권 표준은 paper trading 이고 영어 문서가 이미 그 말을 쓰고 있었습니다.

BREAKING CHANGE: 별칭도 deprecation 경고도 남기지 않았습니다. 옛 이름은
AttributeError 또는 TypeError 로 즉시 실패합니다. 0.0.1 이 2026-08-28 첫
배포라 지금이 가장 싼 시점이고, 호환 폴백을 지우려고 열려 있는 이슈
(#33·#34)에 한 줄을 더하지 않기 위해서입니다. 마이그레이션 표는 CHANGELOG.

  KisAuth(virtual=)              -> KisAuth(paper=)
  VmKis(virtual_auth=, ...)      -> VmKis(paper_auth=, ...)
  kis.virtual / kis.virtual_appkey -> kis.paper / kis.paper_appkey
  domain="real" | "virtual"      -> domain="live" | "paper"
  KisEndpoint(tr_real=, tr_virtual=) -> KisEndpoint(tr_live=, tr_paper=)
  __env__.REAL_DOMAIN 등 상수 6개 -> LIVE_/PAPER_
  VMKIS_VIRTUAL_* (테스트 환경변수) -> VMKIS_PAPER_*

이슈가 남겨 둔 결정 — tr_real/tr_virtual 도 바꿉니다. 반대 논거였던 "KIS
문서와 코드 사이에 번역층이 생긴다"가 성립하지 않습니다. 번역층은 이미
있었습니다. 게다가 tr_* 는 도메인 리터럴과 같은 파일, 같은 함수(resolve())에
있어 따로 둘 경계가 없습니다. 근거는 이슈 본문에 적었습니다.

치환은 규칙을 두 벌로 나눴습니다. 식별자 규칙 21개는 코드와 문서 모두에,
맨몸 \breal\b / \bvirtual\b 은 코드에만 먹였습니다 — 영어 문서에 그대로
먹이면 "No real money is involved" 가 "No live money" 가 됩니다.

한글 조사가 단어 경계를 없앱니다. re 의 \w 는 유니코드 문자를 단어 문자로
보므로 "virtual_auth에는" 이 치환에서 빠졌습니다. 치환 후 다시 grep 해서
3건을 손으로 고쳤습니다.

#75 가 예고한 대로 helpers 의 번역표가 사라졌습니다 — _MODE_TO_DOMAIN 이
항등 사상이 되어 _to_endpoints() 와 함께 지웠습니다.

docs/user/USER_GUIDE.md 의 모의투자 절은 이름만 바꾸지 않고 고쳤습니다.
`kis.virtual = True  # 또는 kis.virtual_account()` 는 읽기 전용 프로퍼티에
대입하는 코드라 원래부터 AttributeError 였습니다. 이름만 바꾸면 버그를
세탁하게 됩니다.

검증: VmKis.virtual 이 사라졌고 KisAuth(virtual=) 가 TypeError 입니다.
테스트를 같은 스크립트로 함께 개명했으므로 통과만으로는 개명 여부를 알 수
없어, 별도 스모크로 확인했습니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(examples): 예제 7개의 create_client(profile=) 를 account= 로 (#84)

create_client 의 인자는 account 입니다. profile 은 없습니다. #75 가 그 개명을
하면서 examples/01_basic/ 3개만 고치고 7개를 놓쳤습니다. 지금 실행하면

    TypeError: create_client() got an unexpected keyword argument 'profile'

파일마다 네 군데를 고쳤습니다 — 매개변수, create_client 호출, --profile
argparse 인자, 그리고 전달부.

문서도 함께 고쳤습니다. examples/*/README.md 가 VMKIS_PROFILE 과
--profile virtual 을 안내하고 있었는데 둘 다 없는 것입니다. 환경변수는
VMKIS_ACCOUNT 이고, 값은 real/virtual 이 아니라 설정 accounts: 아래의 키
이름입니다. `virtual: true` 는 앱의 `mode: "paper"` 가 됐습니다.

재발 방지가 이 작업의 본체입니다. test_examples_run_smoke.py 가 이미
있었지만 (1) CI 가 RUN_INTEGRATION 을 주지 않아 통째로 skip 이고 (2) 예제를
실제로 실행하므로 자격증명이 필요합니다. 그런데 이 결함은 둘 다 필요
없습니다 — create_client 는 호출되는 순간 죽습니다.

--help 로 돌리는 방법은 답이 아닙니다. argparse 가 create_client 보다 먼저
SystemExit 하므로 반환코드 0 을 보고 통과시키면 아무것도 검사하지 않는
초록불이 됩니다.

그래서 AST 로 호출부의 인자를 inspect.signature 와 대조합니다. 단위
테스트라 CI 가 항상 돌리고, 시그니처를 코드에서 읽으므로 다음 개명에도
따라옵니다.

검사기가 아무것도 못 보는 상태도 따로 막았습니다 — 경로가 틀려도, 예제가
create_client 를 그만 써도 위반은 0건이라 조용히 통과하기 때문입니다.

회귀는 두 겹으로 확인했습니다. 실제 예제에 결함을 되살려 실패를 봤고,
결함 문자열을 테스트 안에 박아 검사기 자체의 성능도 검증합니다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

* docs(prompts): #84·#78 프롬프트 문서에 결과 기록

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
문서가 존재하지 않는 시그니처와 삭제된 이름을 적고 있었습니다. 이슈 본문이
3곳을 지목했지만, 검사기를 먼저 만들자 7곳이 더 나왔습니다.

    docs/FAQ.md:54    VmKis(paper=True)          VmKis 에 paper 인자 없음
    docs/FAQ.md:45    VMKIS_REAL_TRADING          그런 환경변수 없음
    docs/FAQ.md:385   from vmkis import setLevel  루트에 없음
    DEVELOPER_GUIDE:597  responses.types.KisQuote 없음
    REGIONAL_GUIDES:304  from vmkis.mock import ... 모듈 자체가 없음
    docs/rules/TEST_RULES...:8  KisAuth(virtual=)   #70 에서 빠뜨린 디렉터리
    CONTRIBUTING.md:231  vmkis.types.Quote        public_types 입니다

FAQ.md:54 는 #70 에서 제가 더 나쁘게 만든 자리입니다. virtual= -> paper= 로
일괄 치환했는데 그 줄이 VmKis(...) 안이었습니다. 틀린 이름을 다른 틀린
이름으로 바꾼 것입니다.

REGIONAL_GUIDES 의 "글로벌" 절은 시그니처 문제가 아니었습니다. server: mock
설정, mock: 블록, vmkis.mock.MockKisClient — 기능 하나가 통째로 허구였고
존재한 적이 없습니다. 고칠 이름이 없으므로 허구를 지우고 실제로 되는 것을
적었습니다 (requests_mock, 또는 모의투자 계좌).

완료 기준 2번(검사 방법)에 대한 답이 tests/unit/test_docs_signatures.py
입니다. 마크다운 코드펜스와 노트북 셀을 파싱해 (1) vmkis 모듈에서 import 하는
이름이 실재하는지 (2) 공개 진입점 호출의 키워드 인자가 시그니처와 맞는지 봅니다.

"모듈이 없다"는 실패로 만들지 않았습니다. DEVELOPER_GUIDE 의 확장 가이드가
vmkis.api.my_api 같은 자리표시자를 일부러 씁니다 — 확인할 수 없는 것과 틀린
것은 다릅니다. 대신 vmkis.mock 은 손으로 지웠습니다. 자동 검사가 모든 것을
대신하지는 않습니다.

파싱 안 되는 블록도 통과시킵니다. 문서에는 ... 나 발췌가 섞이고, SyntaxError
를 실패로 만들면 문서 쓰는 사람이 검사를 꺼 버립니다.

작업 중 코드가 틀린 경우를 만나 #87 로 열었습니다 — create_client 가 모의
계좌에서 항상 실패하고, 템플릿의 기본 계좌가 모의입니다. 문서에는 지금 되는
형태만 적고 안 되는 형태를 각주로 달았습니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
needs-decision 이슈입니다. 코드가 아니라 판단이 산출물입니다.

(A) Protocol 판정 기준을 ARCHITECTURE.md 확장성 절에 넣었습니다. 이 절이
없던 동안 모든 신규 엔드포인트가 Protocol 을 요구받는 것처럼 보였습니다.

src/vmkis 의 Protocol 53개를 전수 분류하니 역할이 셋뿐이었습니다.

  T1 시장 통합       호출자가 국내·아시아·미국을 하나의 이름으로 받는가
  T2 공개 반환 타입   public_types/types 로 내보내며 구체 클래스를 감추는가
  T3 믹스인 self 타입 믹스인이 self 에 무엇이 있다고 가정하는지 선언

이슈가 제안한 기준("국내/해외 통합이 있을 때만")만으로는 53개가 설명되지
않습니다. KisStockInfo 는 구현이 _KisStockInfo 하나뿐인데 Protocol 입니다 —
구체 클래스를 비공개로 두고 Protocol 만 공개하기 때문입니다(T2). 구현 개수가
아니라 공개 여부가 기준입니다.

전수 확인 결과 불필요하게 Protocol 을 쓴 사례는 없었습니다. 53개 전부
T1/T2/T3 에 들어갑니다. 기대했던 "지울 것"은 안 나왔지만 그것도 결과입니다.

(B) overload -> dict 레지스트리 교체는 기각합니다. 이슈가 "타입 검사기가
dict 분기의 반환 타입을 좁힐 수 있는가, 못 하면 하지 않는 편이 낫다"를
선결 조건으로 남겨 두었습니다. pyright(VS Code 의 Pylance 엔진)로 쟀습니다.

  @overload         c.on("price") -> Ticket[Price]                     좁혀짐
  dict 레지스트리    r.on("price") -> Ticket[Price] | Ticket[Orderbook]  못 좁힘
  @overload + dict  h.on("price") -> Ticket[Price]                     좁혀짐

파이썬 타입 시스템에 키에 따라 반환 타입이 달라지는 매핑을 표현할 방법이
없습니다.

이슈에 없던 세 번째 변형(overload 는 남기고 런타임 분기만 dict)도 재 봤습니다.
좁힘은 지켜지지만 price.py 331줄 중 @overload 스텁이 170줄(51%)이라, 절감이
~20줄에 그치면서 간접 참조만 늘어납니다. 즉 (B)는 어느 형태로도 줄이려던
것을 줄이지 못합니다.

보일러플레이트를 줄이려면 손으로 덜 쓰는 쪽이 아니라 생성하는 쪽(#21
codegen)이 남은 선택지입니다.

동작 변경 금지 항목대로 src/ 는 한 줄도 바꾸지 않았습니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
이 이슈의 근거가 저장소에 없었습니다. "AST 파서 400줄은 프로토타입 완성
상태"라며 파싱률 98.9% 를 근거로 삼는데 그 파서가 커밋된 적이 없습니다.
중단 조건이 "파싱률이 급락하면"인데 잴 도구가 없었습니다. 파서를 다시 만드는
것이 첫 작업이 된 이유입니다.

수치를 다시 쟀고, 이슈와 달랐습니다.

                        이슈(08-27)          실측(08-30)
  REST                  274                  272   (이슈는 auth 2개를 REST 로 셈)
  웹소켓                 —                    60    (본문에 분류가 없었습니다)
  REST 완전 파싱         271/274 = 98.9%      272/272 = 100%
  응답 필드              7,979 (유니크 2,801)  5,485 (유니크 2,499)
  접미사 타입 커버리지    54%                  59.9%
  POST(주문)             18                   18
  이름 충돌              —                    30종
  NUMERIC_COLUMNS       파일럿 검증 항목       쓸 수 없음

파싱률 81.9% 로 시작했는데 실패 60건이 전부 "API_URL 없음"이었습니다. 열어
보니 실패가 아니라 웹소켓 구독 함수였습니다 — API_URL 이 없는 것이 정상입니다.
분류를 넣자 REST 100% 가 됐습니다.

이슈에 없던 사실 둘을 찾았습니다.

1. 이름이 유일하지 않습니다. 332개 중 30종이 카테고리 간 충돌입니다
   (inquire_price 는 5곳). 이름으로 키를 잡으면 9%를 조용히 잃습니다 — 제
   생성기가 그랬고, 이슈가 최난도로 지목한 inquire_daily_ccld 를 생성하니
   해외선물 것이 나왔습니다. category/name 으로 키를 바꾸고 모호하면 멈춥니다.

2. NUMERIC_COLUMNS 는 타입 판정 근거가 못 됩니다. 272개 중 194개가 비어
   있고, 71개 필드가 엔드포인트마다 숫자였다 아니었다 합니다.

접미사 표는 유니크 필드 2,499개의 분포를 실측해 32개까지 채웠습니다. 남은
40%는 KisString 으로 둡니다 — 어떤 문자열도 받으므로 런타임 오류가 나지
않습니다. 커버리지는 편의의 문제이지 정확성의 문제가 아닙니다.

법적 경계는 사람이 아니라 기계가 지킵니다. 원본에 LICENSE 가 없으므로 사실만
옮기는데, "docstring verbatim 복사 금지"는 300개 규모에서 눈으로 지켜지지
않습니다. 추출기가 원문 설명을 애초에 담지 않고, 테스트가 생성물에 원본
런타임 어휘와 출력 문구가 섞였는지 검사합니다. 원본에서 실제로 한 줄을
가져와 붙여 검사기가 잡는 것을 확인했습니다.

생성물 8개는 scripts/codegen/pilot/ 에 두고 패키지에 넣지 않았습니다(휠에
포함되지 않는 것을 확인). 파일럿의 목적은 생성기 검증이지 엔드포인트 출시가
아니고, output 리스트/단건 판정·파라미터 검증·scope 바인딩·필드명 번역·
tr_id 분기 조건 다섯 가지가 여전히 사람 몫입니다.

#45 의 Protocol 판정표가 여기서 값을 냈습니다. 파일럿 8개는 전부 단일 시장
비공개 타입이라 Protocol 이 필요 없고, 생성기가 만들지 않아도 되는 근거가
문서에 있습니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
create_client 가 모의 계좌에서 항상 죽었습니다.

    ValueError: id를 입력해야 합니다.

그리고 템플릿 설정의 기본 계좌가 모의라, 문서가 안내하는 정규 경로
(cp 템플릿 -> 채우기 -> create_client())가 끝까지 가지 않았습니다.

이슈가 남긴 선택지 셋 중 무엇을 고를지는 세는 순간 정해졌습니다.
KisEndpoint 21개 중 13개가 tr_paper 가 없습니다 — 시세·차트·상품정보
계열입니다. client/endpoint.py 가 그 성질을 이미 적어 두었습니다:
"모의 계좌로 호출해도 실전 도메인으로 보냅니다".

즉 모의 클라이언트도 실전 앱키와 실전 토큰이 필요합니다. "모의 전용
클라이언트를 인정한다"는 방향은 kis.stock().quote() 에서 죽는 클라이언트를
만들어 냅니다 — 생성은 되고 나중에 터지는, #73 에서 없앤 그 실패 모드입니다.

그래서 create_client 가 설정에서 실전 계좌를 찾아 함께 넘기고, 없으면
무엇을 추가해야 하는지 말하고 멈춥니다. 생성자 쪽에도 전용 검사를 넣어
"id를 입력해야 합니다" 대신 원인을 말하게 했습니다 — 사용자는 id 를
빠뜨린 적이 없습니다.

템플릿의 실전 앱 주석을 풀었습니다. 코드만 고치면 정규 경로는 여전히
막힙니다. 이슈 제목이 "템플릿 기본값이 그것입니다"인 이유입니다.

이 버그가 산 이유는 테스트가 박제하고 있었기 때문입니다. DummyVmKis 가
생성자를 통째로 대체하고 `assert args[0] is None` 을 단언했는데, 그것이
진짜 생성자가 거부하는 바로 그 형태입니다. 대역은 무엇이든 받으므로
테스트는 초록이고 사용자는 ValueError 를 받았습니다. 모킹 없이 끝까지
만드는 테스트를 추가했습니다.

test_template_defaults_to_paper 도 다시 썼습니다. 문자열 비교라 앱이
둘이 되자 깨졌는데, 지키려던 성질은 "mode 가 paper 하나뿐"이 아니라
"실수로 실전에 붙지 않는다"였습니다. load_kis_config(TEMPLATE).account()
.is_paper 로 바꿨습니다. 문자열을 비교하는 테스트는 의도가 아니라 표기를
지킵니다.

유래: VmKis(None, auth) 는 06a63f2(python-kis -> vmkis 개명)에서 그대로
들어왔고 이 저장소에서 동작한 적이 없습니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
승격만 하면 될 줄 알았는데 아니었습니다. git log v0.0.1..HEAD 를 CHANGELOG
와 대조하니 가장 큰 Breaking 두 건이 [미출시] 에 없었습니다.

  #75/#79  설정 파일 3블록 스키마 — 하위 호환 없음
  #69/#74  helpers.load_config 제거

둘 다 사용자 설정 파일과 import 를 깨는 변경입니다. 이것 없이 0.1.0 을
내면 사용자는 무엇이 왜 깨졌는지 알 수 없습니다.

여기에 #87(모의 전용 설정 무효화), #73(조용한 None 폴백), #37(무한 재시도
상한)도 빠져 있었습니다. 총 5건을 채웠습니다.

#82 에서 "릴리스 때 여러 PR 을 훑어 다시 찾아낼 보장이 없다"며 CHANGELOG 를
그 자리에서 적었는데, 그 우려가 사실이었음이 여기서 확인됐습니다.

그래서 빈 [미출시] 절을 남겨 뒀습니다. 다음 변경이 갈 자리가 없으면 또
커밋을 훑게 되고, 훑으면 또 빠집니다.

링크 각주는 이 파일에 없습니다 — 버전 헤더가 링크가 아닙니다. #85 의 완료
기준에 있어 확인만 하고 넘어갑니다.

태그와 PyPI 업로드는 이 PR 에 없습니다. 태그 push 가 publish.yml 을 돌려
PyPI 에 올리고 PyPI 는 같은 버전을 다시 올릴 수 없습니다 — 되돌릴 수 없는
외부 공개입니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
첫 판 생성기가 `output` 하나만 가정해서 블록 102개를 조용히 버리고
있었습니다. 실측하면 REST 272개 중 94개가 블록 2개 이상입니다.

  블록 1개 177개 · 2개 87개 · 3개 6개 · 4개 1개

inquire_daily_ccld 가 128줄 -> 219줄이 된 것이 그 차이입니다. 늘어난
91줄이 output2(체결 요약)이고, 첫 판은 체결 목록만 만들고 요약을
버렸습니다. 그런데 테스트 36건은 전부 통과했습니다 — 없는 것을 세는
검사가 없으면 없어진 줄 모릅니다.

리스트/단건은 샘플의 pd.DataFrame 인자 모양으로 판정합니다.

  pd.DataFrame(x)    -> list
  pd.DataFrame([x])  -> single

결과는 list 43.7% · single 12.6% · unknown 43.7% 입니다. unknown 을
추측으로 채우지 않았습니다. 그쪽 샘플은 isinstance(x, list) 로 방어하는데,
이는 "KIS 가 dict 를 준다"는 증거가 아니라 "원본 생성기도 몰랐다"는
증거입니다. 샘플이 답을 갖고 있지 않으니 우리도 알 수 없습니다.

틀리면 런타임에 터집니다 — KisList.transform 이 dict 를 받으면 TypeError
를 냅니다. 그래서 unknown 은 나중에 다듬을 것이 아니라 실제 위험이고,
생성물에 경고 주석을 박아 사람에게 넘깁니다. KisList 를 관대하게 고치는
방법도 있지만 근거가 없어 하지 않았습니다 — 근거 없이 관대하게 만들면
진짜 오류를 삼킵니다.

페이지네이션은 같은 AST 순회에서 함께 나왔습니다. ctx_area_[fn]k<폭> 이
KisEndpoint.page_size 가 받는 값입니다. 40개 엔드포인트에서 얻었고 폭
분포는 200(25) · 100(14) · 50(1) 입니다.

한계도 분명해졌습니다. COLUMN_MAPPING 이 블록을 나누지 않아 다중 블록
94개는 같은 필드 집합을 공유한 채 사람이 갈라야 합니다. 필드 이름만으로
소속 블록을 알 방법이 없습니다.

회귀 4건 중 둘은 "없는데 아무 값이나 넣는 것"과 "추측으로 채우는 것"을
봅니다. 앞의 둘만 있으면 무조건 200 을 넣는 구현도 통과합니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
TR ID 를 그것이 선택되는 조건과 함께 추출합니다. 첫 판은 조건을 버리고 TR
만 모은 뒤 첫 번째만 쓰고 나머지를 주석으로 흘렸습니다 — 43개가 그렇게
흘러가고 있었습니다.

설계는 새로 하지 않았습니다. client/endpoint.py 의 docstring 이 이미
답입니다: "실전/모의 차원만 떼어내 KisEndpoint 로 옮기면 나머지 차원은
그대로 dict[key, KisEndpoint] 로 남습니다". #43 이 손으로 하던 것을 기계가
하게 했습니다.

#21 이 최난도로 지목한 inquire_daily_ccld 의 4-way 행렬이 2×2 로 접힙니다.

    INQUIRE_DAILY_CCLD_ENDPOINTS = {
        "before": KisEndpoint(tr_live="CTSC9215R", tr_paper="VTSC9215R", ...),
        "inner":  KisEndpoint(tr_live="TTTC0081R", tr_paper="VTTC0081R", ...),
    }

축 분포를 먼저 쟀습니다. env_dv 94 회로 압도적이고 나머지(ord_dv 29,
ovrs_excg_cd 8, pd_dv 4 ...)가 업무 축입니다. 그래서 도메인 축을 상수
하나로 두어도 안전합니다 — 재지 않았으면 쓰이지도 않을 일반화에 시간을
썼을 것입니다.

ast 에 부모 링크가 없어 조건 스택을 들고 하향식으로 걷습니다. elif 가
orelse 안의 If 로 표현되는 것도 함께 다룹니다.

순수 else 가지는 "위 조건들이 전부 아니다"라 하나의 값으로 적을 수
없습니다. 2분기면 반대값으로 채울 수 있지만 3분기 이상에서 틀립니다.
{axis: None} 로 두고 생성물에 경고를 답니다.

생성물이 dict 가 되자 기존 검사 3건이 깨졌습니다 — vars(module) 의 최상위
값만 보고 dict 안을 안 봤기 때문입니다. 고치지 않았다면 분기가 있는
엔드포인트는 "KisEndpoint 0개"로 보여 검사가 조용히 통과했을 것입니다.

"무조건 dict 로 감싸는" 구현을 막는 반대편 검사도 함께 넣었습니다.

이것으로 원본에 정보가 있던 3가지(응답 블록·페이지네이션·TR 분기)를 전부
가져왔습니다. 남은 셋은 원본에 없거나 설계 판단입니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
"#21 이 종료 조건을 만족하는가"를 확인하려고 파일럿 8개를 본문이 지정한
검증 목적 대비로 훑었더니 하나가 비어 있었습니다.

    chk_holiday  page_size=None   <- "평문 CTX_AREA_FK 페이지네이션 (#16)"

원인이 둘이었습니다.

1. 정규식이 `ctx_area_[fn]k(\d+)` 로 폭 숫자를 요구했습니다. KIS 커서 네
   변형 중 하나가 접미사 없는 CTX_AREA_FK 인데 통째로 건너뛰었습니다.
   하필 그것이 #21 이 파일럿 항목으로 명시한 엔드포인트입니다.

2. 고친 뒤에도 생성기가 `if spec.get("page_size"):` 였습니다. 0 은 falsy
   인데 유효한 값입니다 — NO_SUFFIX, #16 이 KisPage 에 도입한 표현입니다.
   None(페이징 없음)과 0(폭 모르는 평문 커서)은 다릅니다.

고친 뒤 분포가 #16 의 전수 조사와 정확히 일치했습니다.

              #16    이번 실측
    FK100      15        15
    FK200      25        25
    FK(평문)    2         2
    FK50        1         1

독립적으로 같은 수가 나왔습니다. 추출기가 옳게 세고 있다는 가장 강한
증거입니다.

회귀 하나가 두 원인을 다 잡습니다. 양쪽을 따로 되살려 확인했고, 0 == False
라서 값 비교만으로는 부족해 is not None 을 따로 단언합니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#36 착수 지시를 받았지만 그 이슈는 열 수 없었습니다. 세 항목이 전부
"1.0.0 예정"을 "완료"로 바꾸는 일이라, 지금 하면 일어나지 않은 릴리스를
일어났다고 적는 문서가 됩니다. API_STABILITY_POLICY.md 스스로 "1.0.0
이후에 지원 기간 정책을 정의합니다. 그전에 약속하면 지킬 수 없는 약속이
됩니다"라고 적고 있었습니다.

막고 있는 #30 을 봤더니 선행 조건이 판정 불가였습니다.

    - [ ] 0.0.x 가 실사용자에게 충분히 노출되었는가   <- "충분히"의 기준 없음
    - [ ] DeprecationWarning 이 사용자에게 도달했는가  <- 관측 수단 없음

체크박스가 있다고 판정 가능한 것이 아닙니다. 그래서 서브이슈 4건이 매달린
채 멈춰 있었습니다.

실측하니 답이 이미 정해져 있었습니다.

    0.0.1 게시 2026-08-28T04:05
    0.1.0 게시 2026-08-29T16:21   -> 0.0.x 수명 약 36시간
    다운로드 111건, last_day == last_week == last_month == 111

세 수치가 같다는 것은 전부 최근 하루 안이라는 뜻이고, 사람의 사용 곡선이
아니라 미러/봇 패턴입니다. 폴백 4종이 경고를 내는 것은 확인했지만 사람이
그 경고를 본 적이 있는지는 알 수 없습니다. 아무도 안 쓴 폴백을 제거하는
것은 마이그레이션 기간을 준 것이 아닙니다.

사용자가 "측정 가능한 게이트로 교체"를 골랐습니다.

    - [ ] 0.1.x 가 90일 이상 게시  -> 2026-11-27
    - [ ] 외부 사용 신호 1건 이상

게이트는 이슈가 아니라 검사가 감시합니다. CLAUDE.md 가 정한 방식입니다 —
"외부 조건 감시는 검사로. 이슈로 만들면 영원히 안 닫히고, 문서에 적으면
아무도 안 봅니다". test_release_gate.py 가 2026-11-27 에 실패하면서 다음에
할 일을 메시지로 적습니다. 일부러 시한폭탄입니다.

게이트 자체의 오설정도 막았습니다. MIGRATION_WINDOW 를 0 으로 만들면 두
테스트가 함께 실패합니다 — 그건 감시가 아니라 사고입니다.

0.1.0 이 정책 문서를 낡게 만든 것도 정리했습니다. 게이트가 0.1.x 를
가리키는데 문서는 0.0.x 를 "현재"라고 적고 있었습니다(17곳). 0.0.x 자체는
"지난 판 (2026-08-28 ~ 08-29)"으로 남겼습니다 — 36시간이라는 사실이 이
판단의 근거이기 때문입니다.

#30 은 닫지 않았습니다. 닫는 조건이 "낼 시점을 정했다"인데 시점이 아니라
조건을 정했습니다. #33·#34·#35·#36 의 blocked 도 유지합니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
마크다운 172개 / 43,512줄을 전수 조사했습니다. 살아 있는 것은 32개 /
11,736줄이고 나머지 140개는 동결(개발 일지 46 · 프롬프트 41 · 보고서 28 ·
생성물 11 · archive 7)입니다.

동결분은 문제가 아닙니다. 문제는 살아 있다고 표시된 32개 안에 죽은 것이
섞여 있고 그것을 구분하는 장치가 없다는 것입니다.

가장 큰 것은 docs/README.md 입니다. 440줄짜리 "문서 인덱스"인데 작성일이
2024년 12월 10일이고, docs/INDEX.md 와 목적이 같습니다. 그 안에 CLAUDE.md
가 손으로 적지 말라고 명시한 것이 가득합니다 — "총 문서 6개", "테스트
커버리지 90%", "아키텍처 문서 (850줄)". 실제는 172개, 950줄입니다. 그리고
GitHub 이 docs/ 를 열면 이 파일을 먼저 렌더링합니다.

한/영 드리프트도 구조적입니다. 어제 #87 로 "모의 계좌도 실전 앱이 필요"를
한국어 3곳에 넣었는데 영문에는 안 들어갔습니다. 영문 QUICKSTART 를 따라간
사용자는 create_client() 에서 막힙니다.

docs/rules/ 는 INDEX 가 스스로 "옛"이라고 적으면서 docs/ 아래 살려 두고
있습니다. #70 개명 때 대상 목록에서 빠져 KisAuth(virtual=) 가 살아남은
곳입니다 — "옛 것"이 docs/ 에 있으면 스윕 대상인지 매번 판단해야 합니다.

권고는 전부 옮기기·지우기·한 줄 덧붙이기입니다. 문서를 새로 쓰지 않습니다
— 172개가 된 원인이 "필요해 보여서 하나 더 쓴 것"입니다.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
머지 14건 · 신설 이슈 12건 · 닫힌 이슈 12건 · 0.1.0 PyPI 배포.

CLAUDE.md 가 정한 대로 개별 일지의 요약이 아니라 반복해서 드러난 것을
적었습니다.

가장 크게 반복된 것은 "통과했다"가 "검사했다"가 아니었던 경우입니다 —
서로 다른 여섯 작업에서 같은 형태가 나왔습니다. 검사가 0건이거나(#73),
CI 가 skip 하거나(#84), 검사기 자신이 버그로 무력하거나(#78), 테스트
대역이 진짜 생성자를 가리거나(#87), 잃어버린 것을 세지 않거나(#21 2차),
검사가 새 자료구조 안을 못 보거나(#21 3차).

대응으로 검사기를 검사하는 테스트 9건을 넣었습니다. 그중 넷은 성격이
다릅니다 — "게으르게 만든 구현"을 잡습니다. page_size 를 무조건 200 으로
넣거나 모든 엔드포인트를 dict 로 감싸는 구현도 "올바른" 테스트는 통과하기
때문입니다.

두 번째는 판정할 수 없게 쓰인 것입니다. #30 의 "충분히 노출"은 기준이
없었고, #21 은 근거 수치를 낸 도구가 커밋되지 않았으며, #36 은 릴리스
전에는 쓸 문장이 없었습니다. 체크박스가 있다고 판정 가능한 것이 아닙니다.

세 번째는 한 곳만 고치고 나머지를 놓친 것 네 번입니다. 전부 대상 목록을
손으로 적어서 생겼습니다.

제가 만든 결함 네 건도 기록했습니다 — 일괄 치환이 만든 VmKis(paper=True),
ast.walk 의 lineno, 커서 정규식의 \d+ 와 falsy 0, 그리고 git branch -m 을
|| 폴백에 넣어 로컬 main 을 개명한 것.


Claude-Session: https://claude.ai/code/session_0173GGKC25BTgokFA2YSiHNq

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant