API 계약 registry
갱신일 2026-08-09 · 기준: ../contracts/ 5개 파일 +
ai@60ee59a·server@1fa7a6a·web contract@398c38e여기가 MVP2 계약 registry다. MVP1/interfaces/api-registry.md는 MVP1 종료 시점 기록으로 동결됐다 — 소급 수정하지 않는다. APP-148 — 초대가 이메일 중심으로 넓어졌다.POST /v1/invitations/accept-by-token이 늘어 경로 37 → 38, operation·schema는 48 → 49다. 초대 상태에EXPIRED가, 초대 목록·알림에expiresAt이 붙었고inviteeName이 nullable이 됐다(미가입자 초대).INVITEE_NOT_FOUND는 폐기됐다 — 미가입자 초대가 정상 경로가 됐기 때문이다. APP-343 — 노트 응답의meetingStartedBy에image가 붙었다. 경로·operation·schema 수는 그대로다. 시작자와participants[]가 이제 같은 네 필드를 쓴다 — 예전에는 시작자만userId·name이라 web이 아바타 이미지를 참여자 목록에서 빌려 썼고, 시작자가 참여자가 아닌 회의에서는 빌릴 데가 없었다. APP-379 — 워크스페이스 멤버 관리가 붙었다.PATCH /v1/workspaces/{workspaceId}/members/{userId}(역할 변경) ·DELETE .../members/{userId}(추방) ·DELETE .../members/me(나가기)가 늘어 경로 38 → 40, operation 49 → 52, schema 49 → 50(ChangeWorkspaceMemberRoleRequest)이다. 셋 다 204 무본문이고 새 오류 코드가 둘 붙었다 —WORKSPACE_MEMBER_NOT_FOUND(404, 워크스페이스는 있고 대상만 멤버가 아님) ·LAST_WORKSPACE_ADMIN(409, ADMIN이 최소 한 명 남아야 함).OWNER역할은 만들지 않았다 — 소유권 이전은 "상대를 ADMIN으로 올리고 내가 나간다" 두 호출이다. 계약 파일을 고치기 전에 이 표부터 본다. 미러를 손으로 고치면 다음 생성에서 덮인다. ⚠️ 생성은./gradlew clean openapi3이다.clean을 빼면build/generated-snippets/의 옛 스니펫이 섞여 삭제된 경로가 계약에 되살아난다 — APP-214 작업 중 실제로meeting-pause·meeting-resume가 부활한 산출물이 나왔다. 복사 전에 경로 수를 센다(현재 39). 스키마 본문은 여기 없다 — 아래 링크가 정본이다.
확인한 사실
1. 파일 전체 — owner · source · status
| 파일 | 경계 | owner | source (원본이 어디) | status | 규모 |
|---|---|---|---|---|---|
| openapi3-server.yml | web → server public REST + ai → server internal REST | heymoa-server | 빌드 생성물 — restdocs-api-spec이 XxxControllerDocsTest에서 생성 | 🟢 구현 반영됨 | 39경로 (public 36 / internal 3) · 51 operation · 50 schema · OpenAPI 3.0.1 · info.version 1.0.0 |
| asyncapi-web-server.yml | web ↔ server 비동기 (전사·노트 토픽 STOMP + 채팅 SSE) | 합본 — 구간마다 다름 | 전사·노트 토픽 STOMP = heymoa-server/asyncapi.yml 미러 / 채팅 SSE = 이 파일이 원본 | 🟢 구현 반영됨 | 8채널 · 8 operation (STOMP 6 / SSE 2) · 2서버 · AsyncAPI 3.0.0 · info.version 2.1.0 |
| asyncapi-server-ai.yml | server ↔ ai 채팅 SSE | heymoa-ai | 계획 문서 — 이벤트 스키마는 web-server 파일을 $ref | 🟡 계획 · 구현이 따라옴 | 1채널 · 1 operation · 이벤트 8종 전부 $ref · AsyncAPI 3.0.0 · info.version 1.1.0 |
| openapi3-ai.yml | server → ai 내부 REST + 분석 결과 callback | heymoa-ai | 수기 원본 — 계약이 먼저, 구현이 따라온다 | 🟡 계획 · 구현이 따라옴 | 3경로 · 3 operation + callback 1 · OpenAPI 3.1.0 · info.version 1.1.0 |
| agent-chat-flow.md | web → server → ai 채팅 루프 | 공동 | 수기 서술 — 계약이 아니라 흐름·경계 규칙 요약 | 🟢 구현 반영됨 | 시퀀스 3종 + 경계 규칙 표 |
heymoa-web/openapi3.yml (구현 레포) | web이 코드 생성에 쓰는 사본 | heymoa-web | 미러 — openapi3-server.yml에서 /internal/** 3경로·3 operation과 전용 schema·의존성 제거 | 🟢 APP-401 반영됨 | 36경로 · 48 operation · 43 schema |
asyncapi-web-server.yml이 합본인 것이 이 registry에서 가장 헷갈리는 지점이다. 같은 파일 안에서 전사·노트 토픽 STOMP 구간은 미러(고치면 덮인다), 채팅 SSE 구간은 원본(여기서 고쳐야 한다)이다. 어느 구간을 만지는지 먼저 확인한다.
회의 상태의 owner는 server다. 목록과 상세의 meetingStatus·meetingStartedAt·
recordedDurationMs·activeSessionStartedAt이 같은 조회 시점 snapshot이며,
meetingStatus는 NOT_STARTED·IN_PROGRESS·PAUSED·ENDED 네 값이다.
web은 기존 recording.started·recording.stopped를 받으면 이벤트 payload로 상태나 시간을
만들지 않고, 현재 노트 상세 query와 캐시된 프로젝트 노트 목록 query를 invalidate한다
(web@fad1d73, recording.stopped는 전사 query도 invalidate). 활성 query만 즉시 refetch하고
비활성 목록은 다음 사용 시 refetch한다.
2. 동기화 방향
흐름을 한 줄로: server 코드 → 생성 → contracts → web 사본 → web 생성 클라이언트. 이 사슬 중간을 손으로 고치면 다음 생성에서 사라진다.
3. 무엇이 바뀌면 무엇을 하나
| 바뀐 것 | 고칠 곳 | 그다음 | 잊으면 |
|---|---|---|---|
| server의 REST 시그니처·DTO | server 코드 (계약 아님) | ./gradlew clean openapi3 → openapi3-server.yml 복사 → web 사본 갱신 → pnpm orval | web 생성 훅이 낡은 채로 남는다 |
| 전사·노트 토픽 STOMP 메시지·목적지 | heymoa-server/asyncapi.yml (원본) | *WebSocketDocsTest 통과 → asyncapi-web-server.yml의 STOMP 구간에 복사 | 미러가 갈라진다 |
| agent 채팅 SSE 이벤트 | asyncapi-web-server.yml (원본) | 끝. asyncapi-server-ai.yml이 $ref하므로 자동 반영 | 두 구간 포맷이 갈라져 passthrough가 깨진다 |
| ai 내부 REST 요청·응답 | openapi3-ai.yml (수기 원본) | ai 구현이 따라간다 | 계약과 구현이 갈라진다 |
| 채팅 흐름의 경계 규칙 | agent-chat-flow.md | 필요하면 interfaces/의 해당 문서도 | 규칙이 구두 전승으로 남는다 |
/internal 경로 추가 | server 코드 | openapi3-server.yml 자동 반영 → web 사본에서 반드시 제거 | web 생성물에 내부 경로가 섞인다 |
계약을 바꿨으면 관련 repo PR과 Linear에 그 commit SHA를 남긴다.
4. 검증
| 파일 | 검증 방법 | 자동인가 |
|---|---|---|
openapi3-server.yml | 생성 자체가 검증 — 코드에서 나오므로 코드와 어긋날 수 없다. ExposedEnumContractTest가 외부 노출 enum 값 집합을 고정 | ✅ server CI (build) |
asyncapi-web-server.yml 전사·노트 토픽 구간 | heymoa-server의 *WebSocketDocsTest가 실제 DTO 직렬화로 원본을 검증 | ✅ server CI |
asyncapi-web-server.yml 채팅 SSE 구간 | 스펙 문법 검증만 — npx --yes @asyncapi/cli validate. server가 passthrough라 자기 DTO가 없어 DocsTest로 검증할 수 없다 | ⚠️ 수동 |
asyncapi-server-ai.yml | 문법 검증 + $ref 해석 | ⚠️ 수동 |
openapi3-ai.yml | 문법 검증만. 수기 원본이라 구현과의 일치는 사람이 지킨다 | ⚠️ 수동 |
heymoa-web/openapi3.yml | pnpm orval이 통과해야 하고, "내부 경로 클라이언트를 만들지 않는다"를 고정하는 테스트가 web에 있다 | ✅ web 로컬 (CI 없음) |
npx --yes @asyncapi/cli validate <파일> # asyncapi 3종
검증 강도가 파일마다 다르다. 생성물 두 개(REST·전사 STOMP)는 코드가 곧 검증이고, 채팅 SSE 3파일은 문법 검증뿐이다. 이 구간이 계약과 구현이 갈라질 수 있는 유일한 지점이며, 실제로 §미결 1이 그 사례다.
5. 소비자 — 누가 이 파일을 읽나
| 파일 | heymoa-web | heymoa-server | heymoa-ai |
|---|---|---|---|
openapi3-server.yml | 코드 생성(사본 경유) — 훅·모델·MSW·faker | 생성 주체 | 내부 조회 API 시그니처 확인 (InternalAgentContext) |
asyncapi-web-server.yml | 수기 zod 프로토콜·MSW 시나리오 구현 | 전사 구간 원본 소유 · SSE 구간 passthrough | 이벤트 스키마의 실질 정본 ($ref 대상) |
asyncapi-server-ai.yml | — | 스트림 소비 규약 확인 | SSE 생산 규약 |
openapi3-ai.yml | — | 호출 시그니처·타임아웃 | 구현 대상 |
agent-chat-flow.md | 승인 UX·게이트 판정 | tee·락·재검증 규칙 | profile 선택·interrupt 규칙 |
web은 어떤 계약도 원본으로 소유하지 않는다 — 전부 소비자다. 그래서 web에서 계약을 고치고 싶을 때는 항상 상류(server 코드 또는 asyncapi-web-server.yml)로 올라가야 한다.
미결
chatKind가 계약에는 있고 구현에는 없다.openapi3-ai.yml이 이 필드로 agent profile이 갈린다고 규정하면서, 같은 파일에 "현재 heymoa-server는 이 필드를 보내지 않는다(요청 DTO에 없다)"고 적어 두었다. 계약 파일이 스스로 미구현을 기록한 유일한 사례이며 status를 🟡로 둔 이유다. APP-172로 MVP2 이월 — 원장: pm/2-product/open-issues.md.agent-chat-flow.md안에 표기가 엇갈리는 곳이 둘 있다. ① tee 개수 — §1 시퀀스는tee 1/2·tee 2/2인데 §3 경계 규칙 표는 셋(USER·ASSISTANT·TOOL)이다. ② keepalive 간격 — "스트림 수명" 행은30초 이하인데 "heartbeat" 행과asyncapi-server-ai.yml은 15초 이하다. interfaces/ 문서들은 각각 셋과 15초를 따랐다. 계약 문서 쪽 정정이 필요하다.- 채팅 SSE 구간에 계약↔구현 자동 검증이 없다. passthrough 구조상 server에 DTO가 없어 생성·검증 모두 불가능하다. 세 서비스 중 어느 쪽이 이 구간의 회귀를 막을지 정해져 있지 않다.
/internal제거가 수작업이다. web 사본을 만들 때 3경로를 손으로 지운다. 자동화 지점이 지정돼 있지 않아 경로가 늘면 누락될 수 있다 — web에 이를 고정하는 테스트가 있지만 사후 탐지이지 예방은 아니다.asyncapi-server-ai.yml의 상대$ref가 파일 배치에 묶여 있다. 같은 폴더의asyncapi-web-server.yml을 파일명으로 참조하므로 둘을 떼어 놓으면 해석이 깨진다. 이관 시 함께 옮겨야 한다.
| 이어서 볼 것 | 어디 |
|---|---|
| 계약 폴더의 파일 목록·성격 | ../contracts/INDEX.md · ../contracts/README.md |
| 세 서비스 공통 규약 | common-conventions.md |
| 경계별 상세 | server-web.md · server-ai.md |
| 계약 설계의 근거 (3-서비스 구조) | MVP1/architecture/system-architecture.md |