From 9a9a32eb5d81d75c8a8bad629a37b8df9d4ae909 Mon Sep 17 00:00:00 2001 From: colosair Date: Thu, 30 Jul 2026 12:01:30 +0900 Subject: [PATCH] =?UTF-8?q?docs(S15P11A705-158):=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EA=B2=80=EC=82=AC=20=EB=91=98=EC=9D=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B8=EC=97=90=20=EB=B0=98=EC=98=81=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=ED=95=98=EC=9C=84=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ai#42 병합 후 main·dev 양쪽 required status checks 에 ai-ci / embedding profile parity 가 추가됐으나 정본이 갱신되지 않았다. 그래서 낡은 값이 CONTRIBUTING.md 에서 workflow.md·P44 로 퍼졌다. 직전 작업에서 갭을 관측했지만 정본 재개정이 범위 밖이라 하위 문서를 낡은 정본에 맞춰 둔 상태였다. 이번에는 정본을 먼저 고치고 하위를 거기에 맞춘다. 실제 설정을 API 로 직접 대조한 결과 main 과 dev 가 완전히 동일했다 — strict, 검사 둘, 미해결 대화 차단, 관리자 포함, 승인 0. 그래서 검사 목록만 고치지 않고 관리자 포함 보호를 main 전용으로 적던 서술도 함께 정정했다. 이것도 틀린 값이었다. 원인은 같은 조건을 두 bullet 에 중복 기재해 한쪽만 낡을 수 있는 구조였다. 조건은 공통으로 한 번만 적고 bullet 은 무엇을 병합하는가 차이만 남겼다. 검사 이름이 protection 과 문자열까지 일치해야 하며 개편 시 정본을 먼저 고친다는 순서 규칙도 박아 두었다. P44 의 "문서와 실제 GitHub 설정 불일치" 완화가 실제로 실패한 사례다. "API 로 재조회" 는 설정을 읽는 것까지만 다루고 문서를 고치는 주체를 정하지 않았다. 완화를 설정을 바꾼 사람이 같은 작업에서 정본을 갱신한다로 강화하고 발생 사실을 각주로 남겼다. WORKLOG 의 직전 줄은 그 시점의 사실 기록이라 고치지 않고 새 줄을 더했다. 이 파일은 merge=union 이라 기존 줄 수정에 중복 위험이 있다 — 직전 작업에서 문서화한 주의의 첫 적용이다. Co-Authored-By: Claude Opus 5 --- CONTRIBUTING.md | 26 +++++++++++++------ docs/WORKLOG.md | 1 + docs/development/workflow.md | 5 ++-- .../proposals/P44-ai-repository-governance.md | 8 +++++- 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25cef54..319c94d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 협업 경계 diff --git a/docs/WORKLOG.md b/docs/WORKLOG.md index 3a72470..92e8218 100644 --- a/docs/WORKLOG.md +++ b/docs/WORKLOG.md @@ -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) | diff --git a/docs/development/workflow.md b/docs/development/workflow.md index 342208c..9d6a55c 100644 --- a/docs/development/workflow.md +++ b/docs/development/workflow.md @@ -12,7 +12,7 @@ Jira 발급·담당자 지정 → 전체 Regression 검증 → 영구 문서와 WORKLOG 갱신 → Draft PR 및 계약 소유자 리뷰 요청 - → strict ai-ci / check와 리뷰 대화 해결 + → strict 상태 검사 둘과 리뷰 대화 해결 → squash 병합·브랜치 삭제 → Jira Done 상태 확인 ``` @@ -51,7 +51,8 @@ reviewer로 지정한다. PR 생성 후 Jira `In Progress`를 확인한다. 병합 전 다음 조건을 모두 확인한다. -- 최신 `dev` 기준 `ai-ci / check` 성공 +- 최신 `dev` 기준 필수 상태 검사 둘 성공 — `ai-ci / check`, + `ai-ci / embedding profile parity` - 모든 리뷰 대화 해결 - PR 본문의 검증 증거와 리스크가 최신 상태 - 필요한 영구 문서와 후속 Jira 티켓 존재 diff --git a/docs/proposals/P44-ai-repository-governance.md b/docs/proposals/P44-ai-repository-governance.md index 47a9da4..83e6ab9 100644 --- a/docs/proposals/P44-ai-repository-governance.md +++ b/docs/proposals/P44-ai-repository-governance.md @@ -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%