Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ CI는 Ruff, compile 검사, pytest/Testcontainers, coverage 게이트, PR 컨테
수행한다.

`app`의 **line과 branch coverage는 각각 80% 이상**이어야 하며 미달이면 `ai-ci / check`가
실패한다(`S15P11A705-110`에서 활성화). 판정은 `tools/check_coverage_gate.py`가 하고
실패한다. 판정은 `tools/check_coverage_gate.py`가 하고
임계값은 그 파일의 상수다 — CI에서 인자로 덮을 수 없다.

`--cov-fail-under`를 쓰지 않는 이유는 그것이 두 지표를 **합산한 하나의 비율**이기
Expand Down Expand Up @@ -158,8 +158,7 @@ ai-ci / embedding profile parity

위 검사 이름은 GitHub branch protection의 required status checks와 문자열까지
일치해야 한다. 검사를 추가하거나 이름을 바꿀 때는 **이 절을 먼저 고치고** 하위 문서와
GitHub 설정을 거기에 맞춘다 — 순서가 뒤집히면 낡은 값이 하위 문서로 퍼진다
(`S15P11A705-158` 실측).
GitHub 설정을 거기에 맞춘다 — 순서가 뒤집히면 낡은 값이 하위 문서로 퍼진다.

## Feed 협업 경계

Expand All @@ -169,7 +168,7 @@ GitHub 설정을 거기에 맞춘다 — 순서가 뒤집히면 낡은 값이
범위, 개인정보 경계, 후보·필터·fallback·impression 의미, Feed 관련 계약 리뷰.
- 백엔드 파트는 구현을 소유한다 — 후보 조회, scoring 실행, API와 cursor,
requestId, DB·Redis·트랜잭션, impression 저장과 중복 처리, 백엔드 테스트.
- Feed 구현은 `S15P11A705-111` 산하 Task와 `back` 레포 PR로 추적한다.
- Feed 구현은 Jira 작업과 `back` 레포 PR로 추적한다.
레포가 다르면 티켓·브랜치·PR도 분리하고 서로 연결한다.
- Feed 계약 변경은 병합 전에 AI 계약 리뷰어에게, 백엔드 런타임 변경은 백엔드
담당자와 AI 계약 리뷰어에게 요청한다.
Expand Down
84 changes: 42 additions & 42 deletions docs/WORKLOG.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/implements/2026-07-23-fastapi-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ DB 접근은 asyncpg 와 원시 SQL 로 구현했고 ORM 을 도입하지 않았
| core | `db.py` | asyncpg 풀. `search_path=ai, public` 고정(public 은 vector 확장이 있는 스키마이고, core 는 경로 밖에 유지한다. T21 참조). pgvector 타입 등록 |
| core | `errors.py` | 영구/일시 오류와 저장 폐기의 분류 |
| core | `security.py` | 내부 공유 시크릿 미들웨어(`/internal/*` 경로에 적용) |
| client | `embedding_client.py` | GMS 의 OpenAI 호환 `/embeddings` 호출(하네스 `embed.py` 를 포팅했다) |
| client | `llm_client.py` | GMS 의 Gemini `generateContent` 호출. responseSchema 와 thinkingBudget=0 을 사용한다 |
| client | `embedding_client.py` | AI API 의 OpenAI 호환 `/embeddings` 호출(하네스 `embed.py` 를 포팅했다) |
| client | `llm_client.py` | AI API 의 Gemini `generateContent` 호출. responseSchema 와 thinkingBudget=0 을 사용한다 |
| cache | `preset_cache.py` | 기동 시 `is_active` 이고 Profile 이 일치하는 Preset 을 적재한다. BLOCKED 는 제외하고, 적재 결과가 0건이면 기동에 실패한다 |
| repository | `ai_state_repo.py` | 조건부 상태 전이(try_start/complete/fail). 컬럼 조립에는 Stage 열거형만 쓴다 |
| repository | `context_embedding_repo.py` | 검색 Query 와 UPSERT(`is_deleted` 는 갱신 대상에서 제외)와 fallback 조회 |
Expand Down Expand Up @@ -52,7 +52,7 @@ DB 접근은 asyncpg 와 원시 SQL 로 구현했고 ORM 을 도입하지 않았

## 검증 방법

로컬에서 `pgvector/pgvector:pg16` 컨테이너를 띄우고 back 레포의 Flyway 마이그레이션(V1/V100/V101)으로 `ai.*` 테이블을 생성했다. 부트스트랩으로 Preset 27건을 적재한 뒤, 실제 GMS(임베딩·Gemini)를 호출해 end-to-end 로 확인했다.
로컬에서 `pgvector/pgvector:pg16` 컨테이너를 띄우고 back 레포의 Flyway 마이그레이션(V1/V100/V101)으로 `ai.*` 테이블을 생성했다. 부트스트랩으로 Preset 27건을 적재한 뒤, 실제 AI API(임베딩·Gemini)를 호출해 end-to-end 로 확인했다.

**`/search`**

Expand Down
6 changes: 3 additions & 3 deletions docs/implements/2026-07-23-keyword-matching-eval.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

`tools/keyword_eval/` 에 커밋했다. 팀이 실제 샘플로 재실행할 수 있게 하기 위해서다. 구성은 다음과 같다.

- `embed.py` — GMS 임베딩 호출. 결과를 디스크에 캐시한다.
- `embed.py` — AI API 임베딩 호출. 결과를 디스크에 캐시한다.
- `samples.yaml` — 임시 맥락 35개. 프리셋을 아는 작성자가 만든 샘플이라 self-reference 편향이 있으므로, 수치는 절대값이 아니라 경향으로 해석한다.
- `test_a/b/c` — 테스트 3종 실행 코드.
- `prompts/keyword_judgment.md` — 판정 프롬프트.
Expand All @@ -37,14 +37,14 @@

### C-2 — 판정 모델 비교

확정한 프롬프트로 3사 4모델(gpt-5-mini, gpt-5-nano, claude-haiku-4-5, gemini-2.5-flash)을 GMS 경로에서 실행해 비교했다. 정확도(스키마 준수·선택 분포)는 4모델이 사실상 동일했다. 따라서 경량 tier 모델로 충분하다. 그중 가장 빠르고(1.12s) 토큰을 가장 적게 쓴(25314) `gemini-2.5-flash`(thinkingBudget=0)를 확정했다. gpt-5-nano 는 지연이 가장 길고 토큰을 가장 많이 써서 탈락했다. 모델이 반환하는 confidence 값은 모든 모델에서 변별력이 낮아 랭킹 신호로 사용하지 않는다. Gemini 는 function-calling 응답이 malformed 로 나와서, 대신 `responseSchema` 방식으로 호출한다.
확정한 프롬프트로 3사 4모델(gpt-5-mini, gpt-5-nano, claude-haiku-4-5, gemini-2.5-flash)을 AI API 경로에서 실행해 비교했다. 정확도(스키마 준수·선택 분포)는 4모델이 사실상 동일했다. 따라서 경량 tier 모델로 충분하다. 그중 가장 빠르고(1.12s) 토큰을 가장 적게 쓴(25314) `gemini-2.5-flash`(thinkingBudget=0)를 확정했다. gpt-5-nano 는 지연이 가장 길고 토큰을 가장 많이 써서 탈락했다. 모델이 반환하는 confidence 값은 모든 모델에서 변별력이 낮아 랭킹 신호로 사용하지 않는다. Gemini 는 function-calling 응답이 malformed 로 나와서, 대신 `responseSchema` 방식으로 호출한다.

확정 사항은 [P26](../proposals/P26-keyword-preset-judgment.md)에 반영했다(M4 종결).

## 남은 것

- 팀원이 프리셋을 보지 않고 작성한 실제 샘플로 B/C 를 다시 측정해야 한다. 현재 샘플은 self-reference 편향이 있어, Recall 과 트리키 케이스가 실제로 유효한지는 새 샘플로만 검증할 수 있다.
- GMS 모델별 크레딧 단가표가 나오면 토큰 사용량을 비용으로 환산해야 한다([spec/cost-estimate.md](../spec/cost-estimate.md) §4 공식에 대입).
- AI API 모델별 크레딧 단가표가 나오면 토큰 사용량을 비용으로 환산해야 한다([spec/cost-estimate.md](../spec/cost-estimate.md) §4 공식에 대입).

## 관련

Expand Down
4 changes: 2 additions & 2 deletions docs/implements/2026-07-24-e3-test-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

## 하네스 (`tests/`)

- **`conftest.py`** — 세션 스코프의 `PostgresContainer(PGVECTOR_IMAGE)`를 띄운다. 이미지는 현재 `pgvector/pgvector:0.8.5-pg16@sha256:1d53…` 로, 운영 환경과 back 레포 `compose.yaml` 의 이미지와 digest 까지 일치한다(최초에는 `0.8.1-pg16` 이었고 `S15P11A705-122` 에서 정합화했다. 경위는 아래 「결정」 참조). 컨테이너에 `schema/ai_snapshot.sql`(back V1/V100/V101 에서 파생)을 적용한 뒤 asyncpg 풀을 연다. 테스트 간 격리는 TRUNCATE 로 한다. 롤백 격리를 쓰지 않는 이유는 동시성 테스트가 여러 커넥션을 쓰기 때문이다. `settings` fixture 가 Embedding Profile 을 주입하며, 테스트 코드에 Profile 문자열 리터럴을 쓰는 것은 금지다.
- **`conftest.py`** — 세션 스코프의 `PostgresContainer(PGVECTOR_IMAGE)`를 띄운다. 이미지는 현재 `pgvector/pgvector:0.8.5-pg16@sha256:1d53…` 로, 운영 환경과 back 레포 `compose.yaml` 의 이미지와 digest 까지 일치한다(최초에는 `0.8.1-pg16` 이었고 Jira 작업에서 정합화했다. 경위는 아래 「결정」 참조). 컨테이너에 `schema/ai_snapshot.sql`(back V1/V100/V101 에서 파생)을 적용한 뒤 asyncpg 풀을 연다. 테스트 간 격리는 TRUNCATE 로 한다. 롤백 격리를 쓰지 않는 이유는 동시성 테스트가 여러 커넥션을 쓰기 때문이다. `settings` fixture 가 Embedding Profile 을 주입하며, 테스트 코드에 Profile 문자열 리터럴을 쓰는 것은 금지다.
- **`fakes.py`** — `FakeEmbeddingClient`/`FakeLLMClient`. 벡터는 sha256 기반으로 결정론적으로 생성한다. 무작위 벡터를 쓰면 유사도 순서 단언이 실행마다 흔들리기 때문이다. 호출 횟수를 기록한다. 여러 시나리오의 핵심 단언이 "호출하지 않았다" 또는 "정확히 한 번 호출했다"이기 때문이다. `on_call` 훅으로 모델 호출과 저장 사이의 시간 창을 결정론적으로 재현한다. sleep 으로 타이밍을 맞추는 방식은 금지다.
- **`builders.py`** — `make_state`/`make_embedding`/`make_preset`. `embedding_profile`·`is_deleted`·두 status 컬럼을 항상 명시한다. 본문 버전 인자는 두지 않았다. 버전은 설계에서 제거된 개념이라 테스트 빌더가 되살리면 안 되기 때문이다. 본문 수정 시나리오는 `context_id` 가 다른 두 State 로 표현한다.
- **`schema/ai_snapshot.sql`** — 테스트 전용 스키마 스냅샷. `ai` 스키마만 담는다(ai 테이블은 core 로의 FK 가 없다). 파일 헤더에 back 파생 출처와 갱신 누락 위험을 명시했다.
Expand All @@ -33,7 +33,7 @@
## 결정

- **Python 3.12 로 통일하고 상한을 `<3.13` 으로 둔다.** GraphRAG 스택(torch/transformers/igraph)의 wheel 이 최신 Python 을 늦게 지원하므로 3.12 가 안전하다. 3.13 지원이 확산되면 상한 완화를 재검토한다.
- **pgvector 이미지는 `0.8.5-pg16` 에 digest 까지 고정한다.** 운영과 back `compose.yaml` 의 실제 이미지와 digest 까지 일치시켜 재현성을 확보했다. 롤링 태그 `pg16` 은 금지한다. 태그만 고정하는 것도 금지한다. 같은 태그가 다른 이미지를 가리킬 수 있기 때문이다. 경위를 남긴다. 최초 결정은 `0.8.1-pg16` 태그 고정이었고 근거는 "back `compose.yaml` 과 일치한다"였다. 그런데 back#31 이 compose 를 `0.8.5-pg16@sha256:1d53…` 로 올리면서 그 근거가 무효가 됐다(운영도 0.8.5 다. infra#41). 어긋난 쪽이 ai 였으므로 `S15P11A705-122` 에서 digest 까지 맞췄다.
- **pgvector 이미지는 `0.8.5-pg16` 에 digest 까지 고정한다.** 운영과 back `compose.yaml` 의 실제 이미지와 digest 까지 일치시켜 재현성을 확보했다. 롤링 태그 `pg16` 은 금지한다. 태그만 고정하는 것도 금지한다. 같은 태그가 다른 이미지를 가리킬 수 있기 때문이다. 경위를 남긴다. 최초 결정은 `0.8.1-pg16` 태그 고정이었고 근거는 "back `compose.yaml` 과 일치한다"였다. 그런데 back#31 이 compose 를 `0.8.5-pg16@sha256:1d53…` 로 올리면서 그 근거가 무효가 됐다(운영도 0.8.5 다. infra#41). 어긋난 쪽이 ai 였으므로 Jira 작업에서 digest 까지 맞췄다.
- **lock 파일을 도입한다.** 합류자의 환경 재현성을 위해서다. `requirements.txt` 는 사람이 읽는 하한 명세로, lock 은 정확한 버전 고정으로 역할을 나눈다.
- **ai-ci 의 PR 제목 Jira 키 검증.** 형식만 보증하며 티켓이 실제로 존재하는지는 보증하지 않는다. 이 한계는 수용했다.

Expand Down
16 changes: 8 additions & 8 deletions docs/implements/2026-07-27-e2e-verification.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# E2E 검증 — 실제 GMS 경로를 전수 확인했다. 파이프라인·검색은 통과했고, 문서만으로는 기동할 수 없었다
# E2E 검증 — 실제 AI API 경로를 전수 확인했다. 파이프라인·검색은 통과했고, 문서만으로는 기동할 수 없었다

- **상태**: 완료
- **날짜**: 2026-07-27
Expand All @@ -9,7 +9,7 @@

## 무엇을 검증했나

ai#5·#6·#11·#14·#16·#17·#18 로 구현이 끝났지만, 그 시점까지 검증된 것은 Fake 기반 테스트 46개(계약 위반·경합 방어)뿐이었다. 실제 GMS 호출, 프리셋 실제 적재, 실제 임베딩 기반의 검색 품질은 한 번도 실행된 적이 없었다.
ai#5·#6·#11·#14·#16·#17·#18 로 구현이 끝났지만, 그 시점까지 검증된 것은 Fake 기반 테스트 46개(계약 위반·경합 방어)뿐이었다. 실제 AI API 호출, 프리셋 실제 적재, 실제 임베딩 기반의 검색 품질은 한 번도 실행된 적이 없었다.

이 세션은 구현에 참여하지 않은 시각에서 `README.md` 와 `docs/spec/` 만 보고 로컬 기동을 재현했다. 절차를 미리 알려주지 않은 것은 의도적이다. 문서만으로 기동이 가능한지가 검증 대상이었기 때문이다. 따라서 막힌 지점 자체가 이 검증의 주 산출물이다.

Expand Down Expand Up @@ -68,7 +68,7 @@ README 에 `docker build` 만 있고 `docker run` 예시가 없다. 이미지는

### 프리셋 적재 — 통과

`python -m app.bootstrap.load_presets` 를 실행했다(실제 GMS 임베딩 1배치 호출).
`python -m app.bootstrap.load_presets` 를 실행했다(실제 AI API 임베딩 1배치 호출).

```
total | embedding_null | profile_kinds | profile | dims | active
Expand Down Expand Up @@ -229,11 +229,11 @@ INSERT INTO core.boundary_probe VALUES (1); -- INSERT 0 1

"FastAPI 는 `core.*` 에 접근하지 않는다"(README:10, [architecture.md](../spec/architecture.md) §7)는 계약이 로컬에서 검증되지 않는다는 뜻이다. 위반해도 성공하기 때문이다. 이 계약은 현재 코드 리뷰로만 지켜지고 있으며 DB 가 강제하지 않는다.

`search_path = ai, public` 이 `core` 를 검색 경로 밖에 두는 1차 방어선이지만, 스키마를 한정한 참조(`core.foo`)는 그대로 통과한다. 인프라 티켓 `S15P11A705-61`(ai 전용 DB role)의 실증 근거다. 프로브 테이블은 즉시 DROP 했다.
`search_path = ai, public` 이 `core` 를 검색 경로 밖에 두는 1차 방어선이지만, 스키마를 한정한 참조(`core.foo`)는 그대로 통과한다. 인프라 티켓 Jira 작업(ai 전용 DB role)의 실증 근거다. 프로브 테이블은 즉시 DROP 했다.

## Docker

빌드 결과는 360MB, 기동은 정상이며, 기동 로그에 `preset cache loaded: 27 presets` 가 찍혔다. 컨테이너 경유 실제 호출까지 확인했다. `/search` 200(실제 GMS 임베딩), Profile 불일치 422, 시크릿 누락 401.
빌드 결과는 360MB, 기동은 정상이며, 기동 로그에 `preset cache loaded: 27 presets` 가 찍혔다. 컨테이너 경유 실제 호출까지 확인했다. `/search` 200(실제 AI API 임베딩), Profile 불일치 422, 시크릿 누락 401.

README 에 없어 이번에 구성해 검증한 명령이다(F5 발견으로 -59 에 인계했다).

Expand Down Expand Up @@ -295,7 +295,7 @@ ValueError: could not convert string to float: '[0.05609131,0.008399963,...]'
| F6 `PRESET_CACHE_TTL_SEC` 미사용 | 문서-구현 불일치 | -59 → 설정·문서·`.env.example`에서 제거(ai#24) | **해결됨** (ai#24) |
| F4 검색 하한 실측 근거 | 근거 보강 | -59 → `personal-search.md` §6에 0.3143·간격 +0.2120 기재 | **반영됨** (ai#22) |
| 판정 비결정성 | 계약 명시 필요 | 현행 유지(허용) 결정 + -59 → `keyword-preset.md` §4.4 신설 | **반영됨** (ai#22) |
| **F2b 권한 경계 미검증** | 인프라 | **`S15P11A705-61`** (ai 전용 DB role) — 근거는 이 문서 | **미해소** |
| **F2b 권한 경계 미검증** | 인프라 | **Jira 작업** (ai 전용 DB role) — 근거는 이 문서 | **미해소** |
| 하네스-운영 코드 분리 | 구조 | 실측상 결과 차이 없음. 프롬프트 사본 3개는 잔존하며 통합은 별건 | 기록 |
| BLOCKED 제외 실데이터 미검증 | 커버리지 갭 | 프리셋에 BLOCKED 가 생기면 자연 해소 | 기록 |
| T22~T24 환경 이슈 | 재현 가능 | [troubleshooting](../troubleshooting/2026-07-27-e2e-env-issues.md) | 이 PR |
Expand All @@ -304,12 +304,12 @@ ValueError: could not convert string to float: '[0.05609131,0.008399963,...]'

```
pytest -q 46 passed
python -m app.bootstrap.load_presets OK: 27 presets upserted (실제 GMS)
python -m app.bootstrap.load_presets OK: 27 presets upserted (실제 AI API)
uvicorn app.main:app --port 8000 preset cache loaded: 27 presets, /health 200
Context 8건 → /context/process 6.0s에 두 status 전부 COMPLETED
후보 밖 keyword_id 0건
docker build -t pinlog-ai . 360MB
docker run (위 명령) /health 200, /search 200(실 GMS), 422, 401
docker run (위 명령) /health 200, /search 200(실 AI API), 422, 401
```

## 남은 것
Expand Down
Loading
Loading