Skip to content

docs(contract): declare the publish recovery routes - #1009

Merged
yeongseon merged 3 commits into
mainfrom
docs/contract-publish-recovery-routes
Oct 5, 2026
Merged

yeongseon merged 3 commits into
mainfrom
docs/contract-publish-recovery-routes

Conversation

@yeongseon

Copy link
Copy Markdown
Collaborator

Refs #994 — 이 PR 은 그 이슈의 일부다 (publish 라우트 4개). 남는 것은 아래 "이 PR 에 없는 것".

문제

GET·DELETE /builds/{run_id}/publish/receipt, POST …/publish/reconcile, GET …/publish/audit 는 계약 1.19.0 / 1.20.0 부터 응답해 왔지만 contract/builder-api.yaml 에는 info.description 의 산문으로만 있었다. paths 에 없으니 클라이언트가 대조할 스키마가 없고, _DISPATCH_ROUTES 에도 없어서 라우트 테스트가 이 넷을 몰랐다. Studio 가 publish_state_unknown 복구 UI 를 붙이려면 (kpubdata-lab/kpubdata-studio#728) 이 선언이 먼저다.

변경 내용

  • contract/builder-api.yaml 1.76.0 → 1.77.0 (additive): 네 operation (getPublishReceipt, resetPublishReceipt, reconcilePublish, getPublishAudit) 과 스키마 PublishReceipt, PublishReconcileRequest, PublishReconcileResponse, PublishReceiptReset, PublishAuditLog, PublishAuditEntry. 형태는 service/publish_api.py 와 service/routes/publish.py 가 실제로 돌려주는 것을 읽어 적었다. wire 는 바뀌지 않는다.
  • service/app.py: API_CONTRACT_VERSION = "1.77.0".
  • contract/fixtures/responses.json: 생성기로 다시 만들었다 (2xx 54 → 59, error 37 → 42).
  • tests/unit/test_service_contract.py: _DISPATCH_ROUTES, _REQUIRED_OPERATIONS, _IMPLEMENTED_OPERATIONS, _OPERATION_STATUS_CODES 에 넷을 추가.
  • tests/unit/test_service_publish.py::TestPublishRecoveryConformance: 실제 요청의 응답을 선언한 스키마와 대조한다 — unknown receipt, succeeded receipt(result 포함), receipt_not_found 404 (세 라우트), 쿼리 누락 400, reconcile 세 갈래(원격 있음 / 없음 / 읽을 수 없음 503), 모르는 필드 400, reset 과 그 뒤의 audit. 마지막 테스트는 부정 테스트다: 선언에 없는 state 값이 검사를 실패시키는지.

이 PR 에 없는 것 (#994 에 남는다)

  • X-Provider-Key 를 header 파라미터로 선언
  • 429 auth_throttled, 과부하 503, X-Request-ID 응답 헤더
  • _DISPATCH_ROUTES 를 라우트 어댑터에서 생성 — 라우트가 if 연쇄라 표로 바꾸는 리팩터링이 먼저 필요하다

검증

로컬에서 pytest 를 실행하지 못했다. 이 환경에서 files.pythonhosted.org 에 연결되지 않아 uv sync 가 실패한다 (pypi.org 는 200). 새 테스트는 이 PR 의 CI 가 첫 실행이다. 의존성 없이 돌릴 수 있는 것은 돌렸다:

$ python3 scripts/check_contract_compat.py --base origin/main
contract compatible with origin/main: 1.76.0 -> 1.77.0
$ python3 scripts/generate_response_fixtures.py --check
contract/fixtures/responses.json matches the contract
$ python3 scripts/check_version_consistency.py
버전 일치: pyproject 0.4.0 == CHANGELOG
$ python3 scripts/check_english_comments.py
checked 443 files: no Korean comments or docstrings
$ ruff check tests/unit/test_service_publish.py tests/unit/test_service_contract.py src/kpubdata_builder/service/app.py
All checks passed!

계약 YAML 은 yaml.safe_load 로 읽어 operation 68개 (이전 64개) 를 확인했다.

🤖 Generated with Claude Code

GET and DELETE /builds/{run_id}/publish/receipt, POST .../publish/reconcile
and GET .../publish/audit have answered since contract 1.19.0 and 1.20.0
but were only in the contract's prose. Declare them with their schemas
(1.76.0 -> 1.77.0, additive), add them to the route and status tables,
and check their real responses against the schemas.

Refs #994

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yeongseon yeongseon added review:R2 Public API, builder API, OpenAPI, spec, transformation (POLICY 18.1) epic:trust Trust & Evidence — verification, drift, provenance labels Oct 4, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yeongseon

Copy link
Copy Markdown
Collaborator Author

리뷰 메모 — 리베이스 때 고쳐야 할 것.

계약 버전이 겹칩니다. 이 PR 은 1.76.0 → 1.77.0 으로 올리는데 main 의 API_CONTRACT_VERSION 이 이미 1.77.0 입니다 (#903 의 profile range trim 이 가져갔습니다). 리베이스하면서 1.78.0 으로 올리고 contract/fixtures/responses.json 을 다시 생성해야 합니다. CHANGELOG 항목의 버전도 같이.

스키마 본문(약 400줄)은 publish_api.py 와 줄 단위로 대조하지 못했습니다 — 설명과 파일 구성, 테스트 목록까지만 봤습니다.

@Eomdahyeon

Copy link
Copy Markdown
Collaborator

이 PR 이 main 과 충돌 상태가 된 것은 내가 #1010 을 머지했기 때문이다 — main 의 계약이 1.77.0 이 됐고 이 PR 도 1.77.0 을 쓴다. 미안하다.

#994 의 나머지 중 둘(X-Provider-Key 파라미터, 429 auth_throttled·과부하 503·X-Request-ID)은 #1024 로 올렸다. 그 PR 은 #1022(#996) → #1023(#1000) 위에 쌓여 1.78.0 ~ 1.80.0 을 쓴다. 이 PR 과 그 스택 중 어느 쪽을 먼저 넣을지 정해 주면 내 쪽 버전 줄을 맞추겠다. 원하면 이 브랜치를 main 위로 리베이스하는 것도 내가 할 수 있다 — 남의 브랜치라 요청 없이는 건드리지 않았다.

@yeongseon

Copy link
Copy Markdown
Collaborator Author

순서 메모: #1022 → #1023 → #1024 스택이 계약 1.78.0 ~ 1.80.0 을 씁니다. 그 스택을 먼저 넣기로 했으니, 이 PR 은 리베이스할 때 1.81.0 으로 올리고 fixture 를 다시 생성해야 합니다 (앞선 코멘트의 1.78.0 은 더 이상 맞지 않습니다). studio#744 가 이 PR 의 선언을 기다립니다.

Eomdahyeon added a commit that referenced this pull request Oct 4, 2026
…503 a stable code (#1023)

Closes #1000

> **#1022(#996) 위에 쌓은 PR 이다.** base 를 그 브랜치로 잡아 이 PR 의 diff 만 보이게 했다.
#1022 가 머지되면 base 를 `main` 으로 바꾼다.

## 문제

일부 에러 본문에 `code` 가 없어서 클라이언트가 문장으로 분기해야 한다. 특히 `POST /builds` 의 큐 초과 429
는 `auth_throttled` 429 와 문장으로만 구분됐다.

## 변경 내용 — 이슈의 네 가지

| 경우 | 상태 | `code` |
|---|---|---|
| async build 큐 초과 | 429 | `build_queue_full` |
| 인증 실패 | 401 | `unauthorized`, 토큰의 `exp` 가 지났으면 `token_expired` |
| JWKS 를 가져오지 못함 | 503 | `auth_unavailable` |
| 과부하 (요청을 읽기 전 소켓 응답) | 503 | `server_overloaded` |
| 재시작으로 중단된 run | — | `credentials_required` — #1022 에서 |

- 본문의 `error` 문장은 그대로 둔다. `code` 만 추가된다.
- 401 본문은 이슈가 "확인하지 않은 것"으로 남긴 부분이다. 확인: `{"error": <AuthError.reason>}`
이었고 `code` 가 없었다. `AuthError.code` 프로퍼티가 상태와 사유에서 코드를 정한다.
- `token_expired` 를 따로 둔 이유: 클라이언트가 할 일이 다르다(토큰을 새로 받아 재시도). 나머지 401 은
`unauthorized` 하나다 — 어떤 검증이 실패했는지까지 코드로 나누지 않았다.
- 계약 1.78.0 → **1.79.0** (additive): `Error.code` 설명에 코드 목록,
`Unauthorized` 응답의 예시와 설명, `submitBuild` 429 예시. `Error` 스키마에는 이미 선택적
`code` 가 있어 스키마 구조는 바뀌지 않는다.

## 검증

- `tests/unit/test_stable_error_codes.py`(큐 초과 본문, 과부하 본문),
`test_oidc_auth.py::TestStableAuthCodes`(거부·JWKS 불가·만료 토큰이 실제 응답 본문에 싣는
코드).
- 인증·서비스·계약 fixture·job 관련 테스트 파일: `721 passed`. 전체 스위트는 돌리지 않았다 — CI 가
확인한다.
- `check_contract_compat.py`: `origin/main` 대비 `1.77.0 -> 1.79.0` 호환.
ruff, mypy 통과.

## 겹치는 것

#1009 가 계약의 같은 영역(공용 응답, 버전 줄)을 고친다. 나중에 머지되는 쪽에서 버전 줄을 맞춰야 한다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Eomdahyeon added a commit that referenced this pull request Oct 4, 2026
…03, and the request id (#1024)

Refs #994

> **#1023(#1000) 위에 쌓은 PR 이다** (그 PR 은 #1022 위에 있다). base 를 그 브랜치로 잡아 이
PR 의 diff 만 보이게 했다. 과부하 503 의 `code` 가 #1023 에서 생기기 때문이다.

## 이 PR 이 하는 것 — #1009 가 "이 PR 에 없는 것"으로 남긴 세 항목 중 둘

1. **`X-Provider-Key` 를 header 파라미터로 선언** —
`components.parameters.ProviderKey`. 프로바이더를 호출하는 다섯
operation(`previewBuild`, `createBuild`, `submitBuild`,
`getProviderStatus`, `testProviderConnection`)에 참조를 달았다. Studio 가 이 헤더를
보내는 호출과 같은 다섯이다.
2. **429 `auth_throttled`, 과부하 503, `X-Request-ID`** — 어느 operation 에서나
나올 수 있으므로 `SignupNotApproved` 와 같은 방식(`x-status` 를 가진 공용 응답)으로 선언했다:
`components.responses.AuthThrottled`, `ServerOverloaded`, 그리고
`components.headers.RequestId`.

계약 1.79.0 → **1.80.0**. wire 는 바뀌지 않는다(선언만). response fixture 를 생성기로 다시
만들었다.

## 하지 않은 것

3. **`_DISPATCH_ROUTES` 를 라우트 어댑터에서 생성** — #1009 본문이 적은 대로 라우트가 if 연쇄라
표로 바꾸는 리팩터링이 먼저다. 이 PR 범위에 넣지 않았다. 그래서 `Refs`.

`X-Request-ID` 는 헤더 컴포넌트로만 선언했고 각 operation 의 응답마다 참조를 달지는 않았다 — 모든
응답(수백 개)에 다는 것은 계약을 읽기 어렵게 만든다고 판단했다. 설명에 "핸들러가 쓰는 모든 응답에 실린다"고 적었다.

## 검증 — 선언과 실제를 대조한다

`tests/unit/test_contract_shared_declarations.py`:
- 파라미터의 이름이 코드의 `PROVIDER_KEY_HEADER` 와 같고, 참조한 operation 집합이 정확히 위 다섯이다
- `_overloaded_response()` 의 상태·본문·헤더가 `ServerOverloaded` 선언과 같다
- 인증 실패 한도를 넘긴 실제 응답의 상태·키·`code` 가 `AuthThrottled` 선언과 같다
- 실제 HTTP 응답에 `X-Request-ID` 가 있다

계약 관련 테스트 파일: `424 passed`. `check_contract_compat.py`: `origin/main` 대비
`1.77.0 -> 1.80.0` 호환. ruff, mypy 통과. 전체 스위트는 CI 가 돌린다.

## #1009 와의 관계

#1009 는 지금 `main` 과 충돌 상태다 — 내가 #1010 을 머지하면서 `main` 이 1.77.0 이 됐고,
#1009 도 1.77.0 을 쓴다. 이 스택(#1022 → #1023 → 이 PR)은 1.78.0 ~ 1.80.0 을 쓴다.
어느 쪽이 먼저 들어가든 나중 쪽이 버전 줄을 다시 맞춰야 하니, 순서를 정해 주면 내 쪽을 그에 맞추겠다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The contract version this branch takes moves from 1.77.0 to 1.81.0: main took
1.77.0 to 1.80.0 (#903, #996, #1000, #994). Response fixtures are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yeongseon

Copy link
Copy Markdown
Collaborator Author

main 을 합쳤습니다. 충돌은 네 파일(CHANGELOG.md, contract/builder-api.yaml, contract/fixtures/responses.json, service/app.py)이었고, 계약 버전을 1.81.0 으로 올렸습니다 — main 이 1.77.0 ~ 1.80.0 을 가져갔기 때문입니다. 본문의 "1.76.0 → 1.77.0" 은 이제 "1.80.0 → 1.81.0" 으로 읽어야 합니다.

의존성 없이 돌릴 수 있는 것만 로컬에서 확인했습니다:

$ python3 scripts/generate_response_fixtures.py --check
contract/fixtures/responses.json matches the contract
$ python3 scripts/check_contract_compat.py --base origin/main
contract compatible with origin/main: 1.80.0 -> 1.81.0
$ python3 scripts/check_version_consistency.py
버전 일치: pyproject 0.4.0 == CHANGELOG
$ python3 scripts/check_english_comments.py src tests scripts
checked 450 files: no Korean comments or docstrings

계약은 operation 68개로 읽힙니다. pytest·mypy 는 로컬에서 돌리지 못했습니다 (files.pythonhosted.org 연결 실패로 uv sync 불가) — 이 푸시의 CI 가 첫 실행입니다. 이 PR 이 main 에 들어가면 studio#744 의 Builder contract drift 를 다시 돌릴 수 있습니다.

@yeongseon
yeongseon merged commit 66de8cd into main Oct 5, 2026
22 checks passed
yeongseon added a commit to kpubdata-lab/kpubdata-studio that referenced this pull request Oct 5, 2026
… test (#746)

Refs kpubdata-lab/kpubdata-builder#994, #728

## 문제

kpubdata-lab/kpubdata-builder#1009 가 Builder `main` 에 들어가면서 계약이 1.81.0 이
됐고, publish 복구 라우트 네 개(`getPublishReceipt`, `resetPublishReceipt`,
`reconcilePublish`, `getPublishAudit`)가 이름 붙은 예시와 함께 선언됐다. Studio 의
`Builder contract drift` 잡은 Builder `main` 의 계약을 읽고, 파싱하지도 목록에 올리지도 않은
예시가 있으면 실패한다. 그래서 지금 `main` 기준의 모든 PR 에서 이 잡이 실패한다.

#744 의 잡 로그(재실행, Builder 1.81.0)가 근거다: `no Studio schema for
getPublishReceipt; map it in OPERATION_SCHEMAS`, 그리고 `ERROR_READERS` 에
없는 에러 예시 5개.

순서를 제가 잘못 잡았다 — builder#1023 때처럼 Studio 가 예시를 먼저 알고 있어야 했는데 builder#1009
를 먼저 머지했다.

## 변경

`src/shared/lib/contractDrift.test.ts` 만 바꾼다. Studio 는 `main` 에서 네 라우트 중
어느 것도 부르지 않는다.

- `OPERATION_SCHEMAS`: 네 operation 을 `skip` 으로 (부르지 않으므로 파싱할 본문이 없다).
- `ERROR_READERS`: 에러 예시 5개를 `notHandled` 로, `code`(`receipt_not_found`,
`reconcile_unavailable`)와 `since: "1.81.0"` 과 함께.
- CHANGELOG `[Unreleased]`.

#728(#744)이 `reconcilePublish`·`resetPublishReceipt` 를 부르기 시작하면 그 둘의
`skip` 과 `notHandled` 항목을 실제 스키마·reader 로 바꿔야 한다.

## 검증

**로컬에서 테스트를 돌리지 못했다** — 이 환경에서 `npm ci` 가 실패한다. 이 PR 의 `Builder contract
drift` 잡이 Builder `main`(1.81.0)으로 도는 것이 첫 실행이자 판정이다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
yeongseon pushed a commit to kpubdata-lab/kpubdata-studio that referenced this pull request Oct 5, 2026
…lish page (#744)

Closes #728

## 문제

publish 가 `publish_state_unknown` 으로 끝나면 Studio 는 "자동 재시도하지 마세요, 운영자에게
요청하세요"만 보여 준다. Builder 에는 복구 경로(`reconcile`, receipt `reset`)가 있지만
Studio 가 부르지 않는다. 사용자가 UI 에서 할 수 있는 일이 없다.

## 변경 내용

- `builderApi.reconcilePublish`(`POST …/publish/reconcile`),
`builderApi.resetPublishReceipt`(`DELETE
…/publish/receipt?target&destination`). 둘 다 재시도 없음. 자격 증명은
`X-Publish-Credential` 헤더로만.
- 응답 스키마 `publishReconcileResponseSchema`, `publishReceiptResetSchema` —
계약의 `PublishReconcileResponse`, `PublishReceiptReset` 와 이름으로 짝지어진다.
- 게시 실패 카드에 **복구 패널**(`PublishRecoveryPanel`), `publish_state_unknown` 일
때만:
- **원격 확인** → 있으면 "게시는 이루어졌습니다"(다시 보낼 버튼 없음), 없으면 기록이 지워지고 "다시 게시 준비"
버튼.
- **기록 초기화** → 두 번째 클릭("초기화 확정")이 있어야 실행되고, "이미 올라간 것은 지워지지 않는다"는 경고를 먼저
보여 준다.
  - 원격을 읽지 못하면(503) "아무것도 바뀌지 않았다"고 말하고 재게시 버튼을 주지 않는다.
  - 기록이 없으면(404 `receipt_not_found`) 정리할 것이 없다고 말한다.
- 요청에서 자격 증명을 받는 배포에서는 원격 확인에 토큰이 필요하다. Studio 는 게시 시작 때 토큰을 버리므로(#615)
다시 입력하게 하고, 그 한 번의 요청에만 쓰고 다시 버린다.
- **어느 것도 자동으로 실행되지 않는다.** "다시 게시 준비"도 사용자가 누르고, 누른 뒤에는 원래의 확인 단계부터 다시
거친다.
- ko/en 문구, CHANGELOG, `EXPECTED_OPERATIONS` 에 두 호출 추가.

## 의존 — kpubdata-lab/kpubdata-builder#1009

네 라우트는 Builder 에서 1.19/1.20 부터 응답해 왔지만 계약 `paths` 에는 #1009 가 처음 선언한다. 그
PR 이 Builder `main` 에 들어가기 전까지:

- 이 PR 의 드리프트 검사는 두 스키마를 대조할 대상이 없어 그냥 지나간다(실패하지 않는다).
- #743(클라이언트의 모든 경로가 계약에 있는지 검사)이 먼저 머지되면, 이 PR 은 그 검사에서 **실패한다** — 두
경로가 아직 계약에 없으므로. #1009 가 먼저 들어가야 한다.

#1009 의 계약 파일로 드리프트 검사를 로컬에서 돌려 봤다: 스키마 대조 461개 통과. 실패한 2개는 계약 파일만 복사하고
fixture 디렉터리를 같이 두지 않아서 난 "fixture 가 옆에 있는가" 검사다 — 스키마와는 무관하다.

## 검증

- `__tests__/publishRecovery.test.tsx`(16개): 요청의 method·path·body·헤더,
토큰이 URL·body 에 없음, 404/503 분류와 재시도 없음, 다른 run 의 응답 불신, Builder 오류 문구를
그대로 보여 주지 않음, 패널이 누르기 전에는 아무것도 부르지 않음, reset 의 두 단계, 토큰 없이는 원격 확인을 하지
않음.
- `npx vitest run`: `238 files passed, 2405 passed | 9 skipped`. `tsc
--noEmit`, eslint 통과.
- 실제 Builder 와 실제 Hugging Face 로 확인한 것은 아니다. 브라우저에서 화면을 직접 보지도 않았다.

`review:R3` — 작성자가 아닌 사람의 승인이 필요하다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Eomdahyeon <213566566+Eomdahyeon@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

epic:trust Trust & Evidence — verification, drift, provenance review:R2 Public API, builder API, OpenAPI, spec, transformation (POLICY 18.1)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants