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
26 changes: 18 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,26 @@ PR 본문에는 Jira, 변경 목적, RED/GREEN/Regression 증거, 번호가 붙
확인한다. 의견마다 반영 내용 또는 반영하지 않은 근거를 답하고, 모든 리뷰 대화를
병합 전에 해결한다. 사후 리뷰를 정상 절차로 사용하지 않는다.

병합 조건은 두 브랜치를 구분한다.
병합 조건은 `dev`와 `main`이 같다. 두 브랜치 모두 strict 상태 검사 **둘**, 미해결
대화 없음, 관리자 포함 보호 적용, 필수 승인 수 0이다.

- **`dev`** — strict `ai-ci / check`, 미해결 대화 없음. 통과하면 squash로 병합하고
기능 브랜치를 삭제한다. 일상 작업의 병합 대상은 여기다.
- **`main`** — strict `ai-ci / check`, 미해결 대화 없음, 관리자 포함 보호 적용.
대상은 `dev`의 릴리스 병합이며 기능 브랜치를 직접 병합하지 않는다. 이 병합이
이미지 publish를 유발하므로 `dev`에서 CI가 통과한 상태만 올린다.
```text
ai-ci / check
ai-ci / embedding profile parity
```

필수 승인 수가 0이므로 리뷰 요청과 실제 검토 여부는 Jira와 PR에서 명시적으로
확인한다. 다른 것은 조건이 아니라 **무엇을 병합하는가**다.

- **`dev`** — 일상 작업의 병합 대상. 조건을 통과하면 squash로 병합하고 기능 브랜치를
삭제한다.
- **`main`** — 대상은 `dev`의 릴리스 병합이며 기능 브랜치를 직접 병합하지 않는다. 이
병합이 이미지 publish를 유발하므로 `dev`에서 CI가 통과한 상태만 올린다.

두 브랜치 모두 필수 승인 수는 0이므로 리뷰 요청과 실제 검토 여부는 Jira와 PR에서
명시적으로 확인한다.
위 검사 이름은 GitHub branch protection의 required status checks와 문자열까지
일치해야 한다. 검사를 추가하거나 이름을 바꿀 때는 **이 절을 먼저 고치고** 하위 문서와
GitHub 설정을 거기에 맞춘다 — 순서가 뒤집히면 낡은 값이 하위 문서로 퍼진다
(`S15P11A705-158` 실측).

## Feed 협업 경계

Expand Down
1 change: 1 addition & 0 deletions docs/WORKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@
| 2026-07-30 | BD-39 가 *"두 값이 같다"* 로 명시한 명제를 사람의 눈에서 CI 로 옮겼다. Embedding Profile 정본을 `back` 의 `application.yml` 리터럴로 두는 (a)안은 그대로 두고 **대조 장치만** 붙인다 — `back#98` 리뷰가 "그 명제를 지키는 장치가 현재 사람의 눈뿐"이라고 지적한 것에 대한 값싼 응답이다. 두 레포가 모두 public 이라 raw endpoint 를 **무인증**으로 읽는다. 토큰을 쓰지 않은 것은 편의가 아니라 경계다 — 주면 `ai` CI 가 타 레포 접근 권한을 상시로 들고 다닌다(대신 `back` 이 private 이 되면 조회 실패로 exit 1 이며 조용히 통과하지 않는다). **런타임 값이 아니라 선언된 리터럴을 비교한다** — 양쪽 다 환경변수 덮어쓰기를 허용하므로 프로세스 환경이 결과를 바꾸면 CI 가 무엇을 검증하는지 알 수 없게 된다. `ai` 쪽은 `Settings()` 를 인스턴스화하지 않고 필드 선언만 읽는다(인스턴스화는 환경변수와 profile 정합 검증을 함께 돌린다). 조회 실패도 exit 1 로 두었다. 실제 두 값이 같은 동안 이 잡은 늘 초록이라 대조기의 탐지 능력 자체는 검증되지 않은 채 남으므로, 네트워크를 타지 않는 14 케이스로 그것을 못박고 **CI 에서 실제 RED 를 관측했다**(run `30507610160` — 리터럴을 `v1`→`v2` 로만 어긋내 `config.py` 의 profile 정합 검증은 통과시킨 채 새 잡만 붉게 만들었다). 리뷰가 함께 제안한 **"기동 시 FastAPI 조회 대조"는 채택하지 않았다** — `app/api/probe.py` 가 *"credential·endpoint·profile 값을 어떤 분기에서도 싣지 않는다"* 로 비노출을 명시하므로 Profile 노출 엔드포인트 신설은 그 결정의 개정이 되고, 이 티켓 범위 밖이다. BD-39 문서 자체도 개정하지 않았다. `ai#40` 줄 누락도 함께 메웠다 (S15P11A705-156) | [ai#40](https://github.com/Team-PinLog/ai/pull/40), [back#98 리뷰](https://github.com/Team-PinLog/back/pull/98), [probe.py](../app/api/probe.py) |
| 2026-07-30 | 두 클라이언트가 HTTP 상태 코드를 **정반대로** 분류하던 것을 명세에 맞췄다. `embedding` 은 `>= 500` 만 Transient 로 보아 **`429` 한 번에 Context 가 영구 실패**했고(`failure-recovery` §2.1 은 Transient), `llm` 은 **모든** non-200 을 Transient 로 보아 **인증 실패가 재스캔 주기 5분마다 GMS 호출을 만들었다**(§2.2 는 `400`·`401`·`403` 을 Permanent). 원인은 두 클라이언트가 각자 상태 코드 표를 들고 있었던 것이라, 매핑을 `errors.py` 의 `classify_http_status` 한 곳으로 모았다 — §2 가 *"`errors.py` 에서 분류한다"* 로 지목한 파일이다. `429` 를 `>= 500` **보다 먼저** 판정해야 4xx 로 떨어지지 않으므로 그 순서를 회귀 테스트로 못박았다. 재시도(§3.1, 총 3회·`0.5→1.0s`·상한 `4.0s`·full jitter)는 `client/retry.py` 에 넣고 **재시도 대상을 `TransientError` 한 종류로 뒀다** — §3.1 의 대상 목록이 §2.1 의 Transient 집합과 같으므로 재시도용 상태 코드 표를 두 번째로 만들지 않았다. **표가 둘이면 갈라지고, 갈라진 결과가 위 두 오분류였다.** 백오프 값을 `Settings` 로 열지 않은 것은 §3.2 의 상한(*"두 호출의 타임아웃 합 + 재시도 시간 < PROCESSING 만료 600s"*)에 묶인 값이기 때문이다 — env 로 열면 그 상한이 배포마다 달라진다. 현재 최악값 `3 × (60 + 90) + 2 × 1.5 = 453s` 이고 **부등식 자체를 테스트가 지킨다**. 대신 `RetryPolicy` 를 생성자 인자로 받아 테스트가 `sleep`·`jitter` 를 주입한다(실제로 잠드는 테스트를 만들지 않는다). 구조화 출력 위반은 `SchemaViolationError(TransientError)` 로 두어 재시도 중엔 Transient 로 동작하고 소진 시 `judge` 가 `PermanentError` 로 **승격**한다 — LLM 출력은 비결정론적이라 재요청에 성공 여지가 있고, 승격을 client 안에서 끝내야 service 가 보는 분류가 §2 의 두 종류로 유지된다(하위 타입인 채 새면 `except TransientError` 가 먼저 잡아 무한 재판정이 되므로 그것도 테스트로 막았다). Embedding 응답 형식 위반은 프로바이더가 같은 형식으로 답하므로 재시도 없이 즉시 Permanent 다. **결함의 근본 원인은 테스트였다** — `fakes.py` 의 `raise_exc` 가 어느 테스트에서도 쓰이지 않아(grep 0건) Transient/Permanent 파이프라인 경로가 한 번도 실행된 적이 없었다. 그 경로를 실제로 주입하자 **티켓에 없던 결함이 하나 더 드러났다**: `keyword_service` 에 `PermanentError` 핸들러가 아예 없어(`context_processing` 광범위 except 없음, `context.py` 는 `BackgroundTasks.add_task`) `llm_client` 만 고치면 401 이 **BackgroundTasks 까지 새어 트레이스백만 남기고 단계는 PROCESSING 에 머문다** — 고치려던 무한 재시도가 경로만 바꿔 남는 구조였다(되돌려 RED 확인). `embedding_service` 는 이 결선을 이미 갖고 있어 비대칭이던 쪽만 맞췄고 **상태 전이 규칙은 바꾸지 않았다**. 로그 레벨도 §2.1 WARN·§2.2 ERROR 로 맞췄다(둘 다 INFO/WARNING 이어서 일시 장애가 묻히고 배포 설정 문제가 알림에 오르지 않았다). client HTTP 계층 테스트는 신설했고 이는 `integration-tests` §4.2(*"HTTP 레벨 목이 아니라 인터페이스 레벨 Fake"*)와 **충돌이 아니다** — §4.2 는 *파이프라인이 client 를 무엇으로 대체하는가* 의 규칙이고, 인터페이스 Fake 는 client 를 통째로 대체하므로 `_embed_batch` 가 429 를 어떻게 분류하는지 볼 수 없다. **`docs/spec/` 은 고치지 않았다**(§4.2 의 계층 구분 명문화는 후속 별건). `132 passed`(착수 baseline 74), Docker 29.6.1 Testcontainers 전량. **Circuit Breaker 와 타임아웃 60/90 재산정은 티켓 제외 범위** — 전자는 인증 실패 같은 전 서비스 영향 오류가 개별 Context FAILED 로 누적되는 문제를 남기고, 후자는 재시도 도입으로 최악값이 150s → 453s 가 된 것과 함께 별건이다 (S15P11A705-121) | [구현 리포트](implements/2026-07-30-retry-and-error-classification.md), [spec/failure-recovery.md](spec/failure-recovery.md), [tests/README](../tests/README.md), [ai#44](https://github.com/Team-PinLog/ai/pull/44) |
| 2026-07-30 | `dev` 전환의 마지막 조각 — `CONTRIBUTING.md` 가 2단 구성으로 개정된 뒤 `main` 을 가리킨 채 남은 문서 6곳을 정합시켰다(`docs/development/workflow.md` 4·8·30·33·53행, `P44` 52행). **`image publish` 의 `main` 기준 서술은 유지했다** — `infra` 의 `ai-image-update.yaml` 이 `test "$SOURCE_BRANCH" = main` 으로 어서션하므로 이것까지 `dev` 로 바꾸면 GitOps 반영이 끊긴다. `workflow.md` 는 이제 "`dev` 에 병합하는 실행 순서" 이므로, 그 문서만 읽는 사람이 `main` 이 언제 무엇을 받는지 모르게 되는 것을 막기 위해 §5 에 릴리스 병합(`dev`→`main`)과 publish 가 거기서 일어난다는 서술을 한 줄 넣었다. `P44` 는 `상태: Accepted` 인 결정 문서라 표 값만 조용히 바꾸지 않고, 작성 시점(2026-07-28)에는 `main` 단일 구성이었고 2단 전환이 `-154`·`-158` 에서 이뤄졌다는 각주를 함께 남겼다 — `implements/` 와 달리 `proposals/` 에 삭제 금지 원칙은 없지만, Accepted 결정문을 이력 없이 고치면 무엇이 언제 정해졌는지가 사라진다. **`ai-ci / check` 와 `ai-ci / embedding profile parity` 둘 다 required 로 확인**했으나(`main`·`dev` 동일) `CONTRIBUTING.md` 병합 조건 절은 여전히 `ai-ci / check` 하나만 적고 있다 — 정본 재개정은 이 티켓 범위 밖이라 문서를 정본에 맞춰 두고 갭만 올린다 (S15P11A705-158) | [workflow](development/workflow.md), [P44](proposals/P44-ai-repository-governance.md), [CONTRIBUTING](../CONTRIBUTING.md) |
| 2026-07-30 | required status checks 가 둘인데 문서가 하나만 적던 것을 정정했다. `ai#42` 병합 후 `main`·`dev` 양쪽 protection 에 `ai-ci / embedding profile parity` 가 추가됐으나 정본이 갱신되지 않아, **낡은 값이 `CONTRIBUTING.md`→`workflow.md`→`P44` 로 퍼졌다** — 직전 작업(위 줄)에서 갭을 관측했지만 정본 재개정이 범위 밖이라 하위 문서를 낡은 정본에 맞춰 둔 상태였다. 이번에 **정본을 먼저 고치고 하위를 거기에 맞췄다.** 실제 설정을 API 로 직접 대조한 결과 `main`·`dev` 가 **완전히 동일**했다(strict·검사 둘·미해결 대화 차단·관리자 포함·승인 0). 그래서 검사 목록만 고치지 않고 **`관리자 포함 보호` 를 `main` 전용으로 적던 서술도 함께 정정**했다 — 이것도 틀린 값이었고, 원인은 같은 조건을 두 bullet 에 중복 기재해 한쪽만 낡을 수 있는 구조였다. 조건은 공통으로 한 번만 적고 bullet 은 "무엇을 병합하는가" 차이만 남겼다. `P44` 의 *"문서와 실제 GitHub 설정 불일치"* 완화가 **실제로 실패한 사례**이므로("API 로 재조회" 는 설정을 읽는 것까지만 다루고 문서를 고치는 주체를 정하지 않았다) 완화 자체를 "설정을 바꾼 사람이 같은 작업에서 정본을 갱신한다" 로 강화하고 발생 사실을 각주로 남겼다. 정본에는 검사 이름이 protection 과 문자열까지 일치해야 하며 개편 시 이 절을 먼저 고친다는 순서 규칙을 박아 두었다. 위 줄은 그 시점의 사실 기록이라 고치지 않고 새 줄을 더했다 — WORKLOG 가 `merge=union` 이라 기존 줄 수정은 중복 위험이 있다(직전 작업에서 문서화한 주의의 첫 적용) (S15P11A705-158) | [CONTRIBUTING](../CONTRIBUTING.md), [workflow](development/workflow.md), [P44](proposals/P44-ai-repository-governance.md) |
5 changes: 3 additions & 2 deletions docs/development/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Jira 발급·담당자 지정
→ 전체 Regression 검증
→ 영구 문서와 WORKLOG 갱신
→ Draft PR 및 계약 소유자 리뷰 요청
→ strict ai-ci / check와 리뷰 대화 해결
→ strict 상태 검사 둘과 리뷰 대화 해결
→ squash 병합·브랜치 삭제
→ Jira Done 상태 확인
```
Expand Down Expand Up @@ -51,7 +51,8 @@ reviewer로 지정한다. PR 생성 후 Jira `In Progress`를 확인한다.

병합 전 다음 조건을 모두 확인한다.

- 최신 `dev` 기준 `ai-ci / check` 성공
- 최신 `dev` 기준 필수 상태 검사 둘 성공 — `ai-ci / check`,
`ai-ci / embedding profile parity`
- 모든 리뷰 대화 해결
- PR 본문의 검증 증거와 리스크가 최신 상태
- 필요한 영구 문서와 후속 Jira 티켓 존재
Expand Down
8 changes: 7 additions & 1 deletion docs/proposals/P44-ai-repository-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,19 @@ Jira, GitHub, 레포 영구 문서만 공동 진실 원천으로 사용한다.
| coverage 수치 맞추기용 테스트 증가 | 계약·실패 경로와 Testcontainers 통합 테스트를 우선하고 제외 확대를 리뷰한다. |
| CI 시간 증가 | 먼저 측정하며, 80% 차단은 테스트 보강 후 별도 Jira·PR에서 활성화한다. |
| Feed와 운영체계 변경 충돌 | 별도 티켓·브랜치·worktree로 병렬 진행하고 운영체계 병합 후 최신 `dev`를 반영한다. |
| 문서와 실제 GitHub 설정 불일치 | branch protection 변경 직후 API로 설정을 재조회한다. |
| 문서와 실제 GitHub 설정 불일치 | 설정을 바꾼 사람이 **같은 작업에서** `CONTRIBUTING.md` 병합 조건 절을 갱신하고, 변경 직후 API로 재조회해 문서와 대조한다. |

※ 이 문서 작성 시점(2026-07-28)의 브랜치는 `main` 단일 구성이었다. 2단 구성
(`dev` 통합 / `main` 배포) 전환은 `S15P11A705-154`·`S15P11A705-158`에서 이뤄졌고,
위 표의 브랜치 서술은 그 결과 상태다. 브랜치 규칙의 정본은
[`CONTRIBUTING.md`](../../CONTRIBUTING.md)다.

※ **"문서와 실제 GitHub 설정 불일치" 리스크는 실제로 발생했다.** `ai#42` 병합 후
required status checks에 `ai-ci / embedding profile parity`가 추가됐으나 정본이
갱신되지 않아, 낡은 값이 `CONTRIBUTING.md`·`workflow.md`·이 문서로 퍼졌다
(`S15P11A705-158`에서 정정). 원래의 완화("API로 재조회")는 **설정을 읽는 것까지만**
다루고 문서를 고치는 주체를 정하지 않아 구멍이 남았으므로 위 표를 그에 맞게 고쳤다.

## 단계적 coverage 기준

S15P11A705-108의 최초 기준선은 Python 3.12, 52 tests에서 line 76.95%
Expand Down
Loading